001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025–2026 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.model;
015
016import java.util.Collection;
017import java.util.List;
018import java.util.Map;
019import java.util.Set;
020
021import java.util.concurrent.ConcurrentHashMap;
022
023import java.util.function.BiFunction;
024import java.util.function.Function;
025
026import javax.lang.model.AnnotatedConstruct;
027
028import javax.lang.model.element.Element;
029
030import org.microbean.assign.Aggregate;
031import org.microbean.assign.Annotated;
032import org.microbean.assign.Selectable;
033
034import org.microbean.bean.Bean;
035
036import static java.util.HashMap.newHashMap;
037
038import static java.util.HashSet.newHashSet;
039
040import static java.util.Objects.requireNonNull;
041
042import static java.util.stream.Collectors.toUnmodifiableSet;
043
044/**
045 * An immutable model of a system's {@linkplain DependencyResolution dependency resolutions}.
046 *
047 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
048 *
049 * @see #toSelectionCache()
050 *
051 * @see #valid()
052 */
053public final class Model {
054
055  private final Set<DependencyResolution> dependencyResolutions;
056
057  private volatile boolean valid;
058
059  private Model() {
060    this(Set.of());
061  }
062
063  private Model(Set<? extends DependencyResolution> dependencyResolutions) {
064    super();
065    if (dependencyResolutions == null || dependencyResolutions.isEmpty()) {
066      this.dependencyResolutions = Set.of();
067      this.valid = true;
068    } else {
069      this.dependencyResolutions = Set.copyOf(dependencyResolutions);
070    }
071  }
072
073  @Override // Object
074  public final boolean equals(final Object other) {
075    return switch (other) {
076    case null -> false;
077    case Model m when this.getClass() == m.getClass() -> this.dependencyResolutions.equals(m.dependencyResolutions);
078    default -> false;
079    };
080  }
081
082  @Override // Object
083  public final int hashCode() {
084    return this.dependencyResolutions.hashCode();
085  }
086
087  /**
088   * Returns {@code true} if and only if this {@link Model} is <dfn>valid</dfn>.
089   *
090   * <p>A {@link Model} is valid if and only if, for each of its {@link DependencyResolution}s, there {@linkplain
091   * DependencyResolution#beans() exists} exactly one {@link Bean}.</p>
092   *
093   * @return {@code true} if and only if this {@link Model} is <dfn>valid</dfn>
094   *
095   * @see #toSelectionCache()
096   */
097  public final boolean valid() {
098    if (this.valid) { // volatile read
099      return true;
100    }
101    for (final DependencyResolution dependencyResolution : this.dependencyResolutions) {
102      if (dependencyResolution.beans().size() != 1) {
103        return false;
104      }
105    }
106    this.valid = true; // volatile write
107    return true;
108  }
109
110  /**
111   * Returns a non-{@code null}, immutable, determinate {@link Set} of all {@link DependencyResolution}s managed by this
112   * {@link Model} that are {@linkplain DependencyResolution#ambiguous() ambiguous}.
113   *
114   * @return a non-{@code null}, immutable, determinate {@link Set} of all {@link DependencyResolution}s managed by this
115   * {@link Model} that are {@linkplain DependencyResolution#ambiguous() ambiguous}
116   *
117   * @see DependencyResolution#ambiguous()
118   */
119  public final Set<DependencyResolution> ambiguousDependencyResolutions() {
120    return this.dependencyResolutions.stream().filter(DependencyResolution::ambiguous).collect(toUnmodifiableSet());
121  }
122
123  /**
124   * Returns a non-{@code null}, immutable, determinate {@link Set} of all {@link DependencyResolution}s managed by this
125   * {@link Model} that are {@linkplain DependencyResolution#unsatisfied() unsatisfied}.
126   *
127   * @return a non-{@code null}, immutable, determinate {@link Set} of all {@link DependencyResolution}s managed by this
128   * {@link Model} that are {@linkplain DependencyResolution#unsatisfied() unsatisfied}
129   *
130   * @see DependencyResolution#unsatisfied()
131   */
132  public final Set<DependencyResolution> unsatisfiedDependencyResolutions() {
133    return this.dependencyResolutions.stream().filter(DependencyResolution::unsatisfied).collect(toUnmodifiableSet());
134  }
135
136  /**
137   * Returns a non-{@code null} {@link BiFunction} that represents a compute-if-absent operation on an internal,
138   * unbounded, thread-safe cache.
139   *
140   * <p>The return value is suitable for passing to the {@link org.microbean.assign.Selectables#caching(Selectable,
141   * BiFunction)} method.</p>
142   *
143   * @return a non-{@code null} {@link BiFunction} that represents a compute-if-absent operation on an internal,
144   * unbounded, thread-safe cache
145   *
146   * @exception IllegalStateException if this {@link Model} is not {@linkplain #valid() valid}
147   *
148   * @see org.microbean.assign.Selectables#caching(Selectable, BiFunction)
149   *
150   * @see #ambiguousDependencyResolutions()
151   *
152   * @see #unsatisfiedDependencyResolutions()
153   */
154  // So you can do:
155  // org.microbean.assign.Selectables.caching(selectable, model.toSelectionCache());
156  public final BiFunction<? super Annotated<? extends AnnotatedConstruct>, Function<? super Annotated<? extends AnnotatedConstruct>, ? extends List<Bean<?>>>, ? extends List<Bean<?>>> toSelectionCache() {
157    if (!this.valid()) {
158      throw new IllegalStateException("not valid");
159    }
160    // TODO: look at this carefully. We have Annotated now, which makes an AnnotatedConstruct and its annotations
161    // cacheable. We may not need AnnotatedConstruct in here, just TypeMirror.
162    final Map<Annotated<? extends AnnotatedConstruct>, List<Bean<?>>> m = new ConcurrentHashMap<>(this.dependencyResolutions.size()); // too big but whatever
163    for (final DependencyResolution dr : this.dependencyResolutions) {
164      m.putIfAbsent(dr.annotated(), dr.beans());
165    }
166    return m::computeIfAbsent; // m is mutable on purpose; we can revisit if we decide that a Model is the absolute source of truth
167  }
168
169  /**
170   * Returns a non-{@code null} {@link Model} built from the supplied {@link Selectable}.
171   *
172   * @param s a non-{@code null} {@link Selectable}
173   *
174   * @return a non-{@code null} {@link Model} built from the supplied {@link Selectable}
175   *
176   * @exception NullPointerException if {@code s} is {@code null}
177   *
178   * @see Selectable
179   */
180  // s would normally be typesafeFiltering and ambiguityReducing but not necessarily cached
181  @SuppressWarnings("unchecked")
182  public static Model of(final Selectable<? super Annotated<? extends AnnotatedConstruct>, ? extends Bean<?>> s) {
183    final Collection<? extends Aggregate> allBeans = s.select(null);
184    if (allBeans.isEmpty()) {
185      return new Model(Set.of());
186    }
187    final Map<Annotated<? extends AnnotatedConstruct>, List<Bean<?>>> m = newHashMap(allBeans.size() * 5); // estimate;
188    final Set<DependencyResolution> dependencyResolutions = newHashSet(allBeans.size() * 5);
189    for (final Aggregate bean : allBeans) {
190      for (final Annotated<? extends Element> dependency : bean.dependencies()) {
191        // TODO: This is irritating. A Bean's dependencies are basically Elements (that's good) but all this Set
192        // business is designed to reduce demand down to qualified types (e.g. TypeMirror, but with Element
193        // annotations).
194        //
195        // Time has passed and now we have the Annotated interface from microbean-assign. This is still a slight
196        // mess. We still probably want to "reduce" an Annotated<Element> to an Annotated<TypeMirror> and cache that
197        // sucker.
198        //
199        // Resuming commentary: That is, you may have 37 (Annotated) Elements that all "have" (asType()) the same
200        // TypeMirror and notional set of annotations, and you really want your model to reflect just the one.
201        //
202        // Now you get into type equality etc. (See
203        // https://github.com/microbean/microbean-construct/issues/31#issuecomment-3565216353 and
204        // https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/lang/model/type/TypeMirror.html#equals(java.lang.Object)
205        // and
206        // https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/lang/model/util/Types.html#isSameType(javax.lang.model.type.TypeMirror,javax.lang.model.type.TypeMirror).)
207        //
208        // Probably what we want to do is a laborious "bring your own Set" implementation where we do not use hashcodes
209        // (there's no way to "do" a hashcode of a TypeMirror that is "compatible" with sametypeness). So keep a running
210        // list of TypeMirrors that we have seen (as determined by sameType).
211        //
212        // Not sure this is right, actually. The solution may be to do work inside the Selectable and the caching
213        // mechanics. Specifically, if a Selectable works on an AnnotatedConstruct, then a caching version of that
214        // should reduce the AnnotatedConstruct to a TypeMirror (with annotations perhaps propagated from the Element to
215        // the TypeMirror)
216        //
217        // Then we want to make synthetic types (ugh) for ...
218        //
219        // See also https://github.com/jakartaee/cdi/issues/877 and https://github.com/jakartaee/inject/issues/40
220        //
221        // Other things that are tangentially related: https://github.com/openjdk/jdk/pull/24775#issuecomment-3133482962
222        dependencyResolutions.add(new DependencyResolution(dependency,
223                                                           m.computeIfAbsent(dependency,
224                                                                             aac -> (List<Bean<?>>)s.select(aac))));
225      }
226    }
227    return new Model(dependencyResolutions);
228  }
229
230  /**
231   * A representation of the attempted <dfn>resolution</dfn> of a <dfn>dependency</dfn>, represented by an {@link
232   * Annotated Annotated&lt;? extends AnnotatedConstruct&gt;}, to a {@link List} of {@link Bean}s that match it.
233   *
234   * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
235   *
236   * @see Annotated
237   *
238   * @see Annotated#of(AnnotatedConstruct)
239   *
240   * @see Bean
241   */
242  public static final class DependencyResolution {
243
244    private final Annotated<? extends AnnotatedConstruct> annotated;
245
246    private final List<Bean<?>> beans;
247
248    private DependencyResolution(final Annotated<? extends AnnotatedConstruct> annotated, final List<Bean<?>> beans) {
249      super();
250      this.annotated = requireNonNull(annotated, "annotated");
251      this.beans = List.copyOf(beans);
252    }
253
254    /**
255     * Returns {@code true} if and only if the return value of an invocation of the {@link #beans()} method {@linkplain
256     * List#size() has a size} greater than {@code 1}.
257     *
258     * @return {@code true} if and only if the return value of an invocation of the {@link #beans()} method {@linkplain
259     * List#size() has a size} greater than {@code 1}
260     *
261     * @see #beans()
262     */
263    public final boolean ambiguous() {
264      return this.beans().size() > 1;
265    }
266
267    /**
268     * Returns the non-{@code null}, determinate {@link Annotated Annotated&lt;? extends AnnotatedConstruct&gt;}
269     * representing this {@link DependencyResolution}'s dependency.
270     *
271     * @return the non-{@code null}, determinate {@link Annotated Annotated&lt;? extends AnnotatedConstruct&gt;}
272     * representing this {@link DependencyResolution}'s dependency
273     */
274    public final Annotated<? extends AnnotatedConstruct> annotated() {
275      return this.annotated;
276    }
277
278    /**
279     * Returns the non-{@code null}, immutable, determinate {@link List} of {@link Bean}s representing this {@link
280     * DependencyResolution}'s dependency resolution.
281     *
282     * @return the non-{@code null}, immutable, determinate {@link List} of {@link Bean}s representing this {@link
283     * DependencyResolution}'s dependency resolution
284     */
285    public final List<Bean<?>> beans() {
286      return this.beans;
287    }
288
289    @Override // Object
290    public final boolean equals(final Object other) {
291      return this == other || switch (other) {
292      case null -> false;
293      case DependencyResolution dr when dr.getClass() == this.getClass() -> this.annotated.equals(dr.annotated) && this.beans.equals(dr.beans);
294      default -> false;
295      };
296    }
297
298    @Override // Object
299    public final int hashCode() {
300      return this.annotated.hashCode() ^ this.beans.hashCode();
301    }
302
303    /**
304     * Returns {@code true} if and only if the return value of an invocation of the {@link #beans()} method {@linkplain
305     * List#isEmpty() is empty}.
306     *
307     * @return {@code true} if and only if the return value of an invocation of the {@link #beans()} method {@linkplain
308     * List#isEmpty() is empty}
309     *
310     * @see #beans()
311     */
312    public final boolean unsatisfied() {
313      return this.beans().isEmpty();
314    }
315
316    @Override // Object
317    public final String toString() {
318      return this.annotated() + " = " + this.beans();
319    }
320
321  }
322
323}