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}