001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2024–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.lang.System.Logger;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Objects;
026import java.util.Set;
027
028import java.util.concurrent.ConcurrentHashMap;
029
030import org.microbean.assign.Matcher;
031
032import org.microbean.construct.Domain;
033
034import static java.util.HashSet.newHashSet;
035
036/**
037 * A {@link Selectable} and {@link Reducible} implementation that works with {@link Bean} and {@link
038 * AttributedType} instances.
039 *
040 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
041 *
042 * @see #Beans(Selectable, Reducible)
043 *
044 * @see #cachingSelectableOf(Collection, Matcher, Map)
045 *
046 * @see Selectable
047 *
048 * @see Reducible
049 *
050 * @see Reducer
051 *
052 * @see RankedReducer
053 *
054 * @see Bean
055 *
056 * @see AttributedType
057 */
058public final class Beans implements Selectable<AttributedType, Bean<?>>, Reducible<AttributedType, Bean<?>> {
059
060
061  /*
062   * Static fields.
063   */
064
065
066  private static final Logger LOGGER = System.getLogger(Beans.class.getName());
067
068  private static final Comparator<Bean<?>> byRankComparator = Comparator
069    .<Bean<?>>comparingInt(Ranked::rank)
070    .reversed();
071
072  private static final Comparator<Bean<?>> byAlternateThenByRankComparator = Comparator
073    .<Bean<?>, Boolean>comparing(Ranked::alternate)
074    .reversed()
075    .thenComparing(byRankComparator);
076
077
078  /*
079   * Instance fields.
080   */
081
082
083  private final Selectable<AttributedType, Bean<?>> s;
084
085  private final Reducible<AttributedType, Bean<?>> r;
086
087
088  /*
089   * Constructors.
090   */
091
092
093  /**
094   * Creates a new {@link Beans}.
095   *
096   * <p>This constructor is best suited for testing.</p>
097   *
098   * @param domain a {@link Domain}; must not be {@code null}
099   *
100   * @param beans an array of zero or more {@link Bean}s; may be {@code null}
101   *
102   * @exception NullPointerException if {@code domain} is {@code null}
103   *
104   * @see #Beans(Domain, Collection)
105   */
106  public Beans(final Domain domain, final Bean<?>... beans) {
107    this(domain, beans == null || beans.length == 0 ? List.of() : List.of(beans));
108  }
109
110  /**
111   * Creates a new {@link Beans}.
112   *
113   * @param domain a {@link Domain}; must not be {@code null}
114   *
115   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
116   *
117   * @exception NullPointerException if any argument is {@code null}
118   *
119   * @see #cachingSelectableOf(Collection, Matcher, Map)
120   *
121   * @see #Beans(Selectable)
122   */
123  public Beans(final Domain domain, final Collection<? extends Bean<?>> beans) {
124    this(cachingSelectableOf(beans,
125                             new IdMatcher(new BeanQualifiersMatcher(),
126                                           new InterceptorBindingsMatcher(),
127                                           new BeanTypeMatcher(domain)),
128                             Map.of()));
129  }
130
131  /**
132   * Creates a new {@link Beans}.
133   *
134   * @param s a {@link Selectable}; must not be {@code null}
135   *
136   * @exception NullPointerException if {@code s} is {@code null}
137   *
138   * @see #Beans(Selectable, Reducer)
139   *
140   * @see RankedReducer#of()
141   */
142  public Beans(final Selectable<AttributedType, Bean<?>> s) {
143    this(s, (Reducer<AttributedType, Bean<?>>)null);
144  }
145
146  /**
147   * Creates a new {@link Beans}.
148   *
149   * @param s a {@link Selectable}; must not be {@code null}
150   *
151   * @param r a {@link Reducer}; may be {@code null} in which case a {@link RankedReducer#of() RankedReducer} will be
152   * used instead
153   *
154   * @exception NullPointerException if {@code s} is {@code null}
155   *
156   * @see #Beans(Selectable, Reducible)
157   *
158   * @see Reducible#of(Selectable, Reducer)
159   */
160  public Beans(final Selectable<AttributedType, Bean<?>> s, final Reducer<AttributedType, Bean<?>> r) {
161    this(s, Reducible.of(s, r == null ? RankedReducer.of() : r));
162  }
163
164  /**
165   * Creates a new {@link Beans}.
166   *
167   * @param s a {@link Selectable}; must not be {@code null}
168   *
169   * @param r a {@link Reducible} to apply to elements selected by the supplied {@link Selectable}; must not be {@code
170   * null}
171   *
172   * @exception NullPointerException if any argument is {@code null}
173   */
174  public Beans(final Selectable<AttributedType, Bean<?>> s, final Reducible<AttributedType, Bean<?>> r) {
175    this.s = Objects.requireNonNull(s, "s");
176    this.r = Objects.requireNonNull(r, "r");
177  }
178
179
180  /*
181   * Instance methods.
182   */
183
184
185  @Override // Selectable<AttributedType, Bean<?>>
186  public final List<Bean<?>> select(final AttributedType c) {
187    return this.s.select(c);
188  }
189
190  @Override // Reducible<AttributedType, Bean<?>>
191  public final Bean<?> reduce(final AttributedType c) {
192    return this.r.reduce(c);
193  }
194
195
196  /*
197   * Static methods.
198   */
199
200
201  /**
202   * Returns a new {@link Selectable} that caches its results.
203   *
204   * <p>The cache is unbounded.</p>
205   *
206   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
207   *
208   * @param idMatcher an {@link IdMatcher}; must not be {@code null}
209   *
210   * @param selections a (normally empty) {@link Map} of precomputed selections; must not be {@code null}
211   *
212   * @return a new {@link Selectable}; never {@code null}
213   *
214   * @exception NullPointerException if any argument is {@code null}
215   */
216  public static final Selectable<AttributedType, Bean<?>> cachingSelectableOf(final Collection<? extends Bean<?>> beans,
217                                                                              final Matcher<? super AttributedType, ? super Id> idMatcher,
218                                                                              final Map<? extends AttributedType, ? extends List<Bean<?>>> selections) {
219
220    Objects.requireNonNull(idMatcher, "idMatcher");
221    final Map<AttributedType, List<Bean<?>>> selectionCache = new ConcurrentHashMap<>();
222    final ArrayList<Bean<?>> newBeans = new ArrayList<>(31); // 31 == arbitrary
223    final Set<Bean<?>> newBeansSet = newHashSet(31); // 31 == arbitrary
224    for (final Entry<? extends AttributedType, ? extends List<Bean<?>>> e : selections.entrySet()) {
225      final List<Bean<?>> selection = e.getValue();
226      if (!selection.isEmpty()) {
227        final Set<Bean<?>> newSelectionSet = newHashSet(7); // 7 == arbitrary
228        final ArrayList<Bean<?>> newSelection = new ArrayList<>(selection.size());
229        for (final Bean<?> b : selection) {
230          if (newSelectionSet.add(b)) {
231            newSelection.add(b);
232          }
233          if (newBeansSet.add(b)) {
234            newBeans.add(b);
235          }
236        }
237        newSelectionSet.clear();
238        newSelection.trimToSize();
239        Collections.sort(newSelection, byAlternateThenByRankComparator);
240        selectionCache.put(e.getKey(), Collections.unmodifiableList(newSelection));
241      }
242    }
243    for (final Bean<?> bean : beans) {
244      if (newBeansSet.add(bean)) {
245        newBeans.add(bean);
246      }
247    }
248    newBeansSet.clear();
249    if (newBeans.isEmpty()) {
250      return Beans::empty;
251    }
252    Collections.sort(newBeans, byAlternateThenByRankComparator);
253    newBeans.trimToSize();
254    return attributedType -> selectionCache.computeIfAbsent(attributedType, at -> newBeans.stream().filter(b -> idMatcher.test(at, b.id())).toList());
255  }
256
257  private static final <C, T> List<T> empty(final C ignored) {
258    return List.of();
259  }
260
261}