001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2024–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.bean; 015 016import java.lang.System.Logger; 017 018import java.util.Collection; 019import java.util.List; 020import java.util.Map; 021import java.util.Objects; 022 023import java.util.concurrent.ConcurrentHashMap; 024 025import javax.lang.model.element.Element; 026import javax.lang.model.element.ExecutableElement; 027import javax.lang.model.element.QualifiedNameable; 028 029import javax.lang.model.type.ArrayType; 030import javax.lang.model.type.DeclaredType; 031import javax.lang.model.type.TypeMirror; 032 033import org.microbean.assign.Types; 034 035import org.microbean.construct.Domain; 036 037import static java.lang.System.Logger.Level.WARNING; 038 039import static javax.lang.model.element.Modifier.FINAL; 040import static javax.lang.model.element.Modifier.PRIVATE; 041import static javax.lang.model.element.Modifier.SEALED; 042import static javax.lang.model.element.Modifier.STATIC; 043 044import static javax.lang.model.type.TypeKind.DECLARED; 045import static javax.lang.model.type.TypeKind.TYPEVAR; 046 047/** 048 * A utility for working with <dfn>bean types</dfn>. 049 * 050 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 051 * 052 * @see #beanTypes(TypeMirror) 053 * 054 * @see #legalBeanType(TypeMirror) 055 */ 056public final class BeanTypes extends Types { 057 058 059 /* 060 * Static fields. 061 */ 062 063 064 private static final Logger LOGGER = System.getLogger(BeanTypes.class.getName()); 065 066 067 /* 068 * Instance fields. 069 */ 070 071 072 // TODO: is this cache actually worth it? 073 private final Map<TypeMirror, BeanTypeList> beanTypesCache; 074 075 076 /* 077 * Constructors. 078 */ 079 080 081 /** 082 * Creates a new {@link BeanTypes}. 083 * 084 * @param domain a {@link Domain}; must not be {@code null} 085 * 086 * @exception NullPointerException if {@code domain} is {@code null} 087 */ 088 public BeanTypes(final Domain domain) { 089 super(domain); 090 this.beanTypesCache = new ConcurrentHashMap<>(); 091 } 092 093 094 /* 095 * Instance methods. 096 */ 097 098 099 /** 100 * Returns a {@link BeanTypeList} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied {@link 101 * TypeMirror} bears. 102 * 103 * <p>The returned {@link BeanTypeList} may be empty.</p> 104 * 105 * @param t a {@link TypeMirror}; must not be {@code null} 106 * 107 * @return a {@link BeanTypeList} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied {@link 108 * TypeMirror} bears; never {@code null} 109 * 110 * @exception NullPointerException if {@code t} is {@code null} 111 * 112 * @microbean.nullability This method never returns {@code null}. 113 * 114 * @microbean.idempotency This method is idempotent and returns determinate values. 115 * 116 * @microbean.threadsafety This method is safe for concurrent use by multiple threads. 117 * 118 * @see #supertypes(TypeMirror, java.util.function.Predicate) 119 */ 120 public final BeanTypeList beanTypes(final TypeMirror t) { 121 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#assignable_parameters 122 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#legal_bean_types 123 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#managed_bean_types 124 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#producer_field_types 125 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#producer_method_types 126 final Domain d = this.domain(); 127 return switch (t.getKind()) { 128 case ARRAY -> 129 this.beanTypesCache.computeIfAbsent(t, t0 -> BeanTypeList.of(d, 130 (legalBeanType(t0) ? 131 List.of(t0, d.javaLangObject().asType()) : 132 List.of()))); 133 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> 134 this.beanTypesCache.computeIfAbsent(t, t0 -> BeanTypeList.of(d, 135 List.of(t0, d.javaLangObject().asType()))); 136 case DECLARED, TYPEVAR -> 137 this.beanTypesCache.computeIfAbsent(t, t0 -> BeanTypeList.of(d, 138 this.supertypes(t0, BeanTypes::legalBeanType))); 139 default -> 140 BeanTypeList.of(d, List.of()); 141 }; 142 } 143 144 /** 145 * Clears caches that may be used internally by this {@link BeanTypes}. 146 * 147 * @microbean.idempotency This method may clear internal state but otherwise has no side effects. 148 * 149 * @microbean.threadsafety This method is safe for concurrent use by multiple threads. 150 */ 151 public final void clearCaches() { 152 this.beanTypesCache.clear(); 153 } 154 155 156 /* 157 * Static methods. 158 */ 159 160 161 /** 162 * Returns {@code true} if and only if the supplied {@link TypeMirror} is a <dfn>legal bean type</dfn> as defined by 163 * the <a href="https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#legal_bean_types">CDI 164 * specification</a>. 165 * 166 * <p>Legal bean types are, exactly:</p> 167 * 168 * <ol> 169 * 170 * <li>{@linkplain javax.lang.model.type.TypeKind#ARRAY Array} types whose {@linkplain ArrayType#getComponentType() 171 * component type}s are legal bean types</li> 172 * 173 * <li>{@linkplain javax.lang.model.type.TypeKind#isPrimitive() Primitive} types</li> 174 * 175 * <li>{@linkplain javax.lang.model.type.TypeKind#DECLARED Declared} types that <dfn>contain</dfn> no {@linkplain 176 * javax.lang.model.type.TypeKind#WILDCARD wildcard type}s for every interpretation and level of containment</li> 177 * 178 * </ol> 179 * 180 * @param t a {@link TypeMirror}; must not be {@code null} 181 * 182 * @return {@code true} if and only if {@code t} is a legal bean type; {@code false} otherwise 183 * 184 * @exception NullPointerException if {@code t} is {@code null} 185 * 186 * @spec https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#legal_bean_types CDI Specification, version 187 * 4.1, section 2.2.1 188 * 189 * @see <a 190 * href="https://issues.redhat.com/browse/CDI-502?focusedId=13036118&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-13036118">CDI-502</a> 191 * 192 * @see <a href="https://github.com/jakartaee/cdi/issues/823">CDI issue 823</a> 193 * 194 * @see <a href="https://issues.redhat.com/browse/WELD-1492">WELD-1492</a> 195 * 196 * @microbean.idempotency This method is idempotent and deterministic. 197 * 198 * @microbean.threadsafety This method itself is safe for concurrent use by multiple threads, but {@link TypeMirror} 199 * implementations and {@link Domain} implementations may not be safe for such use. 200 */ 201 public static final boolean legalBeanType(final TypeMirror t) { 202 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#assignable_parameters 203 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#legal_bean_types 204 return switch (t.getKind()) { 205 206 // "A bean type may be an array type." 207 // 208 // "However, some Java types are not legal bean types: [...] An array type whose component type is not a legal bean 209 // type" 210 case ARRAY -> { 211 if (!legalBeanType(((ArrayType)t).getComponentType())) { // note recursion 212 if (LOGGER.isLoggable(WARNING)) { 213 LOGGER.log(WARNING, t + " has a component type that is an illegal bean type (" + ((ArrayType)t).getComponentType() + ")"); 214 } 215 yield false; 216 } 217 yield true; 218 } 219 220 // "A bean type may be a primitive type. Primitive types are considered to be identical to their corresponding 221 // wrapper types in java.lang." 222 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> true; 223 224 // "A bean type may be a parameterized type with actual [non-wildcard, non-type-variable, see below] type parameters 225 // [arguments] and type variables." (A bean type may be a parameterized type whose type arguments are either array 226 // types, declared types, or type variables.) 227 // 228 // "However, some Java types are not legal bean types: [...] A parameterized type that contains [anywhere, see 229 // below] a wildcard type parameter [argument] is not a legal bean type." 230 // 231 // Some ink has been spilled on what it means for a "parameterized" (generic) type to "contain" a "wildcard type 232 // parameter [argument]" (https://issues.redhat.com/browse/CDI-502). Because it turns out that an "actual type" is a 233 // non-wildcard, non-type-variable type, it follows that *no* wildcard type argument appearing *anywhere* in a bean 234 // type's declaration is permitted. Note that this definition of "actual type" does not appear in the CDI 235 // specification, but only in a (closed) JIRA issue raised against the specification 236 // (https://issues.redhat.com/browse/CDI-502?focusedId=13036118&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-13036118): 237 // "An actual type is a type that is not a wildcard nor [sic] an unresolved [sic] type variable." 238 // 239 // This still seems way overstrict to me but there you have it. 240 case DECLARED -> { 241 for (final TypeMirror ta : ((DeclaredType)t).getTypeArguments()) { 242 if (ta.getKind() != TYPEVAR && !legalBeanType(ta)) { // note recursion 243 if (LOGGER.isLoggable(WARNING)) { 244 LOGGER.log(WARNING, t + " has a type argument that is an illegal bean type (" + ta + ")"); 245 } 246 yield false; 247 } 248 } 249 yield true; 250 } 251 252 // "A type variable is not a legal bean type." (Nothing else is either.) 253 default -> { 254 if (LOGGER.isLoggable(WARNING)) { 255 LOGGER.log(WARNING, t + " is an illegal bean type"); 256 } 257 yield false; 258 } 259 }; 260 } 261 262 /** 263 * Returns {@code true} if and only if the supplied {@link TypeMirror} is a {@linkplain #legalBeanType(TypeMirror) 264 * legal}, {@linkplain javax.lang.model.type.TypeKind#DECLARED declared}, <dfn>proxiable bean type</dfn> as defined by 265 * the <a href="https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unproxyable">CDI specification</a>. 266 * 267 * @param t a {@link TypeMirror}; must not be {@code null} 268 * 269 * @return {@code true} if and only if the supplied {@link TypeMirror} is a <dfn>proxiable bean type</dfn> 270 * 271 * @exception NullPointerException if {@code t} is {@code null} 272 * 273 * @see #proxiableElement(Element) 274 * 275 * @spec https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unproxyable CDI Specification, version 4.1, 276 * section 3.10 277 * 278 * @microbean.idempotency This method is idempotent and deterministic. 279 * 280 * @microbean.threadsafety This method itself is safe for concurrent use by multiple threads, but {@link TypeMirror} 281 * implementations and {@link Domain} implementations may not be safe for such use. 282 */ 283 public static final boolean proxiableBeanType(final TypeMirror t) { 284 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unproxyable 285 return 286 t.getKind() == DECLARED && 287 legalBeanType(t) && 288 proxiableElement(((DeclaredType)t).asElement()); 289 } 290 291 /** 292 * Returns {@code true} if and only if the supplied {@link Element} is an {@linkplain ElementKind#INTERFACE 293 * interface}, or a <dfn>proxiable</dfn> {@linkplain ElementKind#CLASS class}, as defined by the <a 294 * href="https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unproxyable">CDI specification</a>. 295 * 296 * @param e an {@link Element}; must not be {@code null} 297 * 298 * @return {@code true} if and only if the supplied {@link Element} is <dfn>proxiable</fn> 299 * 300 * @excepiton NullPointerException if {@code e} is {@code null} 301 * 302 * @spec https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1unproxyable CDI Specification, version 4.1, 303 * section 3.10 304 * 305 * @microbean.idempotency This method is idempotent and deterministic. 306 * 307 * @microbean.threadsafety This method itself is safe for concurrent use by multiple threads, but {@link TypeMirror} 308 * implementations and {@link Domain} implementations may not be safe for such use. 309 */ 310 static final boolean proxiableElement(final Element e) { 311 // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unproxyable 312 switch (e.getKind()) { 313 case CLASS: 314 if (e.getModifiers().contains(FINAL) || 315 e.getModifiers().contains(SEALED) || 316 ((QualifiedNameable)e).getQualifiedName().contentEquals("java.lang.Object")) { // cheap optimization for a common case 317 return false; 318 } 319 boolean hasNonPrivateZeroArgumentConstructor = false; 320 for (final Element ee : e.getEnclosedElements()) { 321 switch (ee.getKind()) { 322 case CONSTRUCTOR: 323 if (!hasNonPrivateZeroArgumentConstructor && 324 !ee.getModifiers().contains(PRIVATE) && 325 ((ExecutableElement)ee).getParameters().isEmpty()) { 326 hasNonPrivateZeroArgumentConstructor = true; 327 } 328 break; 329 case METHOD: 330 final Collection<?> modifiers = ((ExecutableElement)ee).getModifiers(); 331 if (modifiers.contains(FINAL) && 332 !modifiers.contains(STATIC) && 333 !modifiers.contains(PRIVATE)) { 334 return false; 335 } 336 break; 337 } 338 } 339 return hasNonPrivateZeroArgumentConstructor; 340 case INTERFACE: 341 return true; 342 default: 343 return false; 344 } 345 } 346 347}