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}