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

com.simplaex.bedrock.Control Maven / Gradle / Ivy

package com.simplaex.bedrock;

import lombok.*;
import lombok.experimental.UtilityClass;
import lombok.experimental.Wither;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;

/**
 * Missing control structures for Java.
 */
@UtilityClass
@SuppressWarnings("WeakerAccess")
public class Control {

  @Value
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
  public static class TypeOfBranch {
    private final Class clazz;
    private final ThrowingFunction callable;
  }

  @Value
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
  public static class TypeOfVoidBranch {
    private final Class clazz;
    private final ThrowingConsumer callable;
  }

  @Nonnull
  public static  TypeOfBranch type(@Nonnull final Class clazz, @Nonnull final ThrowingFunction f) {
    return new TypeOfBranch<>(clazz, f);
  }

  @Nonnull
  public static  TypeOfVoidBranch type_(@Nonnull final Class clazz, @Nonnull final ThrowingConsumer f) {
    return new TypeOfVoidBranch<>(clazz, f);
  }

  @SuppressWarnings("unchecked")
  @SafeVarargs
  public static  T typeOf(final Object value, final TypeOfBranch... typeOfBranches) {
    for (val branch : typeOfBranches) {
      if (branch.getClazz().isAssignableFrom(value.getClass())) {
        return Try.execute(() -> (T) ((ThrowingFunction) branch.getCallable()).execute(value)).orElseThrowRuntime();
      }
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  public static void typeOf(final Object value, final TypeOfVoidBranch... branches) {
    for (val branch : branches) {
      if (branch.getClazz().isAssignableFrom(value.getClass())) {
        Try.run(() -> ((ThrowingConsumer) branch.getCallable()).accept(value));
        return;
      }
    }
  }

  @Value
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
  public static class ValueOfBranch {
    private final A value;
    private final ThrowingFunction callable;
  }

  @Value
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
  public static class ValueOfVoidBranch {
    private final A value;
    private final ThrowingConsumer callable;
  }

  @Nonnull
  public static  ValueOfBranch value(@Nonnull final A value, @Nonnull final ThrowingFunction f) {
    return new ValueOfBranch<>(value, f);
  }

  @Nonnull
  public static  ValueOfVoidBranch value_(@Nonnull final A value, @Nonnull final ThrowingConsumer f) {
    return new ValueOfVoidBranch<>(value, f);
  }


  @SuppressWarnings("unchecked")
  @SafeVarargs
  public static  T valueOf(final Object value, final ValueOfBranch... typeOfBranches) {
    for (val branch : typeOfBranches) {
      if (Objects.equals(value, branch.getValue())) {
        return Try.execute(() -> (T) ((ThrowingFunction) branch.getCallable()).execute(value)).orElseThrowRuntime();
      }
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  public static void valueOf(final Object value, final ValueOfVoidBranch... branches) {
    for (val branch : branches) {
      if (Objects.equals(value, branch.getValue())) {
        Try.run(() -> ((ThrowingConsumer) branch.getCallable()).accept(value));
        return;
      }
    }
  }

  public static Thread.UncaughtExceptionHandler uncaughtExceptionHandler() {
    return Optional
      .ofNullable(Thread.currentThread().getUncaughtExceptionHandler())
      .orElse(NoOp.uncaughtExceptionHandler());
  }

  public static void report(@Nonnull final Object err) {
    uncaughtExceptionHandler().uncaughtException(Thread.currentThread(), toThrowable(err));
  }

  public static void forever(@Nonnull final ThrowingRunnable runnable) {
    forever(uncaughtExceptionHandler(), runnable);
  }

  public static void forever(
    @Nonnull final Thread.UncaughtExceptionHandler exceptionHandler,
    @Nonnull final ThrowingRunnable runnable
  ) {
    while (!Thread.currentThread().isInterrupted()) {
      try {
        runnable.execute();
      } catch (final InterruptedException exc) {
        return;
      } catch (final Exception exc) {
        exceptionHandler.uncaughtException(Thread.currentThread(), exc);
      }
    }
  }

  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
  public static boolean wait(final Object monitor) {
    while (!Thread.currentThread().isInterrupted()) {
      try {
        synchronized (monitor) {
          monitor.wait();
        }
      } catch (final InterruptedException exc) {
        return false;
      }
    }
    return true;
  }

  public static void sleep(final Duration duration) {
    final long started = System.nanoTime();
    do {
      try {
        Thread.sleep(duration.toMillis());
        return;
      } catch (final InterruptedException ignore) {
      }
    } while ((System.nanoTime() - started) < duration.toNanos());
  }

  public static void parallel(@Nonnull final Executor executor, @Nonnull final ThrowingRunnable... runnables)
    throws ParallelExecutionException {
    Objects.requireNonNull(executor, "executor must not be null");
    Objects.requireNonNull(runnables, "nullables must not be null");
    final Semaphore semaphore = new Semaphore(0);
    final List exceptions = Collections.synchronizedList(new ArrayList());
    for (final ThrowingRunnable runnable : runnables) {
      executor.execute(() -> {
        try {
          runnable.run();
        } catch (final Exception exc) {
          exceptions.add(exc);
        } finally {
          semaphore.release(1);
        }
      });
    }
    semaphore.acquireUninterruptibly(runnables.length);
    if (!exceptions.isEmpty()) {
      throw new ParallelExecutionException(Seq.ofCollection(exceptions));
    }
  }

  @SuppressWarnings("unchecked")
  @SafeVarargs
  public static  Seq parallel(final @Nonnull Executor executor, final @Nonnull Callable... runnables)
    throws ParallelExecutionException {
    Objects.requireNonNull(executor, "executor must not be null");
    Objects.requireNonNull(runnables, "nullables must not be null");
    final Promise[] promises = new Promise[runnables.length];
    int i = 0;
    for (final Callable runnable : runnables) {
      final Promise promise = Promise.promise();
      promises[i++] = promise;
      executor.execute(() -> {
        try {
          final T result = runnable.call();
          promise.fulfill(result);
        } catch (final Exception exc) {
          promise.fail(exc);
        }
      });
    }
    final SeqBuilder results = Seq.builder();
    final SeqBuilder exceptions = Seq.builder();
    for (final Promise promise : promises) {
      promise.waitFor();
      if (promise.isSuccess()) {
        results.add((T) promise.get());
      } else {
        exceptions.add(promise.getException());
      }
    }
    if (exceptions.isEmpty()) {
      return results.build();
    }
    throw new ParallelExecutionException(exceptions.result());
  }

  @SafeVarargs
  public static  ThrowingConsumer>> parallel(@Nonnull final ThrowingConsumer>... actions) {
    return (callback) -> {
      final boolean[] hasResult = new boolean[actions.length];
      final Object[] results = new Object[actions.length];
      final Object[] errors = new Object[actions.length];
      final AtomicInteger outstanding = new AtomicInteger(actions.length);
      final Function> cb = (ix) -> (err, res) -> {
        synchronized (actions[ix]) {
          if (hasResult[ix]) {
            report(new TaskCompletedMoreThanOnceException(actions[ix], errors[ix], results[ix], err, res));
            return;
          }
          hasResult[ix] = true;
        }
        errors[ix] = err;
        results[ix] = res;
        if (outstanding.decrementAndGet() == 0) {
          final Seq errorsSeq = Seq.ofArrayZeroCopyInternal(errors);
          final Seq resultsSeq = Seq.ofArrayZeroCopyInternal(results);
          final Object error;
          if (errorsSeq.exists(Objects::nonNull)) {
            error = new ParallelExecutionException(errorsSeq.map(Control::toThrowable));
          } else {
            error = null;
          }
          callback.call(error, resultsSeq);
        }
      };
      int i = 0;
      for (final ThrowingConsumer> action : actions) {
        final int myIndex = i++;
        try {
          action.consume(cb.apply(myIndex));
        } catch (final Exception exc) {
          cb.apply(myIndex).fail(exc);
        }
      }
    };
  }

  @AllArgsConstructor(access = AccessLevel.PRIVATE)
  @Wither(AccessLevel.PRIVATE)
  public static final class Async implements ThrowingBiConsumer>, Function> {

    @Value
    @Wither
    private static class AsyncOptions {

      private final Executor executor;
      private final Consumer callbackHandler;

      private static final AsyncOptions DEFAULT_OPTIONS = new AsyncOptions(Runnable::run, Try::unfailable);

      @Nonnull
      public static AsyncOptions defaultOptions() {
        return DEFAULT_OPTIONS;
      }
    }

    @FunctionalInterface
    private interface AsyncCallback {
      void call(@Nonnull final AsyncOptions opts, final Object error, final Out result) throws Exception;
    }

    @FunctionalInterface
    private interface AsyncFunction {
      void run(@Nonnull final AsyncOptions options, final In arg, @Nonnull final AsyncCallback callback) throws Exception;
    }

    private final AsyncFunction function;
    private final AsyncOptions options;

    private Async(@Nonnull final AsyncFunction function) {
      this(function, AsyncOptions.defaultOptions());
    }

    private Async(@Nonnull final ThrowingBiConsumer> function) {
      this((opts, arg, callback) -> function.consume(arg, (error, result) -> callback.call(opts, error, result)));
    }

    @Nonnull
    @SuppressWarnings("CodeBlock2Expr")
    public final  Async then(@Nonnull final ThrowingBiConsumer> function) {
      Objects.requireNonNull(function, "'function' must not be null.");
      return new Async<>((options, argument, callback) -> {
        runWithOptions(options, argument, (opts, error, result) -> {
          if (error == null) {
            async(function).runWithOptions(opts, result, callback);
          } else {
            opts.getCallbackHandler().accept(() -> callback.call(opts, error, null));
          }
        });
      }, options);
    }

    @SuppressWarnings("CodeBlock2Expr")
    @Nonnull
    @SafeVarargs
    public final  Async> then(
      @Nonnull final ThrowingBiConsumer> function,
      @Nonnull final ThrowingBiConsumer>... functions
    ) {
      Objects.requireNonNull(function, "'function' must not be null.");
      Objects.requireNonNull(functions, "'functions' must not be null.");
      @SuppressWarnings("unchecked") final Async[] asyncs = new Async[1 + functions.length];
      asyncs[0] = async(function);
      for (int i = 0; i < functions.length; ) {
        final Async asyncFunction = async(functions[i]);
        i += 1;
        asyncs[i] = asyncFunction;
      }
      return new Async<>((options, argument, callback) -> {
        runWithOptions(options, argument, (opts, error, result) -> {
          if (error == null) {
            final Object[] results = new Object[asyncs.length];
            final Object[] errors = new Object[asyncs.length];
            final Box.IntBox numberOfReturns = Box.intBox(0);
            final Box.IntBox numberOfErrors = Box.intBox(0);
            for (int i = 0; i < asyncs.length; i += 1) {
              final int myIndex = i;
              asyncs[i].runWithOptions(opts, result, (opts2, error2, result2) -> {
                if (error2 == null) {
                  results[myIndex] = result2;
                } else {
                  errors[myIndex] = error2;
                  synchronized (numberOfErrors) {
                    numberOfErrors.update(n -> n + 1);
                  }
                }
                final boolean lastOne;
                synchronized (numberOfReturns) {
                  lastOne = numberOfReturns.update(n -> n + 1) == asyncs.length;
                }
                if (lastOne) {
                  if (numberOfErrors.getValue() == 0) {
                    opts.getCallbackHandler().accept(() -> callback.call(opts, null, new SeqSimple<>(results)));
                  } else {
                    opts.getCallbackHandler().accept(() -> callback.call(opts, new SeqSimple<>(errors), new SeqSimple<>(results)));
                  }
                }
              });
            }
          } else {
            opts.getCallbackHandler().accept(() -> callback.call(opts, error, null));
          }
        });
      }, options);
    }

    @SuppressWarnings("CodeBlock2Expr")
    private void runWithOptions(
      final @Nonnull AsyncOptions options,
      final In argument,
      final @Nonnull AsyncCallback callback
    ) {
      try {
        options.getExecutor().execute(() -> {
          try {
            function.run(options, argument, (opts, error, result) -> {
              opts.getCallbackHandler().accept(() -> callback.call(opts, error, result));
            });
          } catch (final Exception exc) {
            options.getCallbackHandler().accept(() -> callback.call(options, exc, null));
          }
        });
      } catch (final Exception exc) {
        options.getCallbackHandler().accept(() -> callback.call(options, exc, null));
      }
    }

    public void run(final In argument, @Nonnull final Callback callback) {
      runWithOptions(options, argument, (opts, error, result) -> callback.call(error, result));
    }

    public void run(final @Nonnull Executor executor, final In argument, final @Nonnull Callback callback) {
      runWithOptions(options.withExecutor(executor), argument, (opts, error, result) -> callback.call(error, result));
    }

    @Nonnull
    public Promise runPromised(final In argument) {
      return runPromised(options, argument);
    }

    @Nonnull
    public Promise runPromised(final @Nonnull Executor executor, final In argument) {
      return runPromised(options.withExecutor(executor), argument);
    }

    @Nonnull
    private Promise runPromised(final @Nonnull AsyncOptions options, final In argument) {
      final Promise promise = Promise.promise();
      runWithOptions(options, argument, (opts, error, result) -> {
        if (error == null) {
          promise.fulfill(result);
        } else {
          promise.fail(toThrowable(error));
        }
      });
      return promise;
    }

    @Override
    public void consume(final In argument, @Nonnull final Callback callback) {
      run(argument, callback);
    }

    @Nonnull
    @Override
    public Promise apply(final In in) {
      return runPromised(in);
    }
  }

  @Nonnull
  public static  Async async(@Nonnull final ThrowingBiConsumer> function) {
    Objects.requireNonNull(function, "'function' must not be null.");
    return new Async<>(function);
  }

  @SuppressWarnings("unchecked")
  public static Throwable toThrowable(final Object object) {
    if (object instanceof Throwable) {
      return (Throwable) object;
    }
    if (object instanceof String) {
      return new LightweightRuntimeException((String) object);
    }
    if (object instanceof Seq) {
      if (((Seq) object).forAll(obj -> obj instanceof Throwable)) {
        return new ParallelExecutionException((Seq) object);
      }
    }
    return new AsyncExecutionException(object);
  }

  public static  A iterate(final UnaryOperator operation, final BiPredicate exitCriterion, final A startValue) {
    A previousValue = startValue;
    A currentValue = startValue;
    do {
      previousValue = currentValue;
      currentValue = operation.apply(currentValue);
    } while (!exitCriterion.test(previousValue, currentValue));
    return currentValue;
  }

  public static  A exhaustively(final UnaryOperator operation, final A startValue) {
    return iterate(operation, Objects::equals, startValue);
  }
}