001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017-2018 microBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.cdi;
018
019import java.lang.annotation.Annotation;
020
021import java.lang.reflect.AnnotatedElement;
022
023import java.util.Arrays;
024import java.util.ArrayDeque;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Deque;
028import java.util.HashSet;
029import java.util.LinkedHashSet;
030import java.util.Objects;
031import java.util.Set;
032
033import javax.enterprise.inject.spi.Annotated;
034import javax.enterprise.inject.spi.BeanManager;
035
036/**
037 * A utility class housing methods that do helpful things with {@link
038 * Annotation}s, {@link Annotated}s and {@link AnnotatedElement}s.
039 *
040 * @author <a href="https://about.me/lairdnelson"
041 * target="_parent">Laird Nelson</a>
042 */
043public final class Annotations {
044
045
046  /*
047   * Constructors.
048   */
049
050  
051  /**
052   * Creates a new {@link Annotations}.
053   */
054  private Annotations() {
055    super();
056  }
057
058
059  /*
060   * Static methods.
061   */
062  
063
064  /**
065   * Returns a {@link Set} of {@link Annotation}s that are
066   * <em>ultimately qualified</em> with an annotation whose
067   * {@linkplain Annotation#annotationType() annotation type} is equal
068   * to the supplied {@code metaAnnotationType}.
069   *
070   * <p>This method never returns {@code null}.</p>
071   *
072   * @param host the {@link Annotated} whose {@linkplain
073   * Annotated#getAnnotations() annotations} will be used as the
074   * initial set; must not be {@code null}
075   *
076   * @param metaAnnotationType the qualifier {@linkplain
077   * Annotation#annotationType() annotation type} to look for; must
078   * not be {@code null}
079   *
080   * @param beanManager a {@link BeanManager} for retrieving the
081   * appropriate {@linkplain BeanManager#createAnnotatedType(Class)
082   * annotated type} when appropriate; may be {@code null}
083   *
084   * @return a non-{@code null} subset of {@link Annotation}s
085   *
086   * @exception NullPointerException if {@code host} or {@code
087   * metaAnnotationType} is {@code null}
088   *
089   * @see #retainAnnotationsQualifiedWith(Collection, Class,
090   * BeanManager)
091   */
092  public static final Set<Annotation> getAnnotationsQualifiedWith(final Annotated host, final Class<? extends Annotation> metaAnnotationType, final BeanManager beanManager) {
093    return retainAnnotationsQualifiedWith(host.getAnnotations(), Objects.requireNonNull(metaAnnotationType), beanManager);
094  }
095
096  /**
097   * Returns a {@link Set} of {@link Annotation}s that are
098   * <em>ultimately qualified</em> with an annotation whose
099   * {@linkplain Annotation#annotationType() annotation type} is equal
100   * to the supplied {@code metaAnnotationType}.
101   *
102   * <p>This method never returns {@code null}.</p>
103   *
104   * @param host the {@link AnnotatedElement} whose {@linkplain
105   * Annotated#getAnnotations() annotations} will be used as the
106   * initial set; must not be {@code null}
107   *
108   * @param metaAnnotationType the qualifier {@linkplain
109   * Annotation#annotationType() annotation type} to look for; must
110   * not be {@code null}
111   *
112   * @param beanManager a {@link BeanManager} for retrieving the
113   * appropriate {@linkplain BeanManager#createAnnotatedType(Class)
114   * annotated type} when appropriate; may be {@code null}
115   *
116   * @return a non-{@code null} subset of {@link Annotation}s
117   *
118   * @exception NullPointerException if {@code host} or {@code
119   * metaAnnotationType} is {@code null}
120   *
121   * @see #retainAnnotationsQualifiedWith(Collection, Class,
122   * BeanManager)
123   */
124  public static final Set<Annotation> getAnnotationsQualifiedWith(final AnnotatedElement host, final Class<? extends Annotation> metaAnnotationType, final BeanManager beanManager) {
125    return retainAnnotationsQualifiedWith(Arrays.asList(host.getAnnotations()), Objects.requireNonNull(metaAnnotationType), beanManager);
126  }
127
128  /**
129   * Given a {@link Collection} of {@link Annotation}s, returns a
130   * subset of them that are found to be <em>ultimately qualified</em>
131   * with an annotation whose {@linkplain Annotation#annotationType()
132   * annotation type} is equal to the supplied {@code
133   * metaAnnotationType}.
134   *
135   * <p>This method never returns {@code null}.</p>
136   *
137   * @param suppliedAnnotations a {@link Collection} of {@link
138   * Annotation}s that will be used as the initial set; may be {@code
139   * null}
140   *
141   * @param metaAnnotationType the qualifier {@linkplain
142   * Annotation#annotationType() annotation type} to look for; must
143   * not be {@code null}
144   *
145   * @param beanManager a {@link BeanManager} for retrieving the
146   * appropriate {@linkplain BeanManager#createAnnotatedType(Class)
147   * annotated type} when appropriate; may be {@code null}
148   *
149   * @return a non-{@code null} subset of {@link Annotation}s
150   *
151   * @exception NullPointerException if {@code metaAnnotationType} is
152   * {@code null}
153   */
154  public static final Set<Annotation> retainAnnotationsQualifiedWith(final Collection<? extends Annotation> suppliedAnnotations, final Class<? extends Annotation> metaAnnotationType, final BeanManager beanManager) {
155    Objects.requireNonNull(metaAnnotationType);
156    final Set<Annotation> results = new LinkedHashSet<>();
157    if (suppliedAnnotations != null) {
158      for (final Annotation annotation : suppliedAnnotations) {
159        if (annotation != null && isAnnotationQualifiedWith(annotation, metaAnnotationType, beanManager)) {
160          results.add(annotation);
161        }
162      }
163    }
164
165    return results;
166  }
167
168  /**
169   * Returns a {@link Collection} of {@link Annotation}s that are
170   * <em>{@linkplain AnnotatedElement present}</em> on the supplied
171   * {@link Class}, using the {@link
172   * BeanManager#createAnnotatedType(Class)} method as appropriate.
173   *
174   * <p>This method never returns {@code null}.</p>
175   *
176   * @param c the {@link Class} whose {@link Annotation}s should be
177   * returned; must not be {@code null}
178   *
179   * @param beanManager a {@link BeanManager} whose {@link
180   * BeanManager#createAnnotatedType(Class)} method will be called;
181   * may be {@code null} in which case {@link
182   * AnnotatedElement#getAnnotations()} will be called instead
183   *
184   * @return a non-{@code null} {@link Collection} of {@link
185   * Annotation}s
186   *
187   * @see BeanManager#createAnnotatedType(Class)
188   *
189   * @see AnnotatedElement#getAnnotations()
190   */
191  public static final Collection<? extends Annotation> getAnnotations(final Class<?> c, final BeanManager beanManager) {
192    Objects.requireNonNull(c);
193    final Collection<? extends Annotation> returnValue;
194    if (beanManager != null) {
195      final Annotated annotated = beanManager.createAnnotatedType(c);
196      assert annotated != null;
197      returnValue = annotated.getAnnotations();
198    } else {
199      returnValue = Arrays.asList(c.getAnnotations());
200    }
201    return returnValue;
202  }
203
204  /**
205   * Returns {@code true} if the supplied {@link Annotation} is
206   * <em>ultimately qualified</em> by an annotation {@linkplain
207   * Annotation#annotationType() with the supplied
208   * <code>qualifierType</code> as its <code>annotationType</code>}.
209   *
210   * <p><em>Ultimately qualified</em> means that either the qualifier
211   * annotation represented by the supplied {@code qualifierType} is
212   * {@linkplain AnnotatedElement <em>present</em>} on the supplied
213   * {@code annotation} or one of its {@link Annotation}s, or
214   * {@linkplain AnnotatedElement <em>present</em>} on one of
215   * <em>those</em> {@link Annotation}s, and so on.</p>
216   *
217   * @param annotation the {@link Annotation} to check; must not be
218   * {@code null}
219   *
220   * @param qualifierType the {@link Class} representing the {@link
221   * Annotation#annotationType() annotation type} to look for; must
222   * not be {@code null}
223   *
224   * @param beanManager a {@link BeanManager} whose {@link
225   * BeanManager#createAnnotatedType(Class)} method will be called;
226   * may be {@code null}
227   *
228   * @return {@code true} if the supplied {@link Annotation} is
229   * <em>ultimately qualified</em> by an annotation {@linkplain
230   * Annotation#annotationType() with the supplied
231   * <code>qualifierType</code> as its <code>annotationType</code>};
232   * {@code false} otherwise
233   *
234   * @exception NullPointerException if {@code annotation} or {@code
235   * qualifierType} is {@code null}
236   */
237  public static final boolean isAnnotationQualifiedWith(final Annotation annotation, final Class<? extends Annotation> qualifierType, final BeanManager beanManager) {
238    return isAnnotationQualifiedWith(annotation, qualifierType, beanManager, new HashSet<>());
239  }
240
241  private static final boolean isAnnotationQualifiedWith(final Annotation annotation, final Class<? extends Annotation> qualifierType, final BeanManager beanManager, Set<Annotation> seen) {
242    Objects.requireNonNull(annotation);
243    Objects.requireNonNull(qualifierType);
244    if (seen == null) {
245      seen = new HashSet<>();
246    }
247    boolean returnValue = false;
248    if (!isBlacklisted(annotation) && !seen.contains(annotation)) {
249      seen.add(annotation);
250      final Class<? extends Annotation> annotationType = annotation.annotationType();
251      assert annotationType != null;
252      if (annotationType.equals(qualifierType)) {
253        returnValue = true;
254      } else {
255        final Collection<? extends Annotation> metaAnnotations = getAnnotations(annotationType, beanManager);
256        if (metaAnnotations != null && !metaAnnotations.isEmpty()) {
257          for (final Annotation metaAnnotation : metaAnnotations) {
258            if (metaAnnotation != null && isAnnotationQualifiedWith(metaAnnotation, qualifierType, beanManager, seen)) { // RECURSION
259              returnValue = true;
260              break;
261            }
262          }
263        }
264      }
265    }
266    return returnValue;
267  }
268
269  private static final boolean isBlacklisted(final Annotation annotation) {
270    return annotation == null;
271  }
272
273}