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.Queue;
021
022import java.util.function.BiFunction;
023import java.util.function.Supplier;
024
025import javax.lang.model.type.TypeMirror;
026
027import org.microbean.attributes.Attributed;
028import org.microbean.attributes.Attributes;
029import org.microbean.attributes.BooleanValue;
030
031import org.microbean.bean.AmbiguousReductionException;
032import org.microbean.bean.AttributedType;
033import org.microbean.bean.Bean;
034import org.microbean.bean.Creation;
035import org.microbean.bean.Factory;
036import org.microbean.bean.Id;
037import org.microbean.bean.RankedReducer;
038import org.microbean.bean.Reducer;
039import org.microbean.bean.Reducible;
040import org.microbean.bean.Selectable;
041
042import org.microbean.construct.Domain;
043
044import org.microbean.reference.Instances;
045
046import static org.microbean.assign.Qualifiers.anyQualifier;
047import static org.microbean.assign.Qualifiers.primordialQualifier;
048import static org.microbean.assign.Qualifiers.qualifier;
049
050/**
051 * An {@link Instances} implementation that is based on scopes.
052 *
053 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
054 *
055 * @see #supplier(Bean, Creation)
056 *
057 * @see Instances
058 */
059public class ScopedInstances implements Instances {
060
061
062  /*
063   * Static fields.
064   */
065
066
067  private static final Attributes FOR_INSTANTIATION = Attributes.of("ForInstantiation");
068
069
070  /*
071   * Instance fields.
072   */
073
074
075  private final TypeMirror scopeletType;
076
077
078  /*
079   * Constructors.
080   */
081
082
083  /**
084   * Creates a new {@link ScopedInstances}.
085   *
086   * @param domain a {@link Domain}; must not be {@code null}
087   *
088   * @exception NullPointerException if {@code domain} is {@code null}
089   */
090  public ScopedInstances(final Domain domain) {
091    super();
092    this.scopeletType = scopeletType(domain);
093  }
094
095
096  /*
097   * Instance methods.
098   */
099
100
101  /**
102   * Calls the {@link #findScopeId(Collection)} method with the result of an invocation of the {@link
103   * Attributes#attributes()} method on the supplied {@link Attributes} and returns the result.
104   *
105   * @param a an {@link Attributes}; normally itself a scope; must not be {@code null}
106   *
107   * @return the first {@link Attributes} found in the supplied {@link Attributes}' {@linkplain Attributes#attributes()
108   * attributes} that is a scope, or {@code null}
109   *
110   * @exception NullPointerException if {@code a} is {@code null}
111   *
112   * @see #findScopeId(Collection)
113   */
114  private final Attributes findScopeId(final Attributes a) {
115    return this.findScopeId(a.attributes());
116  }
117
118  private final Attributes findScopeId(final Id id) {
119    // Looks for an Any qualifier, which every bean must possess, and then looks on *it* for the scope. This allows us
120    // to "tunnel" scopes (which are Qualifiers in this implementation) without disrupting typesafe resolution, since
121    // meta-attributes are not part of an Attributes' equality computation.
122    final Object anyQualifier = anyQualifier();
123    Attributes scopeId = null;
124    for (final Attributes a : id.attributes()) {
125      if (a.equals(anyQualifier)) {
126        scopeId = this.findScopeId(a);
127        break;
128      }
129    }
130    if (scopeId == null) {
131      throw new IllegalArgumentException("id: " + id);
132    }
133    return scopeId;
134  }
135
136  /**
137   * Finds and returns the <dfn>nearest</dfn> scope identifier in the forest represented by the supplied {@link
138   * Attributes}.
139   *
140   * @param c a {@link Collection} of {@link Attributes}; must not be {@code null}
141   *
142   * @return the <dfn>nearest</dfn> scope identifier in the forest represented by the supplied {@link
143   * Attributes}, or {@code null}
144   *
145   * @exception NullPointerException if {@code c} is {@code null}
146   */
147  protected Attributes findScopeId(final Collection<? extends Attributes> c) {
148    if (c.isEmpty()) {
149      return null;
150    }
151    // Breadth first on purpose. Scope Attributes closer to the Attributes they attribute win over Scope Attributes
152    // further away.
153    final Queue<Attributes> q = new ArrayDeque<>(c);
154    while (!q.isEmpty()) {
155      final Attributes a = q.poll();
156      if (this.isScopeId(a)) {
157        return a;
158      }
159      q.addAll(a.attributes());
160    }
161    return null;
162  }
163
164  /**
165   * Returns {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a
166   * <dfn>scope</dfn>.
167   *
168   * @param a an {@link Attributes}; must not be {@code null}
169   *
170   * @return {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a scope
171   *
172   * @exception NullPointerException if {@code a} is {@code null}
173   */
174  protected boolean isScopeId(final Attributes a) {
175    boolean scopeFound = false;
176    boolean qualifierFound = false;
177    for (final Attributes a0 : a.attributes()) {
178      if (scopeFound) {
179        if (!qualifierFound && a0.equals(qualifier())) {
180          return true;
181        }
182      } else if (qualifierFound) {
183        if (a0.equals(Scopelet.SCOPE)) {
184          return true;
185        }
186      } else if (a0.equals(Scopelet.SCOPE)) {
187        scopeFound = true;
188      } else if (a0.equals(qualifier())) {
189        qualifierFound = true;
190      }
191    }
192    return false;
193  }
194
195  private final boolean normal(final Attributes a) {
196    final BooleanValue v = a.value("normal");
197    return v != null && v.value();
198  }
199
200  private final boolean primordial(final Attributed a) {
201    return this.primordial(a.attributes());
202  }
203
204  /**
205   * Returns {@code true} if and only if the supplied {@link Collection} of {@link Attributes} is deemed to designate
206   * something as <dfn>primordial</dfn>.
207   *
208   * <p>The default implementation of this method returns {@code true} if and only if the supplied {@link Collection}
209   * {@linkplain Collection#contains(Object) contains} the {@linkplain
210   * org.microbean.assign.Qualifiers#primordialQualifier() primordial qualifier}.</p>
211   *
212   * @param c a {@link Collection}; must not be {@code null}
213   *
214   * @return {@code true} if and only if the supplied {@link Collection} of {@link Attributes} is deemed to designate
215   * something as <dfn>primordial</dfn>
216   *
217   * @exception NullPointerException if {@code c} is {@code null}
218   */
219  protected boolean primordial(final Collection<? extends Attributes> c) {
220    return c.contains(primordialQualifier());
221  }
222
223  /**
224   * Returns {@code true} if and only if the supplied {@link Id} is <dfn>proxiable</dfn>.
225   *
226   * @param id an {@link Id}; must not be {@code null}
227   *
228   * @return {@code true} if and only if the supplied {@link Id} is <dfn>proxiable</dfn>
229   *
230   * @exception NullPointerException if {@code id} is {@code null}
231   */
232  @Override // Instances
233  public boolean proxiable(final Id id) {
234    if (!id.types().proxiable()) {
235      return false;
236    }
237    final Attributes scopeId = this.findScopeId(id);
238    return scopeId != null && this.normal(scopeId);
239  }
240
241  @Override // Instances
242  public final <I> Supplier<? extends I> supplier(final Bean<I> bean, final Creation<I> request) {
243    final Id id = bean.id();
244    final Attributes scopeId = this.findScopeId(id);
245    // In this implementation, all Ids must have scopes.
246    if (scopeId == null) {
247      throw new IllegalStateException();
248    }
249    final Factory<I> factory = bean.factory();
250    if (factory instanceof Scopelet<?> && this.primordial(scopeId)) {
251      // This is a request for, e.g., the Singleton Scopelet, which backs the primordial (notional) singleton scope.
252      // Scopelets are always their own factories. The Scopelet implementing the primordial scope (normally Singleton)
253      // is not made or stored by any other Scopelet.
254      final I scopelet = factory.singleton();
255      if (scopelet == null) {
256        return () -> factory.create(request);
257      }
258      assert scopelet == factory : "scopelet != factory: " + scopelet + " != " + factory;
259      return factory::singleton;
260    }
261    final AttributedType t = AttributedType.of(this.scopeletType, findScopeId(scopeId), FOR_INSTANTIATION);
262    return () -> request.<Scopelet<?>>references(t).get().instance(id, factory, request); // assumes a specific kind of reduction; see #reducible
263  }
264
265
266  /*
267   * Static methods.
268   */
269
270
271  // Invoked by method reference only
272  static final Bean<?> handleInactiveScopelets(final Collection<? extends Bean<?>> beans, final AttributedType attributedType) {
273    if (beans.size() < 2) { // 2 because we're disambiguating
274      throw new IllegalArgumentException("beans: " + beans);
275    }
276    Bean<?> b2 = null;
277    Scopelet<?> s2 = null;
278    final Iterator<? extends Bean<?>> i = beans.iterator(); // we use Iterator for good reasons
279    while (i.hasNext()) {
280      final Bean<?> b1 = i.next();
281      if (b1.factory() instanceof Scopelet<?> s1) {
282        if (s2 == null) {
283          assert b2 == null;
284          if (i.hasNext()) {
285            b2 = i.next();
286            if (b2.factory() instanceof Scopelet<?> s) {
287              s2 = s;
288            } else {
289              s2 = null;
290              b2 = null;
291              break;
292            }
293          } else {
294            s2 = s1;
295            b2 = b1;
296            break;
297          }
298        }
299        assert b2 != null;
300        // if (s2.scopeId().equals(s1.scopeId())) { // TODO: would like to make this go away
301          if (s2.active()) {
302            if (s1.active()) {
303              throw new TooManyActiveScopeletsException("scopelet1: " + s1 + "; scopelet2: " + s2);
304            }
305            // drop s1; keep s2
306          } else if (s1.active()) {
307            // drop s2; keep s1
308            s2 = s1;
309            b2 = b1;
310          } else {
311            // both are inactive; drop 'em both and keep going
312            s2 = null;
313            b2 = null;
314          }
315        // } else {
316        //   s2 = null;
317        //   b2 = null;
318        //   break;
319        // }
320      } else {
321        s2 = null;
322        b2 = null;
323        break;
324      }
325    }
326    if (s2 == null) {
327      throw new AmbiguousReductionException(attributedType,
328                                            beans,
329                                            "TODO: this message needs to be better; can't resolve these alternates: " + beans);
330    }
331    assert b2 != null;
332    return b2;
333  }
334
335  static final TypeMirror scopeletType(final Domain domain) {
336    return domain.declaredType(null, domain.typeElement(Scopelet.class.getCanonicalName()), domain.wildcardType());
337  }
338
339  /**
340   * Returns a {@link Reducible} suitable for use with {@link Scopelet}s.
341   *
342   * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null}
343   *
344   * @param selectable a {@link Selectable}; must not be {@code null}
345   *
346   * @return a non-{@code null} {@link Reducible}
347   *
348   * @exception NullPointerException if any argument is {@code null}
349   *
350   * @see #reducible(Domain, Selectable, Reducer)
351   *
352   * @see RankedReducer#of()
353   */
354  public static final Reducible<AttributedType, Bean<?>> reducible(final Domain domain,
355                                                                   final Selectable<AttributedType, Bean<?>> selectable) {
356    return reducible(domain, selectable, RankedReducer.of());
357  }
358
359  /**
360   * Returns a {@link Reducible} suitable for use with {@link Scopelet}s.
361   *
362   * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null}
363   *
364   * @param selectable a {@link Selectable}; must not be {@code null}
365   *
366   * @param reducer a {@link Reducer}; must not be {@code null}
367   *
368   * @return a non-{@code null} {@link Reducible}
369   *
370   * @exception NullPointerException if any argument is {@code null}
371   *
372   * @see #reducible(Domain, Selectable, Reducer, BiFunction)
373   *
374   * @see Reducer#fail(List, Object)
375   */
376  public static final Reducible<AttributedType, Bean<?>> reducible(final Domain domain,
377                                                                   final Selectable<AttributedType, Bean<?>> selectable,
378                                                                   final Reducer<AttributedType, Bean<?>> reducer) {
379    return reducible(domain, selectable, reducer, Reducer::fail);
380  }
381
382  /**
383   * Returns a {@link Reducible} suitable for use with {@link Scopelet}s.
384   *
385   * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null}
386   *
387   * @param selectable a {@link Selectable}; must not be {@code null}
388   *
389   * @param reducer a {@link Reducer}; must not be {@code null}
390   *
391   * @param failureHandler a {@link BiFunction} serving as the supplied {@link Reducer}'s <dfn>failure handler</dfn>;
392   * must not be {@code null}
393   *
394   * @return a non-{@code null} {@link Reducible}
395   *
396   * @exception NullPointerException if any argument is {@code null}
397   */
398  public static final Reducible<AttributedType, Bean<?>>
399    reducible(final Domain domain,
400              final Selectable<AttributedType, Bean<?>> selectable,
401              final Reducer<AttributedType, Bean<?>> reducer,
402              final BiFunction<? super List<? extends Bean<?>>, ? super AttributedType, ? extends Bean<?>> failureHandler) {
403    // Normal reductions are cached.
404    final Reducible<AttributedType, Bean<?>> cachingReducible = Reducible.ofCaching(selectable, reducer, failureHandler);
405    // Reductions of scopelets can't be cached because a Scopelet may be active or inactive at any point for any reason.
406    final Reducible<AttributedType, Bean<?>> scopeletReducible =
407      Reducible.<AttributedType, Bean<?>>of(selectable, reducer, ScopedInstances::handleInactiveScopelets);
408    final TypeMirror scopeletType = scopeletType(domain);
409    return c ->
410      (domain.sameType(scopeletType, c.type()) && c.attributes().contains(FOR_INSTANTIATION) ?
411       // A ScopedInstances is requesting a Scopelet for the purposes of instantiating something else. Use the
412       // scopeletReducible.
413       scopeletReducible :
414       // A ScopedInstances is requesting something "normal". Use the cachingReducible.
415       cachingReducible)
416      .reduce(c);
417  }
418
419}