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.scopelet;
015
016import java.util.ArrayDeque;
017import java.util.Collection;
018import java.util.Iterator;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Queue;
023import java.util.Set;
024
025import java.util.function.BiFunction;
026import java.util.function.BooleanSupplier;
027import java.util.function.Supplier;
028
029import javax.lang.model.type.TypeMirror;
030
031import org.microbean.assign.AttributedType;
032import org.microbean.assign.Selectable;
033
034import org.microbean.attributes.Attributed;
035import org.microbean.attributes.Attributes;
036import org.microbean.attributes.BooleanValue;
037
038import org.microbean.bean.AmbiguousResolutionException;
039import org.microbean.bean.Bean;
040import org.microbean.bean.Creation;
041import org.microbean.bean.Factory;
042import org.microbean.bean.Id;
043import org.microbean.bean.Qualifiers;
044import org.microbean.bean.ReferencesSelector;
045
046import org.microbean.construct.Domain;
047
048import org.microbean.reference.Instances;
049
050import static java.util.Objects.requireNonNull;
051
052/**
053 * An {@link Instances} implementation that is based on scopes.
054 *
055 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
056 *
057 * @see #supplier(Bean, Creation)
058 *
059 * @see Instances
060 */
061public class ScopedInstances implements Instances {
062
063
064  /*
065   * Static fields.
066   */
067
068
069  // Note: deliberately not a scope or qualifier
070  private static final Attributes CONSIDER_ACTIVENESS = Attributes.of("ConsiderActiveness");
071  
072
073  /*
074   * Instance fields.
075   */
076
077
078  private final Qualifiers qualifiers;
079
080  private final Scopes scopes;
081  
082  private final TypeMirror scopeletType;
083
084
085  /*
086   * Constructors.
087   */
088
089
090  /**
091   * Creates a new {@link ScopedInstances}.
092   *
093   * @param domain a {@link Domain}; must not be {@code null}
094   *
095   * @param qualifiers a {@link Qualifiers}; must not be {@code null}
096   *
097   * @param scopes a {@link Scopes}; must not be {@code null}
098   *
099   * @exception NullPointerException if any argument is {@code null}
100   */
101  public ScopedInstances(final Domain domain, final Qualifiers qualifiers, final Scopes scopes) {
102    super();
103    this.qualifiers = requireNonNull(qualifiers, "qualifiers");
104    this.scopes = requireNonNull(scopes, "scopes");
105    this.scopeletType = scopeletType(domain);
106  }
107
108
109  /*
110   * Instance methods.
111   */
112
113
114  /**
115   * Returns {@code true} if and only if the supplied {@link Id} is <dfn>proxiable</dfn>.
116   *
117   * @param id an {@link Id}; must not be {@code null}
118   *
119   * @return {@code true} if and only if the supplied {@link Id} is <dfn>proxiable</dfn>
120   *
121   * @exception NullPointerException if {@code id} is {@code null}
122   */
123  @Override // Instances
124  public boolean proxiable(final Id id) {
125    if (!id.types().proxiable()) {
126      return false;
127    }
128    final Attributes scopeId = this.findScope(id);
129    return scopeId != null && this.scopes.normal(scopeId);
130  }
131
132  @Override // Instances
133  public final <I> Supplier<? extends I> supplier(final Bean<I> bean, final Creation<I> request) {
134    final Id id = bean.id();
135    final Attributes scopeId = this.findScope(id);
136    // In this implementation, all Ids must have scopes.
137    if (scopeId == null) {
138      throw new IllegalStateException();
139    }
140    final Factory<I> factory = bean.factory();
141    if (factory instanceof Scopelet<?> && this.primordial(scopeId)) {
142      // This is a request for, e.g., the Singleton Scopelet, which backs the primordial (notional) singleton scope.
143      // Scopelets are always their own factories. The Scopelet implementing the primordial scope (normally Singleton)
144      // is not made or stored by any other Scopelet.
145      I scopelet = factory.singleton();
146      if (scopelet == null) {
147        return () -> factory.create(request);
148      }
149      assert scopelet == factory : "scopelet != factory: " + scopelet + " != " + factory;
150      return factory::singleton;
151    }
152    final AttributedType st = this.scopeletAttributedType(scopeId);
153    // Get the Scopelet and have it provide the instance
154    return () -> request.<Scopelet<?>>reference(st).instance(id, factory, request); // assumes Scopelet inactivity is handled
155  }
156
157  /*
158   * Returns {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a
159   * <dfn>scope</dfn>.
160   *
161   * @param a an {@link Attributes}; must not be {@code null}
162   *
163   * @return {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a scope
164   *
165   * @exception NullPointerException if {@code a} is {@code null}
166   *
167   * @see Scopes#scope(Attributes)
168   *
169   * @deprecated Use {@link Scopes#scope(Attributes)} instead.
170   */
171  // @Deprecated(forRemoval = true)
172  // protected boolean isScopeId(final Attributes a) {
173  //   return this.scopes.scope(a);
174  // }
175
176  /**
177   * Returns {@code true} if and only if the supplied {@link Collection} of {@link Attributes} is deemed to designate
178   * something as <dfn>primordial</dfn>.
179   *
180   * <p>The default implementation of this method returns {@code true} if and only if the supplied {@link Collection}
181   * {@linkplain Collection#contains(Object) contains} the {@linkplain
182   * org.microbean.bean.Qualifiers#primordialQualifier() primordial qualifier}.</p>
183   *
184   * @param c a {@link Collection}; must not be {@code null}
185   *
186   * @return {@code true} if and only if the supplied {@link Collection} of {@link Attributes} is deemed to designate
187   * something as <dfn>primordial</dfn>
188   *
189   * @exception NullPointerException if {@code c} is {@code null}
190   *
191   * @see Qualifiers#primordialQualifier()
192   */
193  protected boolean primordial(final Collection<? extends Attributes> c) {
194    return c.contains(this.qualifiers.primordialQualifier());
195  }
196
197  /**
198   * Finds and returns the <dfn>nearest</dfn> scope identifier in the forest represented by the supplied {@link
199   * Attributes}.
200   *
201   * @param c a {@link Collection} of {@link Attributes}; must not be {@code null}
202   *
203   * @return the <dfn>nearest</dfn> scope identifier in the forest represented by the supplied {@link
204   * Attributes}, or {@code null}
205   *
206   * @exception NullPointerException if {@code c} is {@code null}
207   *
208   * @see Scopes#findScope(Collection)
209   *
210   * @deprecated Please use {@link Scopes#findScope(Collection)} instead.
211   */
212  @Deprecated(forRemoval = true)
213  final Attributes findScopeId(final Collection<? extends Attributes> c) {
214    return this.scopes.findScope(c);
215  }
216
217  private final Attributes findScope(final Id id) {
218    // Looks for an Any qualifier, which every bean must possess, and then looks on *it* for the scope. This allows us
219    // to "tunnel" scopes (which are Qualifiers in this implementation) without disrupting typesafe resolution, since
220    // meta-attributes are not part of an Attributes' equality computation.
221    final Object anyQualifier = this.qualifiers.anyQualifier();
222    Attributes scopeId = null;
223    for (final Attributes a : id.attributes()) {
224      if (a.equals(anyQualifier)) {
225        scopeId = this.scopes.findScope(a.attributes());
226        break;
227      }
228    }
229    if (scopeId == null) {
230      throw new IllegalArgumentException("id: " + id);
231    }
232    return scopeId;
233  }
234
235  private final boolean primordial(final Attributed a) {
236    return this.primordial(a.attributes());
237  }
238
239  private final AttributedType scopeletAttributedType(final Attributes scopeId) {
240    return AttributedType.of(this.scopeletType, scopeId, CONSIDER_ACTIVENESS);
241  }
242
243
244  /*
245   * Static methods.
246   */
247
248
249  /**
250   * Returns a {@link Selectable Selectable&lt;AttributedType, Bean&lt;?&gt;&gt;} that properly considers the fact that
251   * a {@link Scopelet} may be {@linkplain Scopelet#active() active or inactive} at any point for any reason.
252   *
253   * @param domain a {@link Domain}; must not be {@code null}
254   *
255   * @param selectable a {@link Selectable} that will be used for all {@link AttributedType}s other than {@link
256   * Scopelet} types being sought for the purpose of instantiating or acquiring contextual instances; must not be {@code
257   * null}
258   *
259   * @return a non-{@code null} {@link Selectable}
260   *
261   * @exception  NullPointerException if any argument is {@code null}
262   */
263  public static final Selectable<AttributedType, Bean<?>> selectableOf(final Domain domain,
264                                                                       final Selectable<AttributedType, Bean<?>> selectable) {
265    Objects.requireNonNull(selectable, "selectable");
266    final Selectable<AttributedType, Bean<?>> scopeletSelectable = c -> {
267      Bean<?> activeScopeletBean = null;
268      for (final Bean<?> b : selectable.select(c)) {
269        if (((Scopelet<?>)b.factory()).active()) {
270          if (activeScopeletBean == null) {
271            activeScopeletBean = b;
272          } else {
273            throw new TooManyActiveScopeletsException("scopelet1: " + activeScopeletBean + "; scopelet2: " + b);
274          }
275        }
276      }
277      return activeScopeletBean == null ? List.of() : List.of(activeScopeletBean);
278    };
279    final TypeMirror scopeletType = scopeletType(domain);
280    return c ->
281      domain.sameType(scopeletType, c.type()) && c.attributes().contains(CONSIDER_ACTIVENESS) ?
282      // A ScopedInstances is requesting a Scopelet for the purposes of instantiating something else. Use the
283      // scopeletSelectable.
284      scopeletSelectable.select(c) :
285      // A ScopedInstances is requesting something "normal". Use the unadorned supplied Selectable.
286      selectable.select(c);
287  }
288
289  // Invoked by method reference only
290  // (Actually, not used?)
291  @Deprecated(forRemoval = true)
292  private static final Bean<?> handleInactiveScopelets(final Collection<? extends Bean<?>> beans, final AttributedType attributedType) {
293    if (beans.size() < 2) { // 2 because we're disambiguating
294      throw new IllegalArgumentException("beans: " + beans);
295    }
296    Bean<?> b2 = null;
297    Scopelet<?> s2 = null;
298    final Iterator<? extends Bean<?>> i = beans.iterator();
299    while (i.hasNext()) {
300      final Bean<?> b1 = i.next();
301      if (b1.factory() instanceof Scopelet<?> s1) {
302        if (s2 == null) {
303          assert b2 == null;
304          if (i.hasNext()) {
305            b2 = i.next();
306            if (b2.factory() instanceof Scopelet<?> s) {
307              s2 = s;
308            } else {
309              s2 = null;
310              b2 = null;
311              break;
312            }
313          } else {
314            s2 = s1;
315            b2 = b1;
316            break;
317          }
318        }
319        assert b2 != null;
320        if (s2.active()) {
321          if (s1.active()) {
322            throw new TooManyActiveScopeletsException("scopelet1: " + s1 + "; scopelet2: " + s2);
323          }
324          // drop s1; keep s2
325        } else if (s1.active()) {
326          // drop s2; keep s1
327          s2 = s1;
328          b2 = b1;
329        } else {
330          // both are inactive; drop 'em both and keep going
331          s2 = null;
332          b2 = null;
333        }
334      } else {
335        s2 = null;
336        b2 = null;
337        break;
338      }
339    }
340    if (s2 == null) {
341      throw new AmbiguousResolutionException(attributedType,
342                                             beans,
343                                             "TODO: this message needs to be better; can't resolve these alternates: " + beans);
344    }
345    assert b2 != null;
346    return b2;
347  }
348
349  private static final TypeMirror scopeletType(final Domain domain) {
350    return domain.declaredType(null, domain.typeElement(Scopelet.class.getCanonicalName()), domain.wildcardType());
351  }
352
353}