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.List; 019import java.util.Map; 020import java.util.Objects; 021import java.util.Set; 022 023import java.util.concurrent.ConcurrentHashMap; 024 025import java.util.function.Predicate; 026 027import javax.lang.model.type.ArrayType; 028import javax.lang.model.type.DeclaredType; 029import javax.lang.model.type.TypeKind; 030import javax.lang.model.type.TypeMirror; 031 032import org.microbean.assign.Types; 033 034import org.microbean.construct.Domain; 035 036import static java.lang.System.Logger.Level.WARNING; 037 038/** 039 * A utility for working with <dfn>bean types</dfn>. 040 * 041 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 042 * 043 * @see #beanTypes(TypeMirror) 044 * 045 * @see #legalBeanType(TypeMirror) 046 */ 047public final class BeanTypes extends Types { 048 049 050 /* 051 * Static fields. 052 */ 053 054 055 private static final Logger LOGGER = System.getLogger(BeanTypes.class.getName()); 056 057 058 /* 059 * Instance fields. 060 */ 061 062 063 private final Map<TypeMirror, List<? extends TypeMirror>> beanTypesCache; 064 065 066 /* 067 * Constructors. 068 */ 069 070 071 /** 072 * Creates a new {@link BeanTypes}. 073 * 074 * @param domain a {@link Domain}; must not be {@code null} 075 * 076 * @exception NullPointerException if {@code domain} is {@code null} 077 */ 078 public BeanTypes(final Domain domain) { 079 super(domain); 080 this.beanTypesCache = new ConcurrentHashMap<>(); 081 } 082 083 084 /* 085 * Instance methods. 086 */ 087 088 089 /** 090 * Returns an immutable {@link List} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied 091 * {@link TypeMirror} bears. 092 * 093 * <p>The returned {@link List} may be empty.</p> 094 * 095 * @param t a {@link TypeMirror}; must not be {@code null} 096 * 097 * @return an immutable {@link List} of {@linkplain #legalBeanType(TypeMirror) legal bean types} that the supplied 098 * {@link TypeMirror} bears; never {@code null} 099 * 100 * @exception NullPointerException if {@code t} is {@code null} 101 * 102 * @microbean.nullability This method never returns {@code null}. 103 * 104 * @microbean.idempotency This method is idempotent and returns determinate values. 105 * 106 * @microbean.threadsafety This method is safe for concurrent use by multiple threads. 107 */ 108 public final List<? extends TypeMirror> beanTypes(final TypeMirror t) { 109 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#assignable_parameters 110 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#legal_bean_types 111 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#managed_bean_types 112 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#producer_field_types 113 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#producer_method_types 114 return switch (t.getKind()) { 115 case ARRAY -> this.beanTypesCache.computeIfAbsent(t, t0 -> legalBeanType(t0) ? List.of(t0, this.domain().javaLangObject().asType()) : List.of()); 116 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> this.beanTypesCache.computeIfAbsent(t, t0 -> List.of(t0, this.domain().javaLangObject().asType())); 117 case DECLARED, TYPEVAR -> this.beanTypesCache.computeIfAbsent(t, t0 -> this.supertypes(t0, BeanTypes::legalBeanType)); 118 default -> { 119 assert !legalBeanType(t); 120 yield List.of(); 121 } 122 }; 123 } 124 125 /** 126 * Clears caches that may be used internally by this {@link BeanTypes}. 127 * 128 * @microbean.idempotency This method may clear internal state but otherwise has no side effects. 129 * 130 * @microbean.threadsafety This method is safe for concurrent use by multiple threads. 131 */ 132 public final void clearCaches() { 133 this.beanTypesCache.clear(); 134 } 135 136 137 /* 138 * Static methods. 139 */ 140 141 142 /** 143 * Returns {@code true} if and only if the supplied {@link TypeMirror} is a <dfn>legal bean type</dfn>. 144 * 145 * <p>Legal bean types are, exactly:</p> 146 * 147 * <ol> 148 * 149 * <li>{@linkplain TypeKind#ARRAY Array} types whose {@linkplain ArrayType#getComponentType() component type}s are 150 * legal bean types</li> 151 * 152 * <li>{@linkplain TypeKind#isPrimitive() Primitive} types</li> 153 * 154 * <li>{@linkplain TypeKind#DECLARED Declared} types that contain no {@linkplain TypeKind#WILDCARD wildcard type}s for 155 * every level of containment</li> 156 * 157 * </ol> 158 * 159 * @param t a {@link TypeMirror}; must not be {@code null} 160 * 161 * @return {@code true} if and only if {@code t} is a legal bean type; {@code false} otherwise 162 * 163 * @exception NullPointerException if {@code t} is {@code null} 164 * 165 * @microbean.idempotency This method is idempotent and deterministic. 166 * 167 * @microbean.threadsafety This method itself is safe for concurrent use by multiple threads, but {@link TypeMirror} 168 * implementations and {@link Domain} implementations may not be safe for such use. 169 */ 170 public static final boolean legalBeanType(final TypeMirror t) { 171 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#assignable_parameters 172 // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#legal_bean_types 173 return switch (t.getKind()) { 174 175 // "A bean type may be an array type." 176 // 177 // "However, some Java types are not legal bean types: [...] An array type whose component type is not a legal bean 178 // type" 179 case ARRAY -> { 180 if (!legalBeanType(((ArrayType)t).getComponentType())) { // note recursion 181 if (LOGGER.isLoggable(WARNING)) { 182 LOGGER.log(WARNING, t + " has a component type that is an illegal bean type (" + ((ArrayType)t).getComponentType() + ")"); 183 } 184 yield false; 185 } 186 yield true; 187 } 188 189 // "A bean type may be a primitive type. Primitive types are considered to be identical to their corresponding 190 // wrapper types in java.lang." 191 case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> true; 192 193 // "A bean type may be a parameterized type with actual [see below] type parameters [arguments] and type variables." 194 // 195 // "However, some Java types are not legal bean types: [...] A parameterized type that contains [see below] a 196 // wildcard type parameter [argument] is not a legal bean type." 197 // 198 // Some ink has been spilled on what it means for a "parameterized" (generic) type to "contain" a "wildcard type 199 // parameter [argument]" (https://issues.redhat.com/browse/CDI-502). Because it turns out that "actual type" 200 // apparently means, among other things, a non-wildcard type, it follows that *no* wildcard type argument appearing 201 // *anywhere* in a bean type is permitted. Note that the definition of "actual type" does not appear in the CDI 202 // specification, but only in a closed JIRA issue 203 // (https://issues.redhat.com/browse/CDI-502?focusedId=13036118&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-13036118). 204 // 205 // This still seems way overstrict to me but there you have it. 206 case DECLARED -> { 207 for (final TypeMirror ta : ((DeclaredType)t).getTypeArguments()) { 208 if (ta.getKind() != TypeKind.TYPEVAR && !legalBeanType(ta)) { // note recursion 209 if (LOGGER.isLoggable(WARNING)) { 210 LOGGER.log(WARNING, t + " has a type argument that is an illegal bean type (" + ta + ")"); 211 } 212 yield false; 213 } 214 } 215 yield true; 216 } 217 218 // "A type variable is not a legal bean type." (Nothing else is either.) 219 default -> { 220 if (LOGGER.isLoggable(WARNING)) { 221 LOGGER.log(WARNING, t + " is an illegal bean type"); 222 } 223 yield false; 224 } 225 }; 226 } 227 228}