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<AttributedType, Bean<?>>} 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}