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}