001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.bean;
015
016import java.util.ArrayList;
017import java.util.Collection;
018import java.util.List;
019import java.util.Map;
020import java.util.Objects;
021
022import java.util.concurrent.ConcurrentHashMap;
023
024import java.util.function.BiFunction;
025import java.util.function.Function;
026
027import org.microbean.assign.Matcher;
028
029import static org.microbean.bean.Beans.normalize;
030
031import static org.microbean.bean.Ranked.DEFAULT_RANK;
032
033/**
034 * Utility methods for working with {@link Selectable}s.
035 *
036 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
037 *
038 * @see Selectable
039 */
040public final class Selectables {
041
042  private Selectables() {
043    super();
044  }
045
046  /**
047   * Returns a {@link Selectable} that reduces any ambiguity in the results returned by another {@link Selectable},
048   * considering alternate status and rank.
049   *
050   * @param <C> the criteria type
051   *
052   * @param <E> the element type
053   *
054   * @param s a {@link Selectable}; must not be {@code null}
055   *
056   * @return a non-{@code null} {@link Selectable}
057   *
058   * @exception NullPointerException if {@code s} is {@code null}
059   *
060   * @see Ranked#rank()
061   *
062   * @see Ranked#alternate()
063   */
064  public static final <C, E extends Ranked> Selectable<C, E> ambiguityReducing(final Selectable<C, E> s) {
065    Objects.requireNonNull(s, "s");
066    // Relevant bits:
067    //
068    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unsatisfied_and_ambig_dependencies
069    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#dynamic_lookup (Search for "The iterator() method
070    // must")
071    return c -> {
072      final List<E> elements = s.select(c);
073      if (elements.isEmpty()) {
074        return List.of();
075      } else if (elements.size() == 1) {
076        return List.of(elements.get(0));
077      }
078
079      int maxRank = Integer.MIN_VALUE;
080      final List<E> reductionList = new ArrayList<>(elements.size()); // will never be larger, only smaller
081      boolean reductionListContainsOnlyRankedAlternates = false;
082
083      for (final E element : elements) {
084        if (!element.alternate()) { // TODO: eventually this method will go away
085          // The element is not an alternate. We skip it.
086          continue;
087        }
088
089        final int rank = element.rank(); // TODO: eventually this method will go away
090        if (rank == DEFAULT_RANK) {
091          // The element is an alternate. It has the default rank, so no explicit rank. Headed toward ambiguity. No need
092          // to look at maxRank etc.
093          if (reductionListContainsOnlyRankedAlternates) {
094            reductionListContainsOnlyRankedAlternates = false;
095          }
096          reductionList.add(element);
097          continue;
098        }
099
100        if (reductionList.isEmpty()) {
101          // The element is an alternate. It has an explicit rank. The reduction list is empty. The element's rank is
102          // therefore the highest one encountered so far. Add the element to the reduction list.
103          assert !reductionListContainsOnlyRankedAlternates : "Unexpected reductionListContainsOnlyRankedAlternates: " + reductionListContainsOnlyRankedAlternates;
104          if (rank > maxRank) {
105            maxRank = rank;
106          }
107          reductionList.add(element);
108          reductionListContainsOnlyRankedAlternates = true;
109          continue;
110        }
111
112        if (reductionListContainsOnlyRankedAlternates) {
113          // The element is an alternate. It has an explicit rank. The reduction list is known to contain only ranked
114          // alternates (in fact it should contain exactly one).
115          assert reductionList.size() == 1 : "Unexpected reductionList size: " + reductionList;
116          if (rank > maxRank) {
117            // The element's rank is higher than the rank of the (sole) element in the list. Record the new highest rank
118            // and replace the sole element in the list with this element.
119            maxRank = rank;
120            reductionList.set(0, element);
121          }
122          continue;
123        }
124
125        // The element is an alternate. It has an explicit rank (but this does not matter as we'll see). The list we're
126        // using to store alternates does not have a possibility of reducing to size 1, because it already contains
127        // unranked alternates, so we have to add this element to it, regardless of what its rank is. This operation
128        // will not affect the highest rank.
129        reductionList.add(element);
130      }
131
132      assert reductionListContainsOnlyRankedAlternates ? reductionList.size() == 1 : true : "Unexpected reductionList size: " + reductionList;
133      if (reductionList.isEmpty()) {
134        // No reduction at all took place. "If typesafe resolution results in an ambiguous dependency and the set of
135        // candidate beans contains no alternative, the set of resulting beans contains all candidate beans."
136        return elements;
137      } else if (reductionList.size() == 1) {
138        return List.of(reductionList.get(0));
139      }
140      return List.copyOf(reductionList);
141    };
142  }
143
144  /**
145   * Returns a {@link Selectable} that caches its results.
146   *
147   * <p>The cache is unbounded.</p>
148   *
149   * @param <C> the criteria type
150   *
151   * @param <E> the element type
152   *
153   * @param selectable a {@link Selectable}; must not be {@code null}
154   *
155   * @return a non-{@code null} {@link Selectable}
156   *
157   * @exception NullPointerException if {@code selectable} is {@code null}
158   *
159   * @see #caching(Selectable, BiFunction)
160   */
161  public static <C, E> Selectable<C, E> caching(final Selectable<C, E> selectable) {
162    final Map<C, List<E>> selectionCache = new ConcurrentHashMap<>();
163    return Selectables.<C, E>caching(selectable, selectionCache::computeIfAbsent);
164  }
165
166  /**
167   * Returns a {@link Selectable} that caches its results.
168   *
169   * @param <C> the criteria type
170   *
171   * @param <E> the element type
172   *
173   * @param selectable a {@link Selectable}; must not be {@code null}
174   *
175   * @param f a {@link BiFunction} that returns a cached result, computing it on demand via its supplied mapping {@link
176   * Function} if necessary; must not be {@code null}; normally safe for concurrent use by multiple threads; often a
177   * reference to the {@link ConcurrentHashMap#computeIfAbsent(Object, Function)} method
178   *
179   * @return a non-{@code null} {@link Selectable}
180   *
181   * @exception NullPointerException if {@code selectable} or {@code f} is {@code null}
182   *
183   * @see ConcurrentHashMap#computeIfAbsent(Object, Function)
184   */
185  public static <C, E> Selectable<C, E> caching(final Selectable<C, E> selectable,
186                                                final BiFunction<? super C, Function<? super C, ? extends List<E>>, ? extends List<E>> f) {
187    Objects.requireNonNull(selectable, "selectable");
188    return c -> f.apply(c, selectable::select);
189  }
190
191  /**
192   * Returns a {@link Selectable} whose {@link Selectable#select(Object)} method always returns an {@linkplain List#of()
193   * empty, immutable <code>List</code>}.
194   *
195   * @param <C> the criteria type
196   *
197   * @param <E> the element type
198   *
199   * @return a non-{@code null} {@link Selectable}
200   */
201  public static final <C, E> Selectable<C, E> empty() {
202    return Selectables::empty;
203  }
204
205  private static final <C, E> List<E> empty(final C ignored) {
206    return List.of();
207  }
208
209  /**
210   * Returns a {@link Selectable} using the supplied {@link Collection} as its elements, and the supplied {@link
211   * BiFunction} as its <em>selector function</em>.
212   *
213   * <p>There is no guarantee that this method will return new {@link Selectable} instances.</p>
214   *
215   * <p>The {@link Selectable} instances returned by this method may or may not cache their selections.</p>
216   *
217   * <p>The selector function must select a sublist from the supplied {@link Collection} as mediated by the supplied
218   * criteria. The selector function must additionally be idempotent and must produce a determinate value when given the
219   * same arguments.</p>
220   *
221   * <p>No validation of these semantics of the selector function is performed.</p>
222   *
223   * @param <C> the criteria type
224   *
225   * @param <E> the element type
226   *
227   * @param collection a {@link Collection} of elements from which sublists may be selected; must not be {@code null}
228   *
229   * @param f the selector function; must not be {@code null}
230   *
231   * @return a {@link Selectable}; never {@code null}
232   *
233   * @exception NullPointerException if either {@code collection} or {@code f} is {@code null}
234   */
235  @SuppressWarnings("unchecked")
236  public static <C, E> Selectable<C, E> filtering(final Collection<? extends E> collection,
237                                                  final BiFunction<? super E, ? super C, ? extends Boolean> f) {
238    Objects.requireNonNull(f, "f");
239    return collection.isEmpty() ? empty() : c -> (List<E>)collection.stream().filter(e -> f.apply(e, c)).toList();
240  }
241
242  /**
243   * {@linkplain Beans#normalize(Collection) Normalizes} the supplied {@link Collection} of {@link Bean}s and returns a
244   * {@link Selectable} for it and the supplied {@link Matcher}.
245   *
246   * <p>The {@link Selectable} does not cache its results.</p>
247   *
248   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
249   *
250   * @param m an {@link IdMatcher}; must not be {@code null}
251   *
252   * @return a non-{@code null} {@link Selectable}
253   *
254   * @exception NullPointerException if any argument is {@code null}
255   *
256   * @see #filtering(Collection, BiFunction)
257   *
258   * @see #ambiguityReducing(Selectable)
259   *
260   * @see #caching(Selectable)
261   *
262   * @see Beans#normalize(Collection)
263   */
264  public static final Selectable<AttributedType, Bean<?>> typesafeReducing(final Collection<? extends Bean<?>> beans,
265                                                                           final Matcher<? super AttributedType, ? super Id> m) {
266    Objects.requireNonNull(m, "m");
267    final List<Bean<?>> normalizedBeans = normalize(beans);
268    return normalizedBeans.isEmpty() ? empty() : filtering(normalizedBeans, (b, c) -> m.test(c, b.id()));
269  }
270
271}