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}