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}