package io.ebeaninternal.server.deploy.parse; import io.ebean.annotation.Platform; import io.ebean.annotation.Formula; import io.ebean.annotation.Where; import io.ebean.config.NamingConvention; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import javax.persistence.JoinColumn; import javax.persistence.JoinColumns; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Provides some base methods for processing deployment annotations. All findAnnotation* methods * are capable to search for meta-annotations (annotation that has an other annotation) * *

search algorithm for ONE annotation:

* * DFS (Depth-First-Search) is used. The algorithm is the same as it is used in Spring-Framework, * as the code is taken from there. * *

search algoritm for a Set<Annotation> works a litte bit different, as it does not stop * on the first match, but continues searching down to the last corner to find all annotations.

* *

To prevent endless recursion, the search algoritm tracks all visited annotations

* *

Supports also "java 1.6 repeatable containers" like{@link JoinColumn} / {@link JoinColumns}.

* *

This means, searching for JoinColumn will find them also if they are inside a * JoinColumns annotation

*/ public abstract class AnnotationBase { protected final DatabasePlatform databasePlatform; protected final Platform platform; protected final NamingConvention namingConvention; protected final DeployUtil util; protected AnnotationBase(DeployUtil util) { this.util = util; this.databasePlatform = util.getDbPlatform(); this.platform = databasePlatform.getPlatform(); this.namingConvention = util.getNamingConvention(); } /** * read the deployment annotations. */ public abstract void parse(); /** * Checks string is null or empty . */ protected boolean isEmpty(String s) { return s == null || s.trim().isEmpty(); } /** * Return the annotation for the property. *

* Looks first at the field and then at the getter method. It searches for meta-annotations, but not * recursively in the class hierarchy. *

*

* If a repeatable annotation class is specified and the annotation is platform * specific(see {@link #getPlatformMatchingAnnotation(Set, Platform)}), then the platform specific * annotation is returned. Otherwise the first annotation is returned. Note that you need no longer * handle "java 1.6 repeatable containers" like {@link JoinColumn} / {@link JoinColumns} yourself. *

*

*/ protected T get(DeployBeanProperty prop, Class annClass) { T a = null; Field field = prop.getField(); if (field != null) { a = findAnnotation(field, annClass); } if (a == null) { Method method = prop.getReadMethod(); if (method != null) { a = findAnnotation(method, annClass); } } return a; } /** * Return all annotations for this property. Annotations are not filtered by platfrom and you'll get * really all annotations that are directly, indirectly or meta-present. */ protected Set getAll(DeployBeanProperty prop, Class annClass) { Set ret = null; Field field = prop.getField(); if (field != null) { ret = findAnnotations(field, annClass); } Method method = prop.getReadMethod(); if (method != null) { if (ret != null) { ret.addAll(findAnnotations(method, annClass)); } else { ret = findAnnotations(method, annClass); } } return ret; } /** * Return the annotation for the property. *

* Looks first at the field and then at the getter method. then at class level. * (This is used for SequenceGenerator e.g.) *

*/ protected T find(DeployBeanProperty prop, Class annClass) { T a = get(prop, annClass); if (a == null) { a = findAnnotation(prop.getOwningType(), annClass, platform); } return a; } // this code is taken from the spring framework to find annotations recursively /** * Determine if the supplied {@link Annotation} is defined in the core JDK {@code java.lang.annotation} package. */ public static boolean isInJavaLangAnnotationPackage(Annotation annotation) { return annotation.annotationType().getName().startsWith("java.lang.annotation"); } /** * Find a single {@link Annotation} of {@code annotationType} on the supplied {@link AnnotatedElement}. *

* Meta-annotations will be searched if the annotation is not directly present on the supplied element. *

* Warning: this method operates generically on annotated elements. In other words, this method * does not execute specialized search algorithms for classes or methods. It only traverses through Annotations! * It also does not filter out platform dependent annotations! */ public static A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { if (annotationType == null) { return null; } // check if directly present, if not, start search for meta-annotations. A ann = annotatedElement.getAnnotation(annotationType); if (ann != null) { return ann; } else { return findAnnotation(annotatedElement, annotationType, new HashSet<>()); } } /** * Find a single {@link Annotation} of {@code annotationType} on the supplied class. *

Meta-annotations will be searched if the annotation is not directly present on * the supplied element. *

Note: this method searches for annotations at class & superClass(es)! */ public static A findAnnotationRecursive(Class clazz, Class annotationType) { if (annotationType == null) { return null; } // check if directly present, if not, start search for meta-annotations. A ann = clazz.getAnnotation(annotationType); if (ann != null) { return ann; } else { while (clazz != null && clazz != Object.class) { ann = findAnnotation(clazz, annotationType, new HashSet<>()); if (ann != null) { return ann; } // no meta-annotation present at this class - traverse to superclass clazz = clazz.getSuperclass(); } return null; } } /** * Finds the first annotation of a type for this platform. (if annotation is platform specific, otherwise first * found annotation is returned) */ public static A findAnnotation(AnnotatedElement annotatedElement, Class annotationType, Platform platform) { if (annotationType == null) { return null; } Set anns = findAnnotations(annotatedElement, annotationType); return getPlatformMatchingAnnotation(anns, platform); } /** * Finds all annotations recusively for a class and its superclasses. */ public static Set findAnnotationsRecursive(Class clazz, Class annotationType) { if (annotationType == null) { return null; } Set ret = new LinkedHashSet<>(); Set visited = new HashSet<>(); while (clazz != null && clazz != Object.class) { findMetaAnnotations(clazz, annotationType, ret, visited); clazz = clazz.getSuperclass(); } return ret; } /** * Perform the search algorithm avoiding endless recursion by tracking which * annotations have already been visited. */ @SuppressWarnings("unchecked") private static A findAnnotation(AnnotatedElement annotatedElement, Class annotationType, Set visited) { Annotation[] anns = annotatedElement.getAnnotations(); // directly annotatated or inherited for (Annotation ann : anns) { if (ann.annotationType() == annotationType) { return (A) ann; } } for (Annotation ann : anns) { if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) { A annotation = findAnnotation(ann.annotationType(), annotationType, visited); if (annotation != null) { return annotation; } } } return null; } /** * Find all {@link Annotation}s of {@code annotationType} on the supplied {@link AnnotatedElement}. *

* Meta-annotations will be searched if the annotation is not directly present on the supplied element. *

* Warning: this method operates generically on annotated elements. In other words, this method * does not execute specialized search algorithms for classes or methods. It only traverses through Annotations! */ public static Set findAnnotations(AnnotatedElement annotatedElement, Class annotationType) { if (annotationType == null) { return null; } Set ret = new LinkedHashSet<>(); findMetaAnnotations(annotatedElement, annotationType, ret, new HashSet<>()); return ret; } /** * Perform the search algorithm avoiding endless recursion by tracking which * annotations have already been visited. */ @SuppressWarnings("unchecked") private static void findMetaAnnotations(AnnotatedElement annotatedElement, Class annotationType, Set ret, Set visited) { Annotation[] anns = annotatedElement.getAnnotations(); for (Annotation ann : anns) { if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) { if (ann.annotationType() == annotationType) { ret.add((A) ann); } else { Method repeatableValueMethod = getRepeatableValueMethod(ann, annotationType); if (repeatableValueMethod != null) { try { A[] repeatedAnns = (A[]) repeatableValueMethod.invoke(ann); for (Annotation repeatedAnn : repeatedAnns) { ret.add((A) repeatedAnn); findMetaAnnotations(repeatedAnn.annotationType(), annotationType, ret, visited); } } catch (Exception e) { // catch all exceptions (thrown by invoke) throw new RuntimeException(e); } } else { findMetaAnnotations(ann.annotationType(), annotationType, ret, visited); } } } } } // caches for getRepeatableValueMethod private static Method getNullMethod() { try { return AnnotationBase.class.getDeclaredMethod("getNullMethod"); } catch (NoSuchMethodException e) { return null; } } private static final ConcurrentMap valueMethods = new ConcurrentHashMap<>(); // only a non-null-marker the valueMethods - Cache private static final Method nullMethod = getNullMethod(); /** * Returns the value() method for a possible containerAnnotation. * Method is retuned only, if its signature is array of containingType. */ private static Method getRepeatableValueMethod( Annotation containerAnnotation, Class containingType) { Method method = valueMethods.get(containerAnnotation); if (method == null) { try { method = containerAnnotation.annotationType().getMethod("value"); } catch (NoSuchMethodException e) { method = nullMethod; } catch (Exception e) { throw new RuntimeException(e); } Method prev = valueMethods.putIfAbsent(containerAnnotation, method); method = prev == null ? method : prev; } if (method != nullMethod) { Class retType = method.getReturnType(); if (retType.isArray() && retType.getComponentType() == containingType) { return method; } } return null; } /** * Finds a suitable annotation from Set anns for this platform. * To distinguish between platforms, annotation type T must define * a method withthis signature: *

* Class[] platforms() default {}; *

* The finding rules are: *
    *
  1. Check if T has method "platforms" if not, return ann[0] *
  2. find the annotation that is defined for databasePlatform
  3. *
  4. otherwise return the annotation for default platform (platforms = {})
  5. *
  6. return null *
* (This mechanism is currently used by {@link Where} and {@link Formula}) */ public static T getPlatformMatchingAnnotation(Set anns, Platform matchPlatform) { if (anns.isEmpty()) { return null; } Method getPlatformsMethod = null; T fallback = null; for (T ann : anns) { try { if (getPlatformsMethod == null) { getPlatformsMethod = ann.getClass().getMethod("platforms"); } if (!Platform[].class.isAssignableFrom(getPlatformsMethod.getReturnType())) { return ann; } Platform[] platforms = (Platform[]) getPlatformsMethod.invoke(ann); if (platforms.length == 0) { fallback = ann; } else { for (Platform platform : platforms) { if (matchPlatform == platform) { return ann; } } } } catch (NoSuchMethodException e) { return ann; // not platform specific - return first one } catch (Exception e) { throw new RuntimeException(e); } } return fallback; } }