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 static java.util.HashSet.newHashSet;
033
034/**
035 * A {@link Selectable} and {@link Reducible} implementation that works with {@link Bean} and {@link
036 * AttributedType} instances.
037 *
038 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
039 *
040 * @see #Beans(Selectable, Reducible)
041 *
042 * @see Selectable
043 *
044 * @see Reducible
045 *
046 * @see Reducer
047 *
048 * @see RankedReducer
049 *
050 * @see Bean
051 *
052 * @see AttributedType
053 */
054public final class Beans implements Selectable<AttributedType, Bean<?>>, Reducible<AttributedType, Bean<?>> {
055
056
057  /*
058   * Static fields.
059   */
060
061
062  private static final Logger LOGGER = System.getLogger(Beans.class.getName());
063
064  private static final Comparator<Bean<?>> byRankComparator = Comparator
065    .<Bean<?>>comparingInt(Ranked::rank)
066    .reversed();
067
068  private static final Comparator<Bean<?>> byAlternateThenByRankComparator = Comparator
069    .<Bean<?>, Boolean>comparing(Ranked::alternate)
070    .reversed()
071    .thenComparing(byRankComparator);
072
073
074  /*
075   * Instance fields.
076   */
077
078
079  private final Selectable<AttributedType, Bean<?>> s;
080
081  private final Reducible<AttributedType, Bean<?>> r;
082
083
084  /*
085   * Constructors.
086   */
087
088  /**
089   * Creates a new {@link Beans}.
090   *
091   * <p>{@link Bean} instances selected by the supplied {@link Selectable} will be reduced using a {@link
092   * RankedReducer}.</p>
093   *
094   * @param s a {@link Selectable}; must not be {@code null}
095   *
096   * @see RankedReducer
097   */
098  public Beans(final Selectable<AttributedType, Bean<?>> s) {
099    this(s, Reducible.of(s, RankedReducer.of()));
100  }
101
102  /**
103   * Creates a new {@link Beans}.
104   *
105   * @param s a {@link Selectable}; must not be {@code null}
106   *
107   * @param r a {@link Reducible} to apply to elements selected by the supplied {@link Selectable}; must not be {@code
108   * null}
109   *
110   * @see #cachingSelectableOf(Matcher, Map, Collection)
111   *
112   * @see Reducible#of(Selectable, Reducer)
113   *
114   * @see RankedReducer
115   */
116  public Beans(final Selectable<AttributedType, Bean<?>> s,
117               final Reducible<AttributedType, Bean<?>> r) {
118    this.s = Objects.requireNonNull(s, "s");
119    this.r = Objects.requireNonNull(r, "r");
120  }
121
122
123  /*
124   * Instance methods.
125   */
126
127
128  @Override // Selectable<AttributedType, Bean<?>>
129  public final List<Bean<?>> select(final AttributedType c) {
130    return this.s.select(c);
131  }
132
133  @Override // Reducible<AttributedType, Bean<?>>
134  public final Bean<?> reduce(final AttributedType c) {
135    return this.r.reduce(c);
136  }
137
138
139  /*
140   * Static methods.
141   */
142
143
144  /**
145   * Returns a new {@link Selectable} that caches its results.
146   *
147   * <p>The cache is unbounded.</p>
148   *
149   * @param idMatcher an {@link IdMatcher}; must not be {@code null}
150   *
151   * @param selections a (normally empty) {@link Map} of precomputed selections; must not be {@code null}
152   *
153   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
154   *
155   * @return a new {@link Selectable}; never {@code null}
156   *
157   * @exception NullPointerException if any argument is {@code null}
158   */
159  public static final Selectable<AttributedType, Bean<?>> cachingSelectableOf(final Matcher<? super AttributedType, ? super Id> idMatcher,
160                                                                              final Map<? extends AttributedType, ? extends List<Bean<?>>> selections,
161                                                                              final Collection<? extends Bean<?>> beans) {
162    Objects.requireNonNull(idMatcher, "idMatcher");
163    final Map<AttributedType, List<Bean<?>>> selectionCache = new ConcurrentHashMap<>();
164    final ArrayList<Bean<?>> newBeans = new ArrayList<>(31); // 31 == arbitrary
165    final Set<Bean<?>> newBeansSet = newHashSet(31); // 31 == arbitrary
166    for (final Entry<? extends AttributedType, ? extends List<Bean<?>>> e : selections.entrySet()) {
167      final List<Bean<?>> selection = e.getValue();
168      if (!selection.isEmpty()) {
169        final Set<Bean<?>> newSelectionSet = newHashSet(7); // 7 == arbitrary
170        final ArrayList<Bean<?>> newSelection = new ArrayList<>(selection.size());
171        for (final Bean<?> b : selection) {
172          if (newSelectionSet.add(b)) {
173            newSelection.add(b);
174          }
175          if (newBeansSet.add(b)) {
176            newBeans.add(b);
177          }
178        }
179        newSelectionSet.clear();
180        newSelection.trimToSize();
181        Collections.sort(newSelection, byAlternateThenByRankComparator);
182        selectionCache.put(e.getKey(), Collections.unmodifiableList(newSelection));
183      }
184    }
185    for (final Bean<?> bean : beans) {
186      if (newBeansSet.add(bean)) {
187        newBeans.add(bean);
188      }
189    }
190    newBeansSet.clear();
191    if (newBeans.isEmpty()) {
192      return Beans::empty;
193    }
194    Collections.sort(newBeans, byAlternateThenByRankComparator);
195    newBeans.trimToSize();
196    return attributedType -> selectionCache.computeIfAbsent(attributedType, at -> newBeans.stream().filter(b -> idMatcher.test(at, b.id())).toList());
197  }
198
199  private static final <C, T> List<T> empty(final C ignored) {
200    return List.of();
201  }
202
203}