All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.lang.protocol.ConsumerUtil Maven / Gradle / Ivy


/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2011, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.lang.protocol;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;

/**
 * Various {@link Consumer}-related utility methods.
 */
public final
class ConsumerUtil {

    private
    ConsumerUtil() {}

    /**
     * @return A consumer that forwards the subjects to both {@code delegate1} and {@code delegate2}
     */
    public static  ConsumerWhichThrows
    tee(final ConsumerWhichThrows delegate1, final ConsumerWhichThrows delegate2) {

        return new ConsumerWhichThrows() {

            @Override public void
            consume(T subject) throws EX {
                delegate1.consume(subject);
                delegate2.consume(subject);
            }
        };
    }

    /**
     * @return A consumer that forwards the subjects to both {@code delegate1} and {@code delegate2}
     */
    public static  Consumer
    tee(final Consumer delegate1, final Consumer delegate2) {

        return new Consumer() {

            @Override public void
            consume(T subject) {
                delegate1.consume(subject);
                delegate2.consume(subject);
            }
        };
    }

    /**
     * @return A consumer that forwards the subjects to all the {@code delegates}
     */
    public static  ConsumerWhichThrows
    tee(final Collection> delegates) {

        return new ConsumerWhichThrows() {

            @Override public void
            consume(T subject) throws EX {

                for (ConsumerWhichThrows delegate : delegates) {
                    delegate.consume(subject);
                }
            }
        };
    }

