001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025–2026 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.construct.element;
015
016import java.lang.constant.ClassDesc;
017import java.lang.constant.Constable;
018import java.lang.constant.ConstantDesc;
019import java.lang.constant.DynamicConstantDesc;
020
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Optional;
027
028import javax.lang.model.element.AnnotationMirror;
029import javax.lang.model.element.AnnotationValue;
030import javax.lang.model.element.Element;
031import javax.lang.model.element.ExecutableElement;
032import javax.lang.model.element.QualifiedNameable;
033import javax.lang.model.element.TypeElement;
034
035import javax.lang.model.type.DeclaredType;
036
037import org.microbean.construct.constant.Constables;
038
039import static java.lang.constant.ConstantDescs.BSM_INVOKE;
040import static java.lang.constant.ConstantDescs.CD_Map;
041
042import static java.lang.constant.MethodHandleDesc.ofConstructor;
043
044import static java.util.Collections.unmodifiableMap;
045
046import static java.util.HashMap.newHashMap;
047
048import static java.util.LinkedHashMap.newLinkedHashMap;
049
050import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE;
051
052import static javax.lang.model.util.ElementFilter.methodsIn;
053
054/**
055 * An <strong>experimental</strong> {@link AnnotationMirror} implementation that is partially or wholly synthetic.
056 *
057 * <p>It is possible to create {@link SyntheticAnnotationMirror} instances representing annotations that a Java compiler
058 * will never produce. For example, <a
059 * href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1">annotations cannot refer to each
060 * other, directly or indirectly</a>, but two {@link SyntheticAnnotationMirror}s may do so.</p>
061 *
062 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
063 *
064 * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, section
065 * 9.6.1
066 */
067public final class SyntheticAnnotationMirror implements AnnotationMirror, Constable {
068
069
070  /*
071   * Instance fields.
072   */
073
074
075  private final TypeElement annotationTypeElement;
076
077  private final Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues;
078
079
080  /*
081   * Constructors.
082   */
083
084
085  /**
086   * Creates a new {@link SyntheticAnnotationMirror}.
087   *
088   * @param annotationTypeElement a {@link TypeElement} representing an annotation type; must not be {@code null}; must
089   * return {@link javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from its {@link
090   * Element#getKind() getKind()} method; {@link SyntheticAnnotationTypeElement} implementations are strongly preferred
091   *
092   * @exception NullPointerException if {@code annotationTypeElement} is {@code null}
093   *
094   * @exception IllegalArgumentException if {@code annotationTypeElement} does not return {@link
095   * javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from an invocation of its {@link
096   * Element#getKind() getKind()} method, or if {@code values} has more entries in it than {@code annotationTypeElement}
097   * has {@linkplain Element#getEnclosedElements() anotation elements}
098   *
099   * @see #SyntheticAnnotationMirror(TypeElement, Map)
100   */
101  public SyntheticAnnotationMirror(final TypeElement annotationTypeElement) {
102    this(annotationTypeElement, Map.of());
103  }
104
105  /**
106   * Creates a new {@link SyntheticAnnotationMirror}.
107   *
108   * @param annotationTypeElement a {@link TypeElement} representing an annotation type; must not be {@code null}; must
109   * return {@link javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from its {@link
110   * Element#getKind() getKind()} method; {@link SyntheticAnnotationTypeElement} implementations are strongly preferred
111   *
112   * @param values a {@link Map} of annotation values indexed by annotation element name; must not be {@code null}; must
113   * contain only values that are permissible for annotation elements
114   *
115   * @exception NullPointerException if any argument is {@code null}
116   *
117   * @exception IllegalArgumentException if {@code annotationTypeElement} does not return {@link
118   * javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from an invocation of its {@link
119   * Element#getKind() getKind()} method, or if {@code values} has more entries in it than {@code annotationTypeElement}
120   * has {@linkplain Element#getEnclosedElements() anotation elements}
121   */
122  public SyntheticAnnotationMirror(final TypeElement annotationTypeElement,
123                                   final Map<? extends String, ?> values) {
124    super();
125    if (annotationTypeElement.getKind() != ANNOTATION_TYPE) {
126      throw new IllegalArgumentException("annotationTypeElement: " + annotationTypeElement);
127    }
128    this.annotationTypeElement = annotationTypeElement;
129    final LinkedHashMap<ExecutableElement, AnnotationValue> m = newLinkedHashMap(values.size());
130    final List<? extends ExecutableElement> methods = methodsIn(annotationTypeElement.getEnclosedElements());
131    for (final ExecutableElement e : methods) {
132      final Object value = values.get(e.getSimpleName().toString()); // default value deliberately not included
133      if (value == null) {
134        if (e.getDefaultValue() == null) {
135          // There has to be a value somewhere, or annotationTypeElement or values is illegal
136          throw new IllegalArgumentException("annotationTypeElement: " + annotationTypeElement + "; values: " + values);
137        }
138        // Default values are excluded from the map on purpose, following the contract of
139        // AnnotationValue#getElementValues().
140      } else {
141        m.put(e, value instanceof AnnotationValue av ? av : new SyntheticAnnotationValue(value));
142      }
143    }
144    if (values.size() > m.size()) {
145      throw new IllegalArgumentException("values: " + values);
146    }
147    this.elementValues = m.isEmpty() ? Map.of() : unmodifiableMap(m);
148  }
149
150  /**
151   * Creates a new {@link SyntheticAnnotationMirror} that is an effective <dfn>copy</dfn> of the supplied {@link
152   * AnnotationMirror}.
153   *
154   * @param a a non-{@code null} {@link AnnotationMirror} to semantically copy
155   *
156   * @exception NullPointerException if {@code a} is {@code null}
157   */
158  // (Copy constructor.)
159  public SyntheticAnnotationMirror(final AnnotationMirror a) {
160    super();
161    this.annotationTypeElement = new SyntheticAnnotationTypeElement((TypeElement)a.getAnnotationType().asElement());
162    final Map<? extends ExecutableElement, ? extends AnnotationValue> originalElementValues = a.getElementValues();
163    if (originalElementValues.isEmpty()) {
164      // If there are no explicit values, then...there are no explicit values whether the annotation interface type
165      // contains/encloses any elements at all.
166      this.elementValues = Map.of();
167    } else {
168      // There are explicit values. That also means that the annotation interface type contains/encloses at least one
169      // element.
170      final List<ExecutableElement> syntheticElements = methodsIn(this.annotationTypeElement.getEnclosedElements());
171      assert !syntheticElements.isEmpty();
172      final Map<ExecutableElement, AnnotationValue> newElementValues = newLinkedHashMap(originalElementValues.size());
173      for (final Entry<? extends ExecutableElement, ? extends AnnotationValue> originalEntry : originalElementValues.entrySet()) {
174        final ExecutableElement originalElement = originalEntry.getKey();
175        final ExecutableElement syntheticElement = element(syntheticElements, originalElement.getSimpleName());
176        if (syntheticElement != null) {
177          newElementValues.put(syntheticElement, originalEntry.getValue());
178        }
179      }
180      this.elementValues = unmodifiableMap(newElementValues);
181    }
182  }
183
184
185  /*
186   * Instance methods.
187   */
188
189
190  @Override // Constable
191  public final Optional<DynamicConstantDesc<SyntheticAnnotationMirror>> describeConstable() {
192    return (this.annotationTypeElement instanceof Constable c ? c.describeConstable() : Optional.<ConstantDesc>empty())
193      .flatMap(elementDesc -> Constables.describe(this.toSyntheticValues(),
194                                                  String::describeConstable,
195                                                  SyntheticAnnotationMirror::describeAnnotationValue)
196               .map(valuesDesc -> DynamicConstantDesc.ofNamed(BSM_INVOKE,
197                                                              this.getAnnotationType().asElement().getSimpleName().toString(), // supposed to be unqualified, I guess
198                                                              this.getClass().describeConstable().orElseThrow(),
199                                                              ofConstructor(this.getClass().describeConstable().orElseThrow(),
200                                                                            TypeElement.class.describeConstable().orElseThrow(),
201                                                                            CD_Map),
202                                                              elementDesc,
203                                                              valuesDesc)));
204  }
205
206  @Override // AnnotationMirror
207  public final DeclaredType getAnnotationType() {
208    return (DeclaredType)this.annotationTypeElement.asType();
209  }
210
211  @Override // AnnotationMirror
212  public final Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues() {
213    return this.elementValues;
214  }
215
216  @Override
217  public final String toString() {
218    return "@" + this.annotationTypeElement.toString(); // TODO: not anywhere near good enough
219  }
220
221  // Called by describeConstable().
222  private final Map<? extends String, ?> toSyntheticValues() {
223    if (this.elementValues.isEmpty()) {
224      return Map.of();
225    }
226    final Map<String, Object> rv = newHashMap(this.elementValues.size());
227    for (final Entry<? extends ExecutableElement, ? extends AnnotationValue> e : this.elementValues.entrySet()) {
228      rv.put(e.getKey().getSimpleName().toString(), e.getValue().getValue());
229    }
230    return rv;
231  }
232
233
234  /*
235   * Static methods.
236   */
237
238
239  // Called by describeConstable().
240  private static final Optional<? extends ConstantDesc> describeAnnotationValue(final Object v) {
241    return switch (v) {
242    case null -> Optional.empty(); // deliberately not Optional.of(NULL); annotation values cannot be null
243    case Constable c -> c.describeConstable();
244    case ConstantDesc cd -> Optional.of(cd);
245    case List<?> l -> Constables.describe(l, e -> e instanceof Constable c ? c.describeConstable() : Optional.empty());
246    default -> Optional.empty();
247    };
248  }
249
250  private static final <E extends Element> E element(final Iterable<? extends E> elements, final CharSequence simpleName) {
251    for (final E e : elements) {
252      if (e.getSimpleName().contentEquals(simpleName)) {
253        return e;
254      }
255    }
256    return null;
257  }
258
259}