001package io.ebean.util;
002
003import io.ebean.annotation.Formula;
004import io.ebean.annotation.Platform;
005import io.ebean.annotation.Where;
006
007import java.lang.annotation.Annotation;
008import java.lang.reflect.AnnotatedElement;
009import java.lang.reflect.Method;
010import java.util.HashSet;
011import java.util.LinkedHashSet;
012import java.util.Set;
013import java.util.concurrent.ConcurrentHashMap;
014import java.util.concurrent.ConcurrentMap;
015
016/**
017 * Annotation utility methods to find annotations recursively - taken from the spring framework.
018 */
019public class AnnotationUtil {
020
021  /**
022   * Determine if the supplied {@link Annotation} is defined in the core JDK {@code java.lang.annotation} package.
023   */
024  public static boolean isInJavaLangAnnotationPackage(Annotation annotation) {
025    return annotation.annotationType().getName().startsWith("java.lang.annotation");
026  }
027
028  /**
029   * Find a single {@link Annotation} of {@code annotationType} on the supplied {@link AnnotatedElement}.
030   * <p>
031   * Meta-annotations will be searched if the annotation is not <em>directly present</em> on the supplied element.
032   * <p>
033   * <strong>Warning</strong>: this method operates generically on annotated elements. In other words, this method
034   * does not execute specialized search algorithms for classes or methods. It only traverses through Annotations!
035   * It also does not filter out platform dependent annotations!
036   */
037  @SuppressWarnings("unchecked")
038  public static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
039    if (annotationType == null) {
040      return null;
041    }
042    // check if directly present, if not, start search for meta-annotations.
043    Annotation[] anns = annotatedElement.getAnnotations();
044    if (anns.length == 0) {
045      return null; // no annotations present, so searching for meta annotations not required
046    }
047
048    // As we need the anns array anyway, we iterate over this instead
049    // of using annotatedElement.getAnnotation(...) which is synchronized internally
050    for (Annotation ann : anns) {
051      if (ann.annotationType() == annotationType) {
052        return (A) ann;
053      }
054    }
055
056    return findAnnotation(anns, annotationType, new HashSet<>());
057
058  }
059
060  /**
061   * Find a single {@link Annotation} of {@code annotationType} on the supplied class.
062   * <p>Meta-annotations will be searched if the annotation is not directly present on
063   * the supplied element.
064   * <p><strong>Note</strong>: this method searches for annotations at class & superClass(es)!
065   */
066  @SuppressWarnings("unchecked")
067  public static <A extends Annotation> A findAnnotationRecursive(Class<?> clazz, Class<A> annotationType) {
068    if (annotationType == null) {
069      return null;
070    }
071
072    while (clazz != null && clazz != Object.class) {
073      // check if directly present, if not, start search for meta-annotations.
074      Annotation[] anns = clazz.getAnnotations();
075      if (anns.length != 0) {
076        for (Annotation ann : anns) {
077          if (ann.annotationType() == annotationType) {
078            return (A) ann;
079          }
080        }
081
082        A ann = findAnnotation(anns, annotationType, new HashSet<>());
083        if (ann != null) {
084          return ann;
085        }
086      }
087      // no meta-annotation present at this class - traverse to superclass
088      clazz = clazz.getSuperclass();
089    }
090    return null;
091
092  }
093
094  /**
095   * Finds the first annotation of a type for this platform. (if annotation is platform specific, otherwise first
096   * found annotation is returned)
097   */
098  public static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType, Platform platform) {
099    if (annotationType == null) {
100      return null;
101    }
102    Set<A> anns = findAnnotations(annotatedElement, annotationType);
103    return getPlatformMatchingAnnotation(anns, platform);
104  }
105
106  /**
107   * Finds all annotations recusively for a class and its superclasses or interfaces.
108   */
109  public static <A extends Annotation> Set<A> findAnnotationsRecursive(Class<?> clazz, Class<A> annotationType) {
110    if (annotationType == null) {
111      return null;
112    }
113    Set<A> ret = new LinkedHashSet<>();
114    Set<Annotation> visited = new HashSet<>();
115    Set<Class<?>> visitedInterfaces = new HashSet<>();
116    while (clazz != null && !clazz.getName().startsWith("java.lang.")) {
117      findMetaAnnotationsRecursive(clazz, annotationType, ret, visited, visitedInterfaces);
118      clazz = clazz.getSuperclass();
119    }
120    return ret;
121  }
122
123  /**
124   * Searches the interfaces for annotations.
125   */
126  private static <A extends Annotation> void findMetaAnnotationsRecursive(Class<?> clazz,
127      Class<A> annotationType, Set<A> ret,
128      Set<Annotation> visited, Set<Class<?>> visitedInterfaces) {
129    findMetaAnnotations(clazz, annotationType, ret, visited);
130    for (Class<?> iface : clazz.getInterfaces()) {
131      if (!iface.getName().startsWith("java.lang.") && visitedInterfaces.add(iface)) {
132        findMetaAnnotationsRecursive(iface, annotationType, ret, visited, visitedInterfaces);
133      }
134    }
135  }
136
137  /**
138   * Perform the search algorithm avoiding endless recursion by tracking which
139   * annotations have already been visited.
140   */
141  @SuppressWarnings("unchecked")
142  private static <A extends Annotation> A findAnnotation(Annotation[] anns, Class<A> annotationType, Set<Annotation> visited) {
143
144
145    for (Annotation ann : anns) {
146      if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
147        Annotation[] metaAnns = ann.annotationType().getAnnotations();
148        for (Annotation metaAnn : metaAnns) {
149          if (metaAnn.annotationType() == annotationType) {
150            return (A) metaAnn;
151          }
152        }
153        if (metaAnns.length > 0) {
154          A annotation = findAnnotation(metaAnns, annotationType, visited);
155          if (annotation != null) {
156            return annotation;
157          }
158        }
159      }
160    }
161    return null;
162  }
163
164  /**
165   * Find all {@link Annotation}s of {@code annotationType} on the supplied {@link AnnotatedElement}.
166   * <p>
167   * Meta-annotations will be searched if the annotation is not <em>directly present</em> on the supplied element.
168   * <p>
169   * <strong>Warning</strong>: this method operates generically on annotated elements. In other words, this method
170   * does not execute specialized search algorithms for classes or methods. It only traverses through Annotations!
171   */
172  public static <A extends Annotation> Set<A> findAnnotations(AnnotatedElement annotatedElement, Class<A> annotationType) {
173    if (annotationType == null) {
174      return null;
175    }
176    Set<A> ret = new LinkedHashSet<>();
177    findMetaAnnotations(annotatedElement, annotationType, ret, new HashSet<>());
178    return ret;
179  }
180
181  /**
182   * Perform the search algorithm avoiding endless recursion by tracking which
183   * annotations have already been visited.
184   */
185  @SuppressWarnings("unchecked")
186  private static <A extends Annotation> void findMetaAnnotations(AnnotatedElement annotatedElement, Class<A> annotationType, Set<A> ret, Set<Annotation> visited) {
187
188    Annotation[] anns = annotatedElement.getAnnotations();
189    for (Annotation ann : anns) {
190      if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
191        if (ann.annotationType() == annotationType) {
192          ret.add((A) ann);
193        } else {
194          Method repeatableValueMethod = getRepeatableValueMethod(ann, annotationType);
195          if (repeatableValueMethod != null) {
196            try {
197              A[] repeatedAnns = (A[]) repeatableValueMethod.invoke(ann);
198              for (Annotation repeatedAnn : repeatedAnns) {
199                ret.add((A) repeatedAnn);
200                findMetaAnnotations(repeatedAnn.annotationType(), annotationType, ret, visited);
201              }
202            } catch (Exception e) { // catch all exceptions (thrown by invoke)
203              throw new RuntimeException(e);
204            }
205          } else {
206            findMetaAnnotations(ann.annotationType(), annotationType, ret, visited);
207          }
208        }
209      }
210    }
211  }
212
213  // caches for getRepeatableValueMethod
214  private static Method getNullMethod() {
215    try {
216      return AnnotationUtil.class.getDeclaredMethod("getNullMethod");
217    } catch (NoSuchMethodException e) {
218      return null;
219    }
220  }
221
222  private static final ConcurrentMap<Annotation, Method> valueMethods = new ConcurrentHashMap<>();
223  // only a non-null-marker the valueMethods - Cache
224  private static final Method nullMethod = getNullMethod();
225
226
227  /**
228   * Returns the <code>value()</code> method for a possible containerAnnotation.
229   * Method is retuned only, if its signature is <code>array of containingType</code>.
230   */
231  private static <A extends Annotation> Method getRepeatableValueMethod(
232    Annotation containerAnnotation, Class<A> containingType) {
233
234    Method method = valueMethods.get(containerAnnotation);
235    if (method == null) {
236      try {
237        method = containerAnnotation.annotationType().getMethod("value");
238      } catch (NoSuchMethodException e) {
239        method = nullMethod;
240      } catch (Exception e) {
241        throw new RuntimeException(e);
242      }
243      Method prev = valueMethods.putIfAbsent(containerAnnotation, method);
244      method = prev == null ? method : prev;
245    }
246    if (method != nullMethod) {
247      Class<?> retType = method.getReturnType();
248      if (retType.isArray() && retType.getComponentType() == containingType) {
249        return method;
250      }
251    }
252    return null;
253  }
254
255
256  /**
257   * Finds a suitable annotation from <code>Set<T> anns</code> for this platform.
258   * To distinguish between platforms, annotation type <code>T</code> must define
259   * a method withthis signature:
260   * <p>
261   * <code>Class<? extends DatabasePlatform>[] platforms() default {};</code>
262   * </p>
263   * The finding rules are:
264   * <ol>
265   * <li>Check if T has method "platforms" if not, return <code>ann[0]</code></code>
266   * <li>find the annotation that is defined for <code>databasePlatform</code></li>
267   * <li>otherwise return the annotation for default platform (platforms = {})</li>
268   * <li>return null
269   * </ol>
270   * (This mechanism is currently used by {@link Where} and {@link Formula})
271   */
272  public static <T extends Annotation> T getPlatformMatchingAnnotation(Set<T> anns, Platform matchPlatform) {
273    if (anns.isEmpty()) {
274      return null;
275    }
276    Method getPlatformsMethod = null;
277    T fallback = null;
278    for (T ann : anns) {
279      try {
280        if (getPlatformsMethod == null) {
281          getPlatformsMethod = ann.getClass().getMethod("platforms");
282        }
283        if (!Platform[].class.isAssignableFrom(getPlatformsMethod.getReturnType())) {
284          return ann;
285        }
286        Platform[] platforms = (Platform[]) getPlatformsMethod.invoke(ann);
287        if (platforms.length == 0) {
288          fallback = ann;
289        } else {
290          for (Platform platform : platforms) {
291            if (matchPlatform == platform) {
292              return ann;
293            }
294          }
295        }
296      } catch (NoSuchMethodException e) {
297        return ann; // not platform specific - return first one
298      } catch (Exception e) {
299        throw new RuntimeException(e);
300      }
301    }
302    return fallback;
303  }
304
305}