![JAR search and dependency download from the Maven repository](/logo.png)
net.pincette.rs.Util Maven / Gradle / Ivy
package net.pincette.rs;
import static java.lang.Math.min;
import static java.time.Duration.between;
import static java.time.Instant.now;
import static java.time.temporal.ChronoUnit.SECONDS;
import static java.util.Collections.emptyList;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.locks.LockSupport.park;
import static java.util.concurrent.locks.LockSupport.parkNanos;
import static java.util.logging.Logger.getLogger;
import static net.pincette.rs.Async.mapAsyncSequential;
import static net.pincette.rs.Box.box;
import static net.pincette.rs.Buffer.buffer;
import static net.pincette.rs.Chain.with;
import static net.pincette.rs.Combine.combine;
import static net.pincette.rs.Encode.encode;
import static net.pincette.rs.LambdaSubscriber.lambdaSubscriber;
import static net.pincette.rs.Mapper.map;
import static net.pincette.rs.NotFilter.notFilter;
import static net.pincette.rs.PassThrough.passThrough;
import static net.pincette.rs.Pipe.pipe;
import static net.pincette.rs.Probe.probe;
import static net.pincette.rs.Reducer.reduce;
import static net.pincette.rs.Source.of;
import static net.pincette.util.Collections.list;
import static net.pincette.util.Pair.pair;
import static net.pincette.util.ScheduledCompletionStage.composeAsyncAfter;
import static net.pincette.util.ScheduledCompletionStage.supplyAsyncAfter;
import static net.pincette.util.StreamUtil.rangeExclusive;
import static net.pincette.util.Util.tryToDoRethrow;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.logging.Logger;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import net.pincette.function.ConsumerWithException;
import net.pincette.function.RunnableWithException;
import net.pincette.function.SideEffect;
import net.pincette.rs.encoders.Deflate;
import net.pincette.rs.encoders.DivisibleBy;
import net.pincette.rs.encoders.Gunzip;
import net.pincette.rs.encoders.Gzip;
import net.pincette.rs.encoders.Inflate;
import net.pincette.rs.encoders.Lines;
import net.pincette.util.State;
import net.pincette.util.TimedCache;
import net.pincette.util.Util.GeneralException;
/**
* Some utilities.
*
* @author Werner Donné
* @since 1.0
*/
public class Util {
static final Logger LOGGER = getLogger("net.pincette.rs.base");
private Util() {}
/**
* Returns the published values as a list. This function will block when needed.
*
* @param publisher the publisher.
* @param the value type.
* @return The generated list.
* @since 1.6
*/
public static List asList(final Publisher publisher) {
return asListAsync(publisher).toCompletableFuture().join();
}
public static List asList(final Publisher publisher, final int initialCapacity) {
return asListAsync(publisher, initialCapacity).toCompletableFuture().join();
}
/**
* Returns the published values as a list.
*
* @param publisher the publisher.
* @param the value type.
* @return The generated list.
* @since 1.6
*/
public static CompletionStage> asListAsync(final Publisher publisher) {
return asListAsync(publisher, 1000);
}
public static CompletionStage> asListAsync(
final Publisher publisher, final int initialCapacity) {
final CompletableFuture> future = new CompletableFuture<>();
publisher.subscribe(completerList(future, initialCapacity));
return future;
}
/**
* Returns the first value. This function will block when needed.
*
* @param publisher the publisher.
* @param the value type.
* @return The value.
* @since 1.7.1
*/
public static T asValue(final Publisher publisher) {
return asValueAsync(publisher).toCompletableFuture().join();
}
/**
* Completes with the first emitted value or null
if there was no such value.
*
* @param publisher the publisher.
* @param the value type.
* @return The value.
* @since 1.7.1
*/
public static CompletionStage asValueAsync(final Publisher publisher) {
final CompletableFuture future = new CompletableFuture<>();
with(publisher).first().get().subscribe(completerFirst(future));
return future;
}
/**
* This is a debugging aid that throws an exception when more messages are sent than requested.
*
* @return The pass through processor.
* @param the value type.
* @since 3.0.2
*/
public static Processor backpressureCheck() {
final State requested = new State<>(0L);
final State sent = new State<>(0L);
return probe(
n -> requested.set(requested.get() + n),
v -> {
sent.set(sent.get() + 1);
if (sent.get() > requested.get()) {
throw new GeneralException(
"Sending "
+ sent.get()
+ " values, while only "
+ requested.get()
+ " have been requested.");
}
});
}
/**
* Subscribes to the publisher and cancels it immediately.
*
* @param publisher the given publisher.
* @param the value type.
* @since 3.4
*/
public static void cancel(final Publisher publisher) {
publisher.subscribe(lambdaSubscriber(v -> {}, () -> {}, t -> {}, Subscription::cancel));
}
public static Publisher completablePublisher(
final Supplier>> publisher) {
final Processor passThrough = passThrough();
publisher.get().thenAccept(p -> p.subscribe(passThrough));
return passThrough;
}
private static Subscriber completerEmpty(final CompletableFuture future) {
return lambdaSubscriber(v -> {}, () -> future.complete(null), future::completeExceptionally);
}
private static Subscriber completerFirst(final CompletableFuture future) {
return lambdaSubscriber(
future::complete, () -> future.complete(null), future::completeExceptionally);
}
private static Subscriber completerList(
final CompletableFuture> future, final int initialCapacity) {
final List result = new ArrayList<>(initialCapacity);
return lambdaSubscriber(
result::add, () -> future.complete(result), future::completeExceptionally);
}
static List consume(final Deque deque, final long requested) {
return rangeExclusive(0, min(deque.size(), requested)).map(i -> deque.pollLast()).toList();
}
/**
* Creates a processor that deflates a ByteBuffer
stream.
*
* @return The processor.
* @since 3.0
*/
public static Processor deflate() {
return encode(Deflate.deflate());
}
/**
* Creates a processor that deflates a ByteBuffer
stream.
*
* @return The processor.
* @since 3.0
*/
public static Processor deflate(final Deflater deflater) {
return encode(Deflate.deflate(deflater));
}
public static Subscriber devNull() {
return lambdaSubscriber(v -> {});
}
/**
* Consumes the publisher without doing anything.
*
* @param publisher the given publisher.
* @param the value type.
* @since 3.0
*/
public static void discard(final Publisher publisher) {
publisher.subscribe(devNull());
}
/**
* Returns a processor that produces byte buffers the size of which is divisible by a certain
* number, where the last one may be smaller than that number.
*
* @author Werner Donn\u00e9
* @since 3.0
*/
public static Processor divisibleBy(final int n) {
return encode(DivisibleBy.divisibleBy(n));
}
/**
* Filters the elements leaving out duplicates according to the criterion
over the
* time window
.
*
* @param criterion the given criterion.
* @param window the time window to look at.
* @param the value type.
* @param the criterion type.
* @return The filtered stream.
* @since 3.0
*/
public static Processor duplicateFilter(
final Function criterion, final Duration window) {
final TimedCache cache = new TimedCache<>(window);
return pipe(map((T v) -> pair(v, criterion.apply(v))))
.then(notFilter(pair -> cache.get(pair.second).isPresent()))
.then(
map(
pair ->
Optional.of(pair.second)
.map(
c ->
SideEffect.run(() -> cache.put(c, c))
.andThenGet(() -> pair.first))
.orElse(null)));
}
@SuppressWarnings("java:S1905") // For type inference.
private static CompletionStage> element(
final Publisher publisher, final Processor terminator) {
return reduce(subscribe(publisher, terminator), () -> (T) null, (result, value) -> value)
.thenApply(Optional::ofNullable);
}
/**
* Returns a publisher that emits no values.
*
* @param the value type.
* @return The empty publisher.
* @since 1.0
*/
public static Publisher empty() {
return of(emptyList());
}
/**
* Waits until the empty publisher completes.
*
* @param publisher the publisher.
* @since 1.7.1
*/
public static void empty(final Publisher publisher) {
emptyAsync(publisher).toCompletableFuture().join();
}
/**
* Completes when the empty publisher completes.
*
* @param publisher the publisher.
* @return The empty completion stage.
* @since 1.7.1
*/
public static CompletionStage emptyAsync(final Publisher publisher) {
final CompletableFuture future = new CompletableFuture<>();
publisher.subscribe(completerEmpty(future));
return future;
}
/**
* Returns the first element publisher
emits if there is one.
*
* @param publisher the given publisher.
* @param the value type.
* @return The optional first element.
* @since 1.4
*/
public static CompletionStage> first(final Publisher publisher) {
return element(publisher, new First<>());
}
/**
* Returns a publisher that always emits values from supplier
when asked for
* messages. When the supplier returns null
the publisher completes.
*
* @param supplier the generator function.
* @param the value type.
* @return The publisher.
* @since 1.6
*/
public static Publisher generate(final Supplier supplier) {
return subscriber ->
subscriber.onSubscribe(
new Subscription() {
public void cancel() {
subscriber.onComplete();
}
public void request(final long n) {
boolean completed = false;
for (long i = 0; !completed && i < n; ++i) {
final T value = supplier.get();
if (value != null) {
subscriber.onNext(value);
} else {
completed = true;
subscriber.onComplete();
}
}
}
});
}
/**
* Returns a publisher that always emits values from initial
and next
* when asked for messages. When the supplier returns null
the publisher completes.
*
* @param initial the generator function for the first value.
* @param next the generator function for all the subsequent values. The function receives the
* last value that was emitted.
* @param the value type.
* @return The publisher.
* @since 1.6
*/
public static Publisher generate(final Supplier initial, final UnaryOperator next) {
final State state = new State<>();
return generate(() -> state.set(state.get() == null ? initial.get() : next.apply(state.get())));
}
/**
* Creates a processor that compresses a ByteBuffer
stream in GZIP format.
*
* @return The processor.
* @since 3.0
*/
public static Processor gzip() {
return encode(Gzip.gzip());
}
/**
* Creates a processor that uncompresses a ByteBuffer
stream in GZIP format.
*
* @return The processor.
* @since 3.0
*/
public static Processor gunzip() {
return encode(Gunzip.gunzip());
}
/**
* Creates a processor that inflates a ByteBuffer
stream.
*
* @return The processor.
* @since 3.0
*/
public static Processor inflate() {
return encode(Inflate.inflate());
}
/**
* Creates a processor that inflates a ByteBuffer
stream.
*
* @return The processor.
* @since 3.0
*/
public static Processor inflate(final Inflater inflater) {
return encode(Inflate.inflate(inflater));
}
static Deque> initialStageDeque() {
final ArrayDeque> deque = new ArrayDeque<>(1000);
deque.addFirst(completedFuture(null));
return deque;
}
/**
* Returns a blocking Iterable
with a request size of 100.
*
* @param publisher the publisher from which the elements are buffered.
* @param the element type.
* @return The iterable.
* @since 1.2
*/
public static Iterable iterate(final Publisher publisher) {
return iterate(publisher, 100);
}
/**
* Returns a blocking Iterable
.
*
* @param publisher the publisher from which the elements are buffered.
* @param requestSize the size of the requests the subscriber will issue to the publisher.
* @param the element type.
* @return The iterable.
* @since 1.2
*/
public static Iterable iterate(final Publisher publisher, final long requestSize) {
final BlockingSubscriber subscriber = new BlockingSubscriber<>(requestSize);
publisher.subscribe(subscriber);
return subscriber;
}
/**
* Blocks until the publisher is done.
*
* @param publisher the given publisher.
* @param the value type.
* @since 1.5
*/
public static void join(final Publisher publisher) {
reduce(publisher, (v1, v2) -> v1).toCompletableFuture().join();
}
/**
* Returns the last element publisher
emits if there is one.
*
* @param publisher the given publisher.
* @param the value type.
* @return The optional first element.
* @since 1.4
*/
public static CompletionStage> last(final Publisher publisher) {
return element(publisher, new Last<>());
}
/**
* Returns a processor that receives buffers and interprets the contents as a UTF-8 encoded
* string. It emits the individual lines in the string without the line separators.
*
* @author Werner Donn\u00e9
* @since 3.0
*/
public static Processor lines() {
return encode(Lines.lines());
}
static Optional> nextValues(final Deque deque, final long amount) {
return Optional.of(deque).filter(b -> !b.isEmpty() && amount > 0).map(b -> consume(b, amount));
}
/**
* Returns a subscriber that reacts to the onComplete
event.
*
* @param runnable the given function.
* @param the value type.
* @return The subscriber.
* @since 3.0
*/
public static Subscriber onComplete(final RunnableWithException runnable) {
return lambdaSubscriber(v -> {}, runnable);
}
/**
* Returns a processor that reacts to the onComplete
event. It is transparent to all
* events.
*
* @param runnable the given function.
* @param the value type.
* @return The processor.
* @since 3.2.2
*/
public static Processor onCompleteProcessor(final RunnableWithException runnable) {
return probe(n -> {}, v -> {}, () -> tryToDoRethrow(runnable));
}
/**
* Returns a subscriber that reacts to the onError
event.
*
* @param consumer the given function.
* @param the value type.
* @return The subscriber.
* @since 3.0
*/
public static Subscriber onError(final ConsumerWithException consumer) {
return lambdaSubscriber(v -> {}, () -> {}, consumer);
}
/**
* Returns a processor that reacts to the onError
event. It is transparent to all
* events.
*
* @param consumer the given function.
* @param the value type.
* @return The processor.
* @since 3.2.2
*/
public static Processor onErrorProcessor(
final ConsumerWithException consumer) {
return probe(n -> {}, v -> {}, () -> {}, t -> tryToDoRethrow(() -> consumer.accept(t)));
}
/**
* Returns a subscriber that reacts to the onNext
event.
*
* @param consumer the given function.
* @param the value type.
* @return The subscriber.
* @since 3.0
*/
public static Subscriber onNext(final ConsumerWithException consumer) {
return lambdaSubscriber(consumer);
}
/**
* Returns a processor that reacts to the onNext
event. It is transparent to all
* events.
*
* @param consumer the given function.
* @param the value type.
* @return The processor.
* @since 3.2.2
*/
public static Processor onNextProcessor(final ConsumerWithException consumer) {
return probe(n -> {}, v -> tryToDoRethrow(() -> consumer.accept(v)));
}
static void parking(final Object blocker, final long timeout) {
if (timeout != -1) {
parkNanos(blocker, timeout * 1000);
} else {
park(blocker);
}
}
/**
* Pulls data from a processor without doing anything with it. You can use this to get a pipeline
* started that ends with some side effect.
*
* @param processor the given processor.
* @param the incoming value type.
* @param theo outgoing value type.
* @return The given processor as a subscriber.
* @since 3.0
*/
public static Subscriber pull(final Processor processor) {
processor.subscribe(devNull());
return processor;
}
/**
* Returns a publisher that will recreate the original publisher when an exception occurs.
*
* @param publisher the function to create the original publisher.
* @param retryInterval the time before a new attempt is made.
* @param the value type.
* @return The retrying publisher.
* @since 1.6
*/
public static Publisher retryPublisher(
final Supplier> publisher, final Duration retryInterval) {
return retryPublisher(publisher, retryInterval, null);
}
/**
* Returns a publisher that will recreate the original publisher when an exception occurs.
*
* @param publisher the function to create the original publisher.
* @param retryInterval the time before a new attempt is made.
* @param onException the handler of the exception. It may be null
.
* @param the value type.
* @return The retrying publisher.
* @since 1.6
*/
public static Publisher retryPublisher(
final Supplier> publisher,
final Duration retryInterval,
final Consumer onException) {
final Retry result = new Retry<>(publisher, retryInterval, onException);
publisher.get().subscribe(result);
return box(result, buffer(1));
}
/**
* Subscribes processor
to publisher
and returns the processor as a
* publisher.
*
* @param publisher the given publisher.
* @param processor the given processor.
* @param the type of the publisher.
* @param the type of the returned publisher.
* @return The processor as a publisher.
* @since 1.0
*/
public static Publisher subscribe(
final Publisher publisher, final Processor processor) {
publisher.subscribe(processor);
return processor;
}
/**
* Subscribes subscriber
to processor
and returns processor
* as a subscriber.
*
* @param processor the given processor.
* @param subscriber the given subscriber.
* @param the input type of the processor.
* @param the output type of the processor.
* @return The processor as a subscriber.
* @since 1.6
*/
public static Subscriber subscribe(
final Processor processor, final Subscriber subscriber) {
processor.subscribe(subscriber);
return processor;
}
/**
* Creates a processor where a subscriber can tap the data that goes through it.
*
* @param subscriber the given subscriber.
* @param the value type.
* @return The processor.
* @since 3.0
*/
public static Processor tap(final Subscriber subscriber) {
final Processor pass1 = passThrough();
final Processor pass2 = passThrough();
pass1.subscribe(Fanout.of(list(pass2, subscriber), list(true, false)));
return combine(pass1, pass2);
}
static void trace(final Logger logger, final Supplier message) {
logger.finest(() -> logger.getName() + ": " + message.get());
}
public static R transform(final Processor processor, final T value) {
return transformAsync(processor, value).toCompletableFuture().join();
}
public static CompletionStage transformAsync(
final Processor processor, final T value) {
return asValueAsync(with(Source.of(value)).map(processor).get());
}
public static Processor throttle(final int maxPerSecond) {
final Running running = new Running(maxPerSecond);
return mapAsyncSequential(m -> running.update().thenApply(r -> m));
}
static void throwBackpressureViolation(
final Object victim, final Subscription subscription, final long requested) {
throw new GeneralException(
"Backpressure violation in "
+ subscription.getClass().getName()
+ ". Requested "
+ requested
+ " elements in "
+ victim.getClass().getName()
+ ", which have already been received.");
}
private static class Retry extends PassThrough {
private final Consumer onException;
private final Supplier> publisher;
private final Duration retryInterval;
private long requested;
private Retry(
final Supplier> publisher,
final Duration retryInterval,
final Consumer onException) {
this.publisher = publisher;
this.retryInterval = retryInterval;
this.onException = onException;
}
@Override
protected void more(final long number) {
dispatch(
() -> {
requested += number;
super.more(number);
});
}
@Override
public void onComplete() {
dispatch(super::onComplete);
}
@Override
public void onError(Throwable t) {
dispatch(
() -> {
setError(true);
if (onException != null) {
onException.accept(t);
}
composeAsyncAfter(() -> completedFuture(publisher.get()), retryInterval)
.thenAccept(
p ->
dispatch(
() -> {
setError(false);
p.subscribe(this);
}));
});
}
@Override
public void onNext(final T value) {
dispatch(
() -> {
--requested;
super.onNext(value);
});
}
@Override
public void onSubscribe(final Subscription subscription) {
if (this.subscription == null) {
super.onSubscribe(subscription);
} else {
this.subscription = subscription; // Ignore rule 2.5 because that is the purpose here.
if (requested > 0) {
subscription.request(requested);
}
}
}
}
private static class Running {
private final int maxPerSecond;
private long count = 0;
private Instant second = now().truncatedTo(SECONDS);
private Running(final int maxPerSecond) {
this.maxPerSecond = maxPerSecond;
}
private CompletionStage update() {
final Instant now = now().truncatedTo(SECONDS);
if (between(second, now).toMillis() > 999) {
count = 0;
second = now;
}
return ++count >= maxPerSecond
? supplyAsyncAfter(() -> true, between(now(), now.plusSeconds(1)))
: completedFuture(true);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy