package io.ebeaninternal.server.deploy.parse; import io.ebean.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. */ 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. * <p> * Looks first at the field and then at the getter method. * </p> * <p> * If a <code>repeatable</code> 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 must no longer * handle "java 1.6 repeatable containers" like {@link JoinColumn} / {@link JoinColumns} yourself. * </p> * <p> */ protected <T extends Annotation> T get(DeployBeanProperty prop, Class<T> 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 <T extends Annotation> Set<T> getAll(DeployBeanProperty prop, Class<T> annClass) { Set<T> 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. * <p> * Looks first at the field and then at the getter method. then at class level. * </p> */ protected <T extends Annotation> T find(DeployBeanProperty prop, Class<T> 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}. * <p> * Meta-annotations will be searched if the annotation is not <em>directly present</em> on the supplied element. * <p> * <strong>Warning</strong>: 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 extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) { if (annotationType == null) { return null; } // check if directly present, if not, start recursive traversal 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. * <p>Meta-annotations will be searched if the annotation is not directly present on * the supplied element. * <p><strong>Note</strong>: this method searches for annotations at class & superClass(es)! */ public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) { if (annotationType == null) { return null; } // check if directly present, if not, start recursive traversal 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; } // not 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 extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType, Platform platform) { if (annotationType == null) { return null; } Set<A> anns = findAnnotations(annotatedElement, annotationType); return getPlatformMatchingAnnotation(anns, platform); } /** * Perform the search algorithm avoiding endless recursion by tracking which * annotations have already been visited. */ @SuppressWarnings("unchecked") private static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> 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}. * <p> * Meta-annotations will be searched if the annotation is not <em>directly present</em> on the supplied element. * <p> * <strong>Warning</strong>: 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 <A extends Annotation> Set<A> findAnnotations(AnnotatedElement annotatedElement, Class<A> annotationType) { if (annotationType == null) { return null; } Set<A> 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 <A extends Annotation> void findMetaAnnotations(AnnotatedElement annotatedElement, Class<A> annotationType, Set<A> ret, Set<Annotation> 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<Annotation, Method> valueMethods = new ConcurrentHashMap<>(); private static final Method nullMethod = getNullMethod(); /** * Returns the <code>value()</code> method for a possible containerAnnotation. * Method is retuned only, if its signature is <code>array of containingType</code>. */ private static <A extends Annotation> Method getRepeatableValueMethod( Annotation containerAnnotation, Class<A> 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 <code>Set<T> anns</code> for this platform. * To distinguish between platforms, annotation type <code>T</code> must define * a method withthis signature: * <p> * <code>Class<? extends DatabasePlatform>[] platforms() default {};</code> * </p> * The finding rules are: * <ol> * <li>Check if T has method "platforms" if not, return <code>ann[0]</code></code> * <li>find the annotation that is defined for <code>databasePlatform</code></li> * <li>otherwise return the annotation for default platform (platforms = {})</li> * <li>return null * </ol> * (This mechanism is currently used by {@link Where} and {@link Formula}) */ public static <T extends Annotation> T getPlatformMatchingAnnotation(Set<T> 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; } }