    /**
     * Converts the source into a {@link ConsumerWhichThrows ConsumerWhichThrows<T, EX>}.
     * 

* This is always possible, because the source is only allowed to throw unchecked exceptions. *

* * @param The element type * @param The target consumer's exception * @deprecated Superseded by {@link #widen2(ConsumerWhichThrows)}. */ @Deprecated public static ConsumerWhichThrows asConsumerWhichThrows(final Consumer source) { return new ConsumerWhichThrows() { @Override public void consume(T subject) { source.consume(subject); } }; } /** * Converts the source into a {@link ConsumerWhichThrows ConsumerWhichThrows<T, EX>}. *

* This is always possible, because the source consumes a superclass of T, and the * source is only allowed to throw a subclass of EX. *

* * @param The element type * @param The target consumer's exception * @deprecated Not necessary if you declare variables, fields an parameters as "{@code ConsumerWhichThrows}" */ @Deprecated public static ConsumerWhichThrows widen(final ConsumerWhichThrows source) { @SuppressWarnings("unchecked") ConsumerWhichThrows result = (ConsumerWhichThrows) source; return result; } /** * Converts the source into a {@link ConsumerWhichThrows ConsumerWhichThrows<T, EX>}. *

* This is always possible, because the source consumes a superclass of T, and the * source is only allowed to throw a {@code RuntimeException}s. *

* * @param The element type * @param The target consumer's exception */ public static ConsumerWhichThrows widen2(final ConsumerWhichThrows source) { @SuppressWarnings("unchecked") ConsumerWhichThrows result = (ConsumerWhichThrows) source; return result; } /** * Converts the source into a {@link Consumer Consumer<T>}. *

* This is always possible, because both ar allowed to throw {@link RuntimeException}s. *

* * @param The element type * @param The source consumer's exception */ public static Consumer asConsumer(final ConsumerWhichThrows source) { @SuppressWarnings("unchecked") Consumer result = (Consumer) source; return result; } /** * @return A {@link Writer} which forwards the characters to a {@link ConsumerWhichThrows * ConsumerWhichThrows<Character, IOException>} */ @NotNullByDefault(false) public static Writer characterConsumerWriter(final ConsumerWhichThrows delegate) { return new Writer() { @Override public void write(int c) throws IOException { delegate.consume((char) c); } @Override public void write(char[] cbuf, int off, int len) throws IOException { for (; len > 0; len--) this.write(cbuf[off++]); } @Override public void flush() { } @Override public void close() { } }; } /** * Creates and returns a {@link Consumer Consumer<Character>} which aggregates characters to lines, which it * passes to the {@code delegate}. *

* Notice that iff the last consumed character is neither a CR nor an LF (a.k.a. "last line lacks a line * separator"), then that last line will not be sent to the delegate. *

*/ public static ConsumerWhichThrows lineAggregator(final ConsumerWhichThrows delegate) { return new ConsumerWhichThrows() { private final StringBuilder sb = new StringBuilder(); private boolean crPending; @Override public void consume(Character c) throws E { if (c == '\r') { delegate.consume(this.sb.toString()); this.sb.setLength(0); this.crPending = true; } else if (c == '\n') { if (this.crPending) { this.crPending = false; } else { delegate.consume(this.sb.toString()); this.sb.setLength(0); } } else { this.crPending = false; this.sb.append(c); } } }; } /** * @return A {@link Consumer} that writes lines to the given file with the default character encoding */ public static ConsumerWhichThrows lineConsumer(File file, boolean append) throws IOException { return ConsumerUtil.lineConsumer(new FileWriter(file, append), true); } /** * @return A {@link Consumer} that writes lines to the given file with the given encoding */ public static ConsumerWhichThrows lineConsumer(File file, String charsetName, boolean append) throws IOException { return ConsumerUtil.lineConsumer(new FileOutputStream(file, append), charsetName, true); } /** * @return A {@link Consumer} that writes lines to the given {@link OutputStream} with the given encoding */ private static ConsumerWhichThrows lineConsumer(OutputStream stream, String charsetName, boolean closeOnFinalize) throws UnsupportedEncodingException { return ConsumerUtil.lineConsumer(new OutputStreamWriter(stream, charsetName), closeOnFinalize); } /** * @return A {@link Consumer} that writes strings to the given {@link Writer}, augmented with a line separator */ public static ConsumerWhichThrows lineConsumer(final Writer writer, final boolean closeOnFinalize) { return new ConsumerWhichThrows() { @Override public void consume(String line) throws IOException { writer.write(line + ConsumerUtil.LINE_SEPARATOR); // Write line and line separator atomically. writer.flush(); } @Override protected void finalize() throws Throwable { if (closeOnFinalize) writer.close(); } }; } private static final String LINE_SEPARATOR = System.getProperty("line.separator"); /** * @return A {@link Consumer} that writes strings to the given {@link Writer}, augmented with a line separator */ public static Consumer lineConsumer(final PrintStream printStream, final boolean closeOnFinalize) { return new Consumer() { @Override public void consume(String line) { printStream.println(line); } @Override protected void finalize() { if (closeOnFinalize) printStream.close(); } @Override public String toString() { return printStream.toString(); } }; } /** * The returned producer is a factory for consumers of {@code T}. The subjects sent to these consumers are * forwarded immediately to the given {@code target}. * * @see #combineInOrder(ConsumerWhichThrows) */ public static Producer> combine(final Consumer target) { return new Producer>() { @Override public Consumer produce() { return new Consumer() { @Override public void consume(T subject) { target.consume(subject); } }; } }; } /** * The returned producer is a factory for consumers of {@code T}. The first subject sent to each of these consumers * is passed to the given {@code target} in the order the consumers were created (not in the order in * which the subjects were sent to the consumers). * * @see #combine(Consumer) */ public static Producer> combineInOrder(final ConsumerWhichThrows target) { final Queue> outstanding = new LinkedList>(); final Map, T> postponed = new HashMap, T>(); return new Producer>() { @Override public ConsumerWhichThrows produce() { ConsumerWhichThrows consumer = new ConsumerWhichThrows() { @Override public void consume(T subject) throws EX { synchronized (outstanding) { if (outstanding.isEmpty()) { throw new IllegalStateException("Can consume only one subject"); } if (outstanding.peek() == this) { // The call appeared "at the right time"; pass the subject immediately to the // target consumer. outstanding.remove(); target.consume(subject); // Flush as many postponed results as possible. while (!outstanding.isEmpty() && postponed.containsKey(outstanding.element())) { target.consume(postponed.get(outstanding.remove())); } } else { // The invocation happened "too early"; store the subject only to pass it to the the // target when the time is right. if (postponed.containsKey(this)) { throw new IllegalStateException("Can consume only one subject"); } postponed.put(this, subject); } } } }; synchronized (outstanding) { outstanding.add(consumer); } return consumer; } }; } /** * Returns a list of consumers of size {@code n}. When all comsumers have consumed their first subject, then these * subjects are passed to the {@code target} consumer; then again when all consumers have consumed their second * subject, and so on. */ public static List> splice(final int n, final ConsumerWhichThrows, EX> target) { final List> buffers = new ArrayList>(n); List> consumers = new ArrayList>(n); for (int i = 0; i < n; i++) { consumers.add(new ConsumerWhichThrows() { final Queue buffer = new LinkedList(); { buffers.add(this.buffer); } @Override public void consume(T subject) throws EX { synchronized (buffers) { this.buffer.add(subject); // Verify that all buffers contain at least one element. for (Queue b : buffers) { if (b.isEmpty()) return; } // Move the HEAD elements of all buffers to a temporary list. List subjects = new ArrayList(n); for (Queue b : buffers) { subjects.add(b.remove()); } // Pass the list to the target consumer. target.consume(subjects); } } }); } return consumers; } /** * @return A consumer that adds the subjects it consumes to the given collection */ public static Consumer addToCollection(final Collection drain) { return new Consumer() { @Override public void consume(T subject) { drain.add(subject); } }; } /** * @return A {@link ConsumerWhichThrows} which throws each subject it consumes */ public static ConsumerWhichThrows throwsSubject() { return new ConsumerWhichThrows() { @Override public void consume(EX throwable) throws EX { throw throwable; } }; } /** * The combination of a {@link Producer} and a {@link Consumer}. * * @param The type that the {@link Produmer} produces * @param The type that the {@link Produmer} consumes */ public interface Produmer extends Producer, Consumer {} /** * The returned {@link Produmer} simply produces the last consumed subject, or {@code null} if no subject * has been consumed yet. */ public static Produmer store() { return new Produmer() { @Nullable private T store; @Override public void consume(T subject) { this.store = subject; } @Override @Nullable public T produce() { return this.store; } }; } /** * Creates and returns a {@link Consumer} which forwards the cumulated quantity to the given {@code * delegate}. * * @param initialCount Initial value for the cumulated quantity, usually {@code 0L} */ public static Consumer cumulate(final Consumer delegate, final long initialCount) { return new Consumer() { long count = initialCount; @Override public void consume(Number n) { delegate.consume((this.count += n.longValue())); } }; } /** * Creates and returns a {@link Consumer Consumer<Long>} which forwards the quantity to the given {@code * delegate}, but only if the quantity is equal to or greater than the limit, which starts with {@code * initialLimit} and increases exponentially. */ public static Consumer compressExponentially(final long initialLimit, final Consumer delegate) { return new Consumer() { long limit = initialLimit; @Override public void consume(Long n) { if (n >= this.limit) { delegate.consume(n); do { this.limit <<= 1; } while (n >= this.limit); } } }; } /** * Forwards each subject it consumes to the given delegate, but only iff the * subject is not compressable. */ public static Consumer compress(final Consumer delegate, final Predicate compressable) { return new Consumer() { @Override public void consume(T subject) { if (!compressable.evaluate(subject)) delegate.consume(subject); } }; } /** * Replaces sequences of compressable subjects with one compressed subject. *

* Leading and trailing compressables are discarded. *

*/ public static Consumer compress(final Consumer delegate, final Predicate compressable, final T compressed) { return new Consumer() { int state; @Override public void consume(T subject) { boolean isCompressable = compressable.evaluate(subject); if (isCompressable) { if (this.state == 1) this.state = 2; } else { if (this.state == 2) { delegate.consume(compressed); } delegate.consume(subject); this.state = 1; } } }; } @SuppressWarnings("unchecked") public static Consumer nop() { return (Consumer) ConsumerUtil.NOP; } private static final Consumer NOP = new Consumer() { @Override public void consume(Object subject) { ; } }; /** * Wraps the delegate such that its declared exception is caught and ignored. */ public static Consumer ignoreExceptions(final Class exceptionClass, final ConsumerWhichThrows delegate) { return new Consumer() { @Override public void consume(@Nullable T subject) { assert subject != null; try { delegate.consume(subject); } catch (RuntimeException re) { if (!exceptionClass.isAssignableFrom(re.getClass())) throw re; ; } catch (Error e) { // SUPPRESS CHECKSTYLE IllegalCatch if (!exceptionClass.isAssignableFrom(e.getClass())) throw e; ; } catch (Throwable t) { // SUPPRESS CHECKSTYLE IllegalCatch assert exceptionClass.isAssignableFrom(t.getClass()); ; } } }; } }