/*****************************************************************************
* ------------------------------------------------------------------------- *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
*****************************************************************************/
package com.google.mu.util;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A funnel that dispatches a sequence of inputs through arbitrary batch conversions while
* maintaining first-in-first-out order. For example, the following code can either batch load users
* from a user store, or batch load from third party user store, or else create a dummy user
* immediately without conversion:
*
* <pre>{@code
* Funnel<User> funnel = new Funnel<>();
* Funnel.Batch<Long, User> userStoreBatch = funnel.through(userStore::loadUsers);
* Funnel.Batch<ThirdPartyUser, User> thirdPartyBatch = funnel.through(thirdPartyClient::loadUsers);
* for (UserDto dto : users) {
* if (dto.hasUserId()) {
* userStoreBatch.accept(dto.getUserId());
* } else if (dto.hasThirdParty()) {
* thirdPartyBatch.accept(dto.getThirdParty());
* } else {
* funnel.add(createDummyUser(dto));
* }
* }
* List<User> users = funnel.run();
* }</pre>
*
* <p>Elements flow out of the funnel in the same order as they enter, regardless of which
* {@link Batch} converted them, or if they were directly {@link #add added} into the funnel without
* conversion.
*
* @param <T> the output type
*/
public final class Funnel<T> {
private int size = 0;
private final List<Batch<?, T>> batches = new ArrayList<>();
private final Batch<T, T> passthrough = through(Function.identity());
/**
* Holds the elements to be converted through a single batch conversion.
*
* @param <F> batch input element type
* @param <T> batch output element type
*/
public static final class Batch<F, T> implements Consumer<F> {
private final Funnel<T> funnel;
private final Function<? super List<F>, ? extends Collection<? extends T>> converter;
private final List<Indexed<F, T>> indexedSources = new ArrayList<>();
Batch(Funnel<T> funnel, Function<? super List<F>, ? extends Collection<? extends T>> converter) {
this.funnel = funnel;
this.converter = requireNonNull(converter);
}
/** Adds {@code source} to be converted. */
@Override public void accept(F source) {
accept(source, Function.identity());
}
/**
* Adds {@code source} to be converted.
* {@code postConversion} will be applied after the batch conversion completes,
* to compute the final result for this input.
*/
public void accept(F source, Function<? super T, ? extends T> postConversion) {
indexedSources.add(new Indexed<>(funnel.size++, source, postConversion));
}
/**
* Adds {@code source} to be converted.
* {@code aftereffect} will be applied against the conversion result after the batch completes.
*/
public void accept(F source, Consumer<? super T> aftereffect) {
requireNonNull(aftereffect);
accept(source, v -> {
aftereffect.accept(v);
return v;
});
}
void convertInto(ArrayList<T> output) {
if (indexedSources.isEmpty()) {
return;
}
List<F> params = indexedSources.stream().map(i -> i.value).collect(toList());
List<T> results = new ArrayList<>(converter.apply(params));
if (params.size() != results.size()) {
throw new IllegalStateException(
converter + " expected to return " + params.size() + " elements for input "
+ params + ", but got " + results + " of size " + results.size() + ".");
}
for (int i = 0; i < indexedSources.size(); i++) {
indexedSources.get(i).setAtIndex(results.get(i), output);
}
}
}
/**
* Returns a {@link Batch} accepting elements that, when {@link #run} is called,
* will be converted together in a batch through {@code converter}.
*/
public <F> Batch<F, T> through(
Function<? super List<F>, ? extends Collection<? extends T>> converter) {
Batch<F, T> batch = new Batch<>(this, converter);
batches.add(batch);
return batch;
}
/** Adds {@code element} to the funnel. */
public void add(T element) {
passthrough.accept(element);
}
/**
* Runs all batch conversions and returns conversion results together with elements {@link #add
* added} as is, in encounter order.
*/
public List<T> run() {
ArrayList<T> output = new ArrayList<>(Collections.nCopies(size, null));
for (Batch<?, T> batch : batches) {
batch.convertInto(output);
}
return output;
}
private static final class Indexed<F, T> {
private final int index;
final F value;
private final Function<? super T, ? extends T> converter;
Indexed(int index, F value, Function<? super T, ? extends T> converter) {
this.index = index;
this.value = requireNonNull(value);
this.converter = requireNonNull(converter);
}
void setAtIndex(T from, List<? super T> to) {
to.set(index, converter.apply(from));
}
}
}