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.BiPredicate;
026import java.util.function.Function;
027import java.util.function.Predicate;
028import java.util.function.ToIntFunction;
029
030import org.microbean.assign.AttributedType;
031import org.microbean.assign.Matcher;
032import org.microbean.assign.Selectable;
033
034import static org.microbean.bean.Beans.normalize;
035
036/**
037 * Utility methods for working with {@link Selectable}s.
038 *
039 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
040 *
041 * @see Selectable
042 *
043 * @see org.microbean.assign.Selectables
044 */
045public final class Selectables {
046
047  private Selectables() {
048    super();
049  }
050
051  /**
052   * Returns a {@link Selectable} that reduces any ambiguity in the results returned by another {@link Selectable},
053   * considering alternate status and rank.
054   *
055   * @param <C> the criteria type
056   *
057   * @param <E> the element type
058   *
059   * @param s a {@link Selectable}; must not be {@code null}
060   *
061   * @return a non-{@code null} {@link Selectable}
062   *
063   * @exception NullPointerException if {@code s} is {@code null}
064   *
065   * @see Ranked#rank()
066   *
067   * @see Ranked#alternate()
068   *
069   * @see #ambiguityReducing(Selectable, Predicate, ToIntFunction)
070   *
071   * @deprecated Please use the {@link #ambiguityReducing(Selectable, Predicate, ToIntFunction)} method instead.
072   */
073  @Deprecated(forRemoval = true, since = "0.0.19")
074  public static final <C, E extends Ranked> Selectable<C, E> ambiguityReducing(final Selectable<C, E> s) {
075    return ambiguityReducing(s, Ranked::alternate, Ranked::rank);
076  }
077
078  /**
079   * Returns a {@link Selectable} that reduces any ambiguity in the results returned by another {@link Selectable},
080   * considering alternate status and rank.
081   *
082   * @param <C> the criteria type
083   *
084   * @param <E> the element type
085   *
086   * @param s a {@link Selectable}; must not be {@code null}
087   *
088   * @param p a {@link Predicate} that tests whether an element is an <dfn>alternate</dfn>; must not be {@code null}
089   *
090   * @param ranker a {@link ToIntFunction} that returns a <dfn>rank</dfn> for an alternate; a rank of {@code 0}
091   * indicates no particular rank; must not be {@code null}
092   *
093   * @return a non-{@code null} {@link Selectable}
094   *
095   * @exception NullPointerException if any argument is {@code null}
096   */
097  public static final <C, E> Selectable<C, E> ambiguityReducing(final Selectable<C, E> s,
098                                                                final Predicate<? super E> p,
099                                                                final ToIntFunction<? super E> ranker) {
100    Objects.requireNonNull(s, "s");
101    Objects.requireNonNull(p, "p");
102    Objects.requireNonNull(ranker, "ranker");
103
104    // Relevant bits:
105    //
106    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unsatisfied_and_ambig_dependencies
107    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#dynamic_lookup (Search for "The iterator() method
108    // must")
109    return c -> {
110      final List<E> elements = s.select(c);
111      final int size = elements.size();
112      switch (size) {
113      case 0 -> List.of();
114      case 1 -> List.of(elements.get(0));
115      default -> {}
116      };
117
118      int maxRank = Integer.MIN_VALUE;
119      final List<E> reductionList = new ArrayList<>(size); // will never be larger, only smaller
120      boolean reductionListContainsOnlyRankedAlternates = false;
121
122      for (final E element : elements) {
123        if (!p.test(element)) { // TODO: eventually this method will go away
124          // The element is not an alternate. We skip it.
125          continue;
126        }
127
128        final int rank = ranker.applyAsInt(element);
129        if (rank == 0) {
130          // The element is an alternate. It has the default rank, so no explicit rank. Headed toward ambiguity. No need
131          // to look at maxRank etc.
132          if (reductionListContainsOnlyRankedAlternates) {
133            reductionListContainsOnlyRankedAlternates = false;
134          }
135          reductionList.add(element);
136          continue;
137        }
138
139        if (reductionList.isEmpty()) {
140          // The element is an alternate with an explicit rank. The reduction list is empty. The element's rank is
141          // therefore by definition the highest one encountered so far. Add the element to the reduction list.
142          assert !reductionListContainsOnlyRankedAlternates : "Unexpected reductionListContainsOnlyRankedAlternates: " + reductionListContainsOnlyRankedAlternates;
143          assert rank > maxRank : "rank <= maxRank: " + rank + " <= " + maxRank; // TODO: I think this is correct
144          maxRank = rank;
145          reductionList.add(element);
146          reductionListContainsOnlyRankedAlternates = true;
147          continue;
148        }
149
150        if (reductionListContainsOnlyRankedAlternates) {
151          // The element is an alternate. It has an explicit rank. The (non-empty) reduction list is known to contain
152          // only ranked alternates (in fact it should contain exactly one).
153          assert reductionList.size() == 1 : "Unexpected reductionList size: " + reductionList;
154          if (rank > maxRank) {
155            // The element's rank is higher than the rank of the (sole) element in the list. Record the new highest rank
156            // and replace the sole element in the list with this element.
157            maxRank = rank;
158            reductionList.set(0, element);
159          }
160          continue;
161        }
162
163        // The element is an alternate. It has an explicit rank (but this does not matter as we'll see). The list we're
164        // using to store alternates does not have a possibility of reducing to size 1, because it already contains
165        // unranked alternates, so we have to add this element to it, regardless of what its rank is. This operation
166        // will not affect the highest rank.
167        reductionList.add(element);
168      }
169
170      assert reductionListContainsOnlyRankedAlternates ? reductionList.size() == 1 : true : "Unexpected reductionList size: " + reductionList;
171      if (reductionList.isEmpty()) {
172        // No reduction at all took place. "If typesafe resolution results in an ambiguous dependency and the set of
173        // candidate beans contains no alternative, the set of resulting beans contains all candidate beans."
174        return elements;
175      } else if (reductionList.size() == 1) {
176        return List.of(reductionList.get(0));
177      }
178      return List.copyOf(reductionList);
179    };
180  }
181
182  /**
183   * {@linkplain Beans#normalize(Collection) Normalizes} the supplied {@link Collection} of {@link Bean}s and returns a
184   * {@link Selectable} for it and the supplied {@link Matcher}.
185   *
186   * <p>The {@link Selectable} does not cache its results.</p>
187   *
188   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
189   *
190   * @param m an {@link IdMatcher}; must not be {@code null}
191   *
192   * @return a non-{@code null} {@link Selectable}
193   *
194   * @exception NullPointerException if any argument is {@code null}
195   *
196   * @see org.microbean.assign.Selectables#filtering(Collection, BiPredicate)
197   *
198   * @see #ambiguityReducing(Selectable)
199   *
200   * @see org.microbean.assign.Selectables#caching(Selectable)
201   *
202   * @see Beans#normalize(Collection)
203   */
204  public static final Selectable<AttributedType, Bean<?>> typesafeReducing(final Collection<? extends Bean<?>> beans,
205                                                                           final Matcher<? super AttributedType, ? super Id> m) {
206    Objects.requireNonNull(m, "m");
207    final List<Bean<?>> normalizedBeans = normalize(beans);
208    return
209      normalizedBeans.isEmpty() ?
210      org.microbean.assign.Selectables.empty() :
211      org.microbean.assign.Selectables.filtering(normalizedBeans, (b, c) -> m.test(c, b.id()));
212  }
213
214}