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