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<? extends AnnotatedConstruct>}, 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<? extends AnnotatedConstruct>} 269 * representing this {@link DependencyResolution}'s dependency. 270 * 271 * @return the non-{@code null}, determinate {@link Annotated Annotated<? extends AnnotatedConstruct>} 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}