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}