/***************************************************************************** * ------------------------------------------------------------------------- * * 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)); } } }