Accumulators.java
package org.codefilarete.stalactite.sql.result;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.codefilarete.tool.function.Hanger.Holder;
/**
* A library of {@link Accumulator}
*
* @author Guillaume Mary
*/
public final class Accumulators {
/**
* Creates an {@link Accumulator} that adds input elements into a new {@link Collection} (created by given factory).
*
* @param <T> type of elements
* @param <C> resulting {@link Collection} type
* @param collectionFactory a {@link Supplier} returning a new {@link Collection} of appropriate type
* @return an {@link Accumulator} which collects all the input elements into a {@link Collection}, in encounter order
*/
public static <T, C extends Collection<T>> Accumulator<T, C, C> toCollection(Supplier<C> collectionFactory) {
return new CollectionSupport<>(collectionFactory);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new {@link List}.
*
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@code List}, in encounter order
* @see #toUnmodifiableList()
*/
public static <T> Accumulator<T, ? extends List<T>, List<T>> toList() {
return new CollectionSupport<>(ArrayList::new);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new unmodifiable {@link List}.
*
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@link List}, in encounter order
*/
public static <T> Accumulator<T, ? extends List<T>, List<T>> toUnmodifiableList() {
return Accumulators.<T>toList().andThen(Collections::unmodifiableList);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new {@link Set}.
*
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T> Accumulator<T, ? extends Set<T>, Set<T>> toSet() {
return new CollectionSupport<>(HashSet::new);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new unmodifiable {@link Set}.
*
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T> Accumulator<T, ? extends Set<T>, Set<T>> toUnmodifiableSet() {
return Accumulators.<T>toSet().andThen(Collections::unmodifiableSet);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new {@link Set} and keep encounter order.
* Expected use is for query which is already sorted : it avoids to lose sorting order, as a difference with {@link #toSet()}.
*
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T> Accumulator<T, Set<T>, Set<T>> toKeepingOrderSet() {
return new CollectionSupport<>(LinkedHashSet::new);
}
/**
* Creates an {@link Accumulator} that adds input elements into a new {@link NavigableSet}.
*
* @param comparator the {@link Comparator} used to compare input elements
* @param <T> type of input elements
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T> Accumulator<T, NavigableSet<T>, NavigableSet<T>> toSortedSet(Comparator<T> comparator) {
return new CollectionSupport<>(() -> new TreeSet<>(comparator));
}
/**
* Creates an {@link Accumulator} that puts input elements into a new {@link Map} as value with key given by
* {@code keyMapper}.
* If key is encountered several times, only first element is kept in final result.
*
* @param <T> type of input elements
* @param <K> type of {@link Map} key
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T, K> Accumulator<T, ?, Map<K, T>> groupingBy(Function<? super T, ? extends K> keyMapper) {
return groupingBy(keyMapper, HashMap::new, getFirst());
}
/**
* Creates an {@link Accumulator} that puts input elements into a new {@link Map} with key given by
* {@code keyMapper} and value given by {@code downstream}.
* If key is encountered several times, only first element is kept in final result.
*
* @param <T> type of input elements
* @param <K> type of {@link Map} key
* @param <V> type of {@link Map} value
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T, K, V> Accumulator<T, ?, Map<K, V>> groupingBy(
Function<? super T, ? extends K> keyMapper,
Accumulator<? super T, ?, V> downstream) {
return groupingBy(keyMapper, HashMap::new, downstream);
}
/**
* Creates an {@link Accumulator} that puts input elements into a new {@link Map} with key given by
* {@code keyMapper} and value given by {@code downstream}.
* {@link Map} instance creation is controlled by {@code mapFactory}.
*
* @param <T> type of input elements
* @param <K> type of {@link Map} key
* @param <V> type of {@link Map} value
* @param <M> type of resulting {@link Map}
* @param <S> type of {@code downStream} seed
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T, K, V, S, M extends Map<K, V>> Accumulator<T, ?, M> groupingBy(
Function<? super T, ? extends K> keyMapper,
Supplier<M> mapFactory,
Accumulator<? super T, S, V> downstream) {
// Algorithm : resulting Map is built by mapFactory and aggregation is made by downstream one, which puts object
// of wrong type in Map, which is doable thanks to Map plasticity, finally all values are replaced by downstream
// finisher with appropriate type
Supplier<S> downstreamSupplier = downstream.supplier();
BiConsumer<S, ? super T> downstreamAggregator = downstream.aggregator();
BiConsumer<Map<K, S>, T> accumulator = (m, t) -> {
K key = keyMapper.apply(t);
S container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
downstreamAggregator.accept(container, t);
};
// Downstream finisher will replace every value by its own type in resulting map
Function<S, S> downstreamFinisher = (Function<S, S>) downstream.finisher();
Function<Map<K, S>, M> finisher = resultingMap -> {
resultingMap.replaceAll((k, v) -> downstreamFinisher.apply(v));
return (M) resultingMap;
};
return new AccumulatorSupport<>((Supplier<Map<K, S>>) mapFactory, accumulator, finisher);
}
/**
* Creates an {@link Accumulator} that extracts a property from input elements and aggregates it into another {@link Accumulator}.
*
* @param <T> type of input elements
* @param <K> type of extracting property
* @param <S> type of the seed of the secondary {@link Accumulator}
* @param <R> type of the result
* @return an {@link Accumulator} which collects all the input elements into a {@link Set}, in encounter order
*/
public static <T, K, S, R> Accumulator<T, ?, R> mapping(
Function<? super T, ? extends K> mapper,
Accumulator<? super K, S, R> downstream) {
BiConsumer<S, ? super K> downstreamAccumulator = downstream.aggregator();
return new AccumulatorSupport<>(downstream.supplier(),
(r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)),
downstream.finisher());
}
/**
* Creates an {@link Accumulator} that returns first non-null input element (will return null if no non-null
* elements were found).
* <strong>It will return first element / bean / entity, not row. Meaning that whole {@link java.sql.ResultSet} will
* be consumed to build the bean.</strong>
*
* @param <T> type of input elements
* @return an {@link Accumulator} which returns first non-null input element
*/
public static <T> Accumulator<T, ?, T> getFirst() {
return new AccumulatorSupport<T, Holder<T>, T>(Holder::new, (holder, t) -> {
if (holder.get() == null) {
holder.set(t);
}
}, Holder::get);
}
/**
* Creates an {@link Accumulator} that returns first non-null input element which is expected to be the only one :
* will throw an exception if another non-null object is found
*
* @param <T> type of input elements
* @return an {@link Accumulator} which returns last input element
*/
public static <T> Accumulator<T, ?, T> getFirstUnique() {
return new AccumulatorSupport<T, Holder<T>, T>(Holder::new, (holder, t) -> {
if (holder.get() != null) {
throw new NonUniqueObjectException("Object was expected to be a lonely one but another object is present");
}
holder.set(t);
}, Holder::get);
}
static class CollectionSupport<T, C extends Collection<T>> extends AccumulatorSupport<T, C, C> {
CollectionSupport(Supplier<C> supplier) {
super(supplier, Collection::add);
}
}
/**
* Generic {@link Accumulator} implementation class.
*
* @param <T> the type of elements to be collected
* @param <S> the type of the seed that collects elements
* @param <R> the type of the result
*/
static class AccumulatorSupport<T, S, R> implements Accumulator<T, S, R> {
private final Supplier<S> supplier;
private final BiConsumer<S, T> accumulator;
private final Function<S, R> finisher;
AccumulatorSupport(Supplier<S> supplier,
BiConsumer<S, T> accumulator,
Function<S, R> finisher) {
this.supplier = supplier;
this.accumulator = accumulator;
this.finisher = finisher;
}
AccumulatorSupport(Supplier<S> supplier,
BiConsumer<S, T> accumulator) {
this(supplier, accumulator, s -> (R) s);
}
@Override
public Supplier<S> supplier() {
return this.supplier;
}
@Override
public BiConsumer<S, T> aggregator() {
return this.accumulator;
}
@Override
public Function<S, R> finisher() {
return this.finisher;
}
}
public static class NonUniqueObjectException extends RuntimeException {
public NonUniqueObjectException(String message) {
super(message);
}
}
/** Not-exposed constructor to prevent from instantiation to apply tool class design */
private Accumulators() {
// voluntarily empty
}
}