001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2024 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; 015 016import java.lang.System.Logger; 017 018import java.lang.constant.ClassDesc; 019import java.lang.constant.Constable; 020import java.lang.constant.ConstantDesc; 021import java.lang.constant.DynamicConstantDesc; 022import java.lang.constant.MethodHandleDesc; 023 024import java.util.ArrayList; 025import java.util.Comparator; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.Set; 031 032import java.util.concurrent.ConcurrentHashMap; 033 034import java.util.function.Predicate; 035 036import javax.lang.model.element.Element; 037import javax.lang.model.element.QualifiedNameable; 038 039import javax.lang.model.type.ArrayType; 040import javax.lang.model.type.DeclaredType; 041import javax.lang.model.type.IntersectionType; 042import javax.lang.model.type.TypeKind; 043import javax.lang.model.type.TypeMirror; 044import javax.lang.model.type.TypeVariable; 045 046import org.microbean.lang.TypeAndElementSource; 047 048import static java.lang.System.Logger.Level.WARNING; 049 050import static java.lang.constant.ConstantDescs.BSM_INVOKE; 051 052import static java.util.HashSet.newHashSet; 053 054import static java.util.stream.Stream.concat; 055 056import static org.microbean.lang.ConstantDescs.CD_TypeAndElementSource; 057 058/** 059 * A utility for working with <dfn>bean types</dfn>. 060 * 061 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 062 * 063 * @see #beanTypes(TypeMirror) 064 * 065 * @see #legalBeanType(TypeMirror) 066 */ 067public final class BeanTypes implements Constable { 068 069 070 /* 071 * Static fields. 072 */ 073 074 075 private static final Logger LOGGER = System.getLogger(BeanTypes.class.getName()); 076 077 078 /* 079 * Instance fields. 080 */ 081 082 083 private final Map<TypeMirror, List<TypeMirror>> beanTypesCache; 084 085 private final Comparator<TypeMirror> c; 086 087 private final TypeAndElementSource tes; 088 089 090 /* 091 * Constructors. 092 */ 093 094 095 /** 096 * Creates a new {@link BeanTypes}. 097 * 098 * @param tes a {@link TypeAndElementSource}; must not be {@code null} 099 */ 100 public BeanTypes(final TypeAndElementSource tes) { 101 super(); 102 this.tes = Objects.requireNonNull(tes, "tes"); 103 this.c = new SpecializationComparator(); 104 this.beanTypesCache = new ConcurrentHashMap<>(); 105 } 106 107 108 /* 109 * Instance methods. 110 */ 111 112 /** 113 * Returns an {@link Optional} housing a {@link ConstantDesc} that represents this {@link BeanTypes}. 114 * 115 * <p>This method never returns {@code null}.</p> 116 * 117 * <p>The default implementation of this method relies on the presence of a {@code public} constructor that accepts a 118 * single {@link TypeAndElementSource}-typed argument.</p> 119 * 120 * <p>The {@link Optional} returned by an invocation of this method may be, and often will be, {@linkplain 121 * Optional#isEmpty() empty}.</p> 122 * 123 * @return an {@link Optional} housing a {@link ConstantDesc} that represents this {@link BeanTypes}; never {@code 124 * null} 125 * 126 * @see Constable#describeConstable() 127 */ 128 @Override // Constable 129 public final Optional<? extends ConstantDesc> describeConstable() { 130 return (this.tes instanceof Constable c ? c.describeConstable() : Optional.<ConstantDesc>empty()) 131 .map(tesDesc -> DynamicConstantDesc.of(BSM_INVOKE, 132 MethodHandleDesc.ofConstructor(ClassDesc.of(this.getClass().getName()), 133 CD_TypeAndElementSource), 134 tesDesc)); 135 } 136 137 /** 138 * Clears caches that may be used internally by this {@link BeanTypes}. 139 * 140 * @idempotency This method may clear internal state but otherwise has no side effects. 141 * 142 * @threadsafety This method is safe for concurrent use by multiple threads. 143 */ 144 public final void clearCaches() { 145 this.beanTypesCache.clear(); 146 } 147 148 /** 149 * Returns an immutable {@link List} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied 150 * {@link TypeMirror} bears. 151 * 152 * <p>The returned {@link List} may be empty.</p> 153 * 154 * @param t a {@link TypeMirror}; must not be {@code null} 155 * 156 * @return an immutable {@link List} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied 157 * {@link TypeMirror} bears; never {@code null} 158 * 159 * @exception NullPointerException if {@code t} is {@code null} 160 * 161 * @nullability This method never returns {@code null}. 162 * 163 * @idempotency This method is idempotent and returns determinate values. 164 * 165 * @threadsafety This method is safe for concurrent use by multiple threads. 166 */ 167 public final List<TypeMirror> beanTypes(final TypeMirror t) { 168 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#assignable_parameters 169 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#legal_bean_types 170 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#managed_bean_types 171 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#producer_field_types 172 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#producer_method_types 173 return switch (t.getKind()) { 174 case ARRAY -> this.beanTypesCache.computeIfAbsent(t, t0 -> legalBeanType(t0) ? List.of(t0, tes.declaredType("java.lang.Object")) : List.of()); 175 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> this.beanTypesCache.computeIfAbsent(t, t0 -> List.of(t0, tes.declaredType("java.lang.Object"))); 176 case DECLARED, TYPEVAR -> this.beanTypesCache.computeIfAbsent(t, t0 -> supertypes(t0, BeanTypes::legalBeanType)); 177 default -> { 178 assert !legalBeanType(t); 179 yield List.of(); 180 } 181 }; 182 } 183 184 final List<TypeMirror> supertypes(final TypeMirror t) { 185 return this.supertypes(t, BeanTypes::returnTrue); 186 } 187 188 private final List<TypeMirror> supertypes(final TypeMirror t, final Predicate<? super TypeMirror> p) { 189 final ArrayList<TypeMirror> nonInterfaceTypes = new ArrayList<>(7); // arbitrary size 190 final ArrayList<TypeMirror> interfaceTypes = new ArrayList<>(17); // arbitrary size 191 supertypes(t, p, nonInterfaceTypes, interfaceTypes, newHashSet(13)); // arbitrary size 192 nonInterfaceTypes.trimToSize(); 193 interfaceTypes.trimToSize(); 194 return 195 concat(nonInterfaceTypes.stream(), // non-interface supertypes are already sorted from most-specific to least 196 interfaceTypes.stream().sorted(this.c)) // have to sort interfaces because you can extend them in any order 197 .toList(); 198 } 199 200 private final void supertypes(final TypeMirror t, 201 final Predicate<? super TypeMirror> p, 202 final ArrayList<? super TypeMirror> nonInterfaceTypes, 203 final ArrayList<? super TypeMirror> interfaceTypes, 204 final Set<? super String> seen) { 205 if (seen.add(name(t))) { 206 if (p.test(t)) { 207 if (isInterface(t)) { 208 interfaceTypes.add(t); // reflexive 209 } else { 210 nonInterfaceTypes.add(t); // reflexive 211 } 212 } 213 for (final TypeMirror directSupertype : tes.directSupertypes(t)) { 214 this.supertypes(directSupertype, p, nonInterfaceTypes, interfaceTypes, seen); // note recursion 215 } 216 } 217 } 218 219 220 /* 221 * Static methods. 222 */ 223 224 225 /** 226 * Returns {@code true} if and only if the supplied {@link TypeMirror} is a <dfn>legal bean type</dfn>. 227 * 228 * <p>Legal bean types are, exactly:</p> 229 * 230 * <ol> 231 * 232 * <li>{@linkplain TypeKind#ARRAY Array} types whose {@linkplain ArrayType#getComponentType() component type}s are 233 * legal bean types</li> 234 * 235 * <li>{@linkplain TypeKind#isPrimitive() Primitive} types</li> 236 * 237 * <li>{@linkplain TypeKind#DECLARED Declared} types that contain no {@linkplain TypeKind#WILDCARD wildcard type}s for 238 * every level of containment</li> 239 * 240 * </ol> 241 * 242 * @param t a {@link TypeMirror}; must not be {@code null} 243 * 244 * @return {@code true} if and only if {@code t} is a legal bean type; {@code false} otherwise 245 * 246 * @exception NullPointerException if {@code t} is {@code null} 247 * 248 * @idempotency This method is idempotent and deterministic. 249 * 250 * @threadsafety This method itself is safe for concurrent use by multiple threads, but {@link TypeMirror} 251 * implementations and {@link TypeAndElementSource} implementations may not be safe for such use. 252 */ 253 public static final boolean legalBeanType(final TypeMirror t) { 254 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#assignable_parameters 255 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#legal_bean_types 256 return switch (t.getKind()) { 257 258 // "A bean type may be an array type." 259 // 260 // "However, some Java types are not legal bean types: [...] An array type whose component type is not a legal bean 261 // type" 262 case ARRAY -> { 263 if (!legalBeanType(((ArrayType)t).getComponentType())) { // note recursion 264 if (LOGGER.isLoggable(WARNING)) { 265 LOGGER.log(WARNING, t + " has a component type that is an illegal bean type (" + ((ArrayType)t).getComponentType() + ")"); 266 } 267 yield false; 268 } 269 yield true; 270 } 271 272 // "A bean type may be a primitive type. Primitive types are considered to be identical to their corresponding 273 // wrapper types in java.lang." 274 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> true; 275 276 // "A bean type may be a parameterized type with actual [see below] type parameters [arguments] and type variables." 277 // 278 // "However, some Java types are not legal bean types: [...] A parameterized type that contains [see below] a 279 // wildcard type parameter [argument] is not a legal bean type." 280 // 281 // Some ink has been spilled on what it means for a "parameterized" (generic) type to "contain" a "wildcard type 282 // parameter [argument]" (https://issues.redhat.com/browse/CDI-502). Because it turns out that "actual type" 283 // apparently means, among other things, a non-wildcard type, it follows that *no* wildcard type argument appearing 284 // *anywhere* in a bean type is permitted. Note that the definition of "actual type" does not appear in the CDI 285 // specification, but only in a closed JIRA issue 286 // (https://issues.redhat.com/browse/CDI-502?focusedId=13036118&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-13036118). 287 // 288 // This still seems way overstrict to me but there you have it. 289 case DECLARED -> { 290 for (final TypeMirror typeArgument : ((DeclaredType)t).getTypeArguments()) { 291 if (typeArgument.getKind() != TypeKind.TYPEVAR && !legalBeanType(typeArgument)) { // note recursion 292 if (LOGGER.isLoggable(WARNING)) { 293 LOGGER.log(WARNING, t + " has a type argument that is an illegal bean type (" + typeArgument + ")"); 294 } 295 yield false; 296 } 297 } 298 yield true; 299 } 300 301 // "A type variable is not a legal bean type." (Nothing else is either.) 302 default -> { 303 if (LOGGER.isLoggable(WARNING)) { 304 LOGGER.log(WARNING, t + " is an illegal bean type"); 305 } 306 yield false; 307 } 308 }; 309 } 310 311 static final String name(final TypeMirror t) { 312 return switch (t.getKind()) { 313 case ARRAY -> name(((ArrayType)t).getComponentType()) + "[]"; 314 case BOOLEAN -> "boolean"; 315 case BYTE -> "byte"; 316 case CHAR -> "char"; 317 case DECLARED -> name(((DeclaredType)t).asElement()); 318 case DOUBLE -> "double"; 319 case FLOAT -> "float"; 320 case INT -> "int"; 321 case INTERSECTION -> { 322 final java.util.StringJoiner sj = new java.util.StringJoiner("&"); 323 for (final TypeMirror bound : ((IntersectionType)t).getBounds()) { 324 sj.add(name(bound)); 325 } 326 yield sj.toString(); 327 } 328 case LONG -> "long"; 329 case SHORT -> "short"; 330 case TYPEVAR -> name(((TypeVariable)t).asElement()); 331 default -> t.toString(); 332 }; 333 } 334 335 static final String name(final Element e) { 336 return e instanceof QualifiedNameable qn ? name(qn) : name(e.getSimpleName()); 337 } 338 339 private static final String name(final QualifiedNameable qn) { 340 final CharSequence n = qn.getQualifiedName(); 341 return n == null || n.isEmpty() ? name(qn.getSimpleName()) : name(n); 342 } 343 344 private static final String name(final CharSequence cs) { 345 return cs instanceof String s ? s : cs.toString(); 346 } 347 348 private static final boolean isInterface(final TypeMirror t) { 349 return t.getKind() == TypeKind.DECLARED && isInterface(((DeclaredType)t).asElement()); 350 } 351 352 private static final boolean isInterface(final Element e) { 353 return e.getKind().isInterface(); 354 } 355 356 private static final <T> boolean returnTrue(final T ignored) { 357 return true; 358 } 359 360 361 /* 362 * Inner and nested classes. 363 */ 364 365 366 private final class SpecializationComparator implements Comparator<TypeMirror> { 367 368 private SpecializationComparator() { 369 super(); 370 } 371 372 @Override 373 public final int compare(final TypeMirror t, final TypeMirror s) { 374 if (t == s) { 375 return 0; 376 } else if (t == null) { 377 return 1; // nulls right 378 } else if (s == null) { 379 return -1; // nulls right 380 } else if (tes.sameType(t, s)) { 381 return 0; 382 } else if (tes.subtype(t, s)) { 383 // t is a subtype of s; s is a proper supertype of t 384 return -1; 385 } else if (tes.subtype(s, t)) { 386 // s is a subtype of t; t is a proper supertype of s 387 return 1; 388 } else { 389 return name(t).compareTo(name(s)); 390 } 391 } 392 393 } 394 395}