Better support for repeatable annotations (needs Java 8)

This commit is contained in:
Roland Praml
2016-10-31 23:36:43 +01:00
parent 18f943ab2b
commit 7b19973836
11 changed files with 316 additions and 79 deletions
+2 -2
View File
@@ -241,8 +241,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
@@ -0,0 +1,11 @@
package com.avaje.ebean.annotation;
import javax.validation.constraints.NotNull;
/**
* special validation group for &#64;NotNull annotation to enforce <code>NOT NULL</code> generation on DDL.
* Normally if you put the {@link NotNull} annotation on a property, Ebean will only generate a
* <code>NOT NULL</code> in DDL if you do not change the validation-groups!
*/
public interface EbeanDDL {
}
@@ -1,11 +1,13 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.avaje.ebean.Query;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
/**
* Assign to a property to be based on a SQL formula.
@@ -67,6 +69,7 @@ import com.avaje.ebean.Query;
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Formula.List.class)
public @interface Formula {
/**
@@ -102,5 +105,17 @@ public @interface Formula {
* }</pre>
*/
String join() default "";
Class<? extends DatabasePlatform>[] platforms() default {};
/**
* Repeatable support for {@link Formula}.
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface List {
Formula[] value() default {};
}
}
@@ -1,10 +1,13 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
/**
* Add an Literal to add to the where clause when a many property (List, Set or
* Map) is loaded or refreshed.
@@ -40,6 +43,7 @@ import java.lang.annotation.Target;
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Where.List.class)
public @interface Where {
/**
@@ -50,5 +54,19 @@ public @interface Where {
* </p>
*/
String clause();
/**
* The platform where this annotation is active. Default: any platform
*/
Class<? extends DatabasePlatform>[] platforms() default {};
/**
* Repeatable support for {@link Where}.
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface List {
Where[] value() default {};
}
}
@@ -6,6 +6,7 @@ import com.avaje.ebeaninternal.server.query.SqlJoinType;
import javax.persistence.JoinColumn;
import java.util.ArrayList;
import java.util.Set;
/**
* Represents a join to another table during deployment phase.
@@ -91,6 +92,15 @@ public class DeployTableJoin {
}
}
/**
* Add a JoinColumn set.
*/
public void addJoinColumn(boolean order, Set<JoinColumn> jcSet, BeanTable beanTable) {
for (JoinColumn jc: jcSet) {
addJoinColumn(order, jc, beanTable);
}
}
/**
* Return the join columns.
*/
@@ -1,7 +1,8 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import java.util.Set;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.MapKey;
@@ -87,14 +88,10 @@ public class AnnotationAssocManys extends AnnotationParser {
// check for manually defined joins
BeanTable beanTable = prop.getBeanTable();
JoinColumn joinColumn = get(prop, JoinColumn.class);
if (joinColumn != null) {
prop.getTableJoin().addJoinColumn(true, joinColumn, beanTable);
}
JoinColumns joinColumns = get(prop, JoinColumns.class);
Set<JoinColumn> joinColumns = getAll(prop, JoinColumn.class);
if (joinColumns != null) {
prop.getTableJoin().addJoinColumn(true, joinColumns.value(), beanTable);
prop.getTableJoin().addJoinColumn(true, joinColumns, beanTable);
}
JoinTable joinTable = get(prop, JoinTable.class);
@@ -1,13 +1,13 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import java.util.Map;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
@@ -93,7 +93,7 @@ public class AnnotationAssocOnes extends AnnotationParser {
if (validationAnnotations) {
NotNull notNull = get(prop, NotNull.class);
if (notNull != null) {
if (notNull != null && isEbeanValidationGroups(notNull.groups())) {
prop.setNullable(false);
// overrides optional attribute of ManyToOne etc
prop.getTableJoin().setType(SqlJoinType.INNER);
@@ -102,8 +102,7 @@ public class AnnotationAssocOnes extends AnnotationParser {
// check for manually defined joins
BeanTable beanTable = prop.getBeanTable();
JoinColumn joinColumn = get(prop, JoinColumn.class);
if (joinColumn != null) {
for (JoinColumn joinColumn : getAll(prop, JoinColumn.class)) {
prop.getTableJoin().addJoinColumn(false, joinColumn, beanTable);
if (!joinColumn.updatable()) {
prop.setDbUpdateable(false);
@@ -113,14 +112,18 @@ public class AnnotationAssocOnes extends AnnotationParser {
}
}
JoinColumns joinColumns = get(prop, JoinColumns.class);
if (joinColumns != null) {
prop.getTableJoin().addJoinColumn(false, joinColumns.value(), beanTable);
}
JoinTable joinTable = get(prop, JoinTable.class);
if (joinTable != null) {
prop.getTableJoin().addJoinColumn(false, joinTable.joinColumns(), beanTable);
for (JoinColumn joinColumn : joinTable.joinColumns()) {
prop.getTableJoin().addJoinColumn(false, joinColumn, beanTable);
if (!joinColumn.updatable()) {
prop.setDbUpdateable(false);
}
if (!joinColumn.nullable()) {
prop.setNullable(false);
}
}
}
info.setBeanJoinType(prop, prop.isNullable());
@@ -1,5 +1,7 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebean.annotation.Formula;
import com.avaje.ebean.annotation.Where;
import com.avaje.ebean.config.NamingConvention;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
@@ -9,7 +11,14 @@ 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;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.validation.constraints.Size;
/**
* Provides some base methods for processing deployment annotations.
@@ -38,28 +47,58 @@ public abstract class AnnotationBase {
return s == null || s.trim().isEmpty();
}
/**
* Return the annotation for the property.
* 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, Class)}), then the platform specific
* annotation is returned. Otherwise the first annotation is retured. 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 != findAnnotation(field, annClass, databasePlatform.getClass())) {
System.out.println("Difference (field) " + field + ", " + annClass);
}
}
if (a == null) {
Method m = prop.getReadMethod();
if (m != null) {
a = findAnnotation(m, annClass);
Method method = prop.getReadMethod();
if (method != null) {
a = findAnnotation(method, annClass);
if (a != findAnnotation(method, annClass, databasePlatform.getClass())) {
System.out.println("Difference (method) " + method + ", " + annClass);
}
}
}
return a;
}
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>
@@ -69,7 +108,7 @@ public abstract class AnnotationBase {
protected <T extends Annotation> T find(DeployBeanProperty prop, Class<T> annClass) {
T a = get(prop, annClass);
if (a == null) {
a = findAnnotation(prop.getOwningType(), annClass);
a = findAnnotation(prop.getOwningType(), annClass, databasePlatform.getClass());
}
return a;
}
@@ -91,6 +130,7 @@ public abstract class AnnotationBase {
* <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) {
@@ -132,6 +172,19 @@ public abstract class AnnotationBase {
}
}
/**
* 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,
Class<? extends DatabasePlatform> databasePlatform) {
if (annotationType == null) {
return null;
}
Set<A> anns = findAnnotations(annotatedElement, annotationType);
return getPlatformMatchingAnnotation(anns, databasePlatform);
}
/**
* Perform the search algorithm avoiding endless recursion by tracking which
* annotations have already been visited.
@@ -139,7 +192,7 @@ public abstract class AnnotationBase {
@SuppressWarnings("unchecked")
private static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> visited) {
Annotation[] anns = annotatedElement.getDeclaredAnnotations();
Annotation[] anns = annotatedElement.getAnnotations(); // directly annotatated or inherited
for (Annotation ann : anns) {
if (ann.annotationType() == annotationType) {
return (A) ann;
@@ -155,4 +208,144 @@ public abstract class AnnotationBase {
}
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<A>();
findMetaAnnotations(annotatedElement, annotationType, ret, new HashSet<Annotation>());
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 final Method getNullMethod() {
try {
return AnnotationBase.class.getDeclaredMethod("getNullMethod");
} catch (NoSuchMethodException e) {
return null;
}
}
private static final ConcurrentMap<Annotation, Method> valueMethods = new ConcurrentHashMap<Annotation, Method>();
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, Class<? extends DatabasePlatform> databasePlatform) {
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 (!Class[].class.isAssignableFrom(getPlatformsMethod.getReturnType())) {
return ann;
}
Class<?>[] platforms = (Class[]) getPlatformsMethod.invoke(ann);
if (platforms.length == 0) {
fallback = ann;
} else {
for (Class<?> platform : platforms) {
if (databasePlatform.isAssignableFrom(platform)) {
return ann;
}
}
}
} catch (NoSuchMethodException e) {
return ann; // not platform specific - return first one
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return fallback;
}
}
@@ -18,6 +18,8 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@@ -119,15 +121,8 @@ public class AnnotationClass extends AnnotationParser {
descriptor.setName("Embeddable:" + cls.getSimpleName());
}
Indices indices = AnnotationBase.findAnnotation(cls, Indices.class);
if (indices != null) {
for (Index index: indices.value()) {
descriptor.addIndex(new IndexDefinition(index.columnNames(), index.name(), index.unique()));
}
}
Index index = AnnotationBase.findAnnotation(cls,Index.class);
if (index != null) {
Set<Index> indices = AnnotationBase.findAnnotations(cls, Index.class);
for (Index index: indices) {
descriptor.addIndex(new IndexDefinition(index.columnNames(), index.name(), index.unique()));
}
@@ -188,15 +183,8 @@ public class AnnotationClass extends AnnotationParser {
descriptor.setCache(cache);
}
NamedQueries namedQueries = AnnotationBase.findAnnotation(cls,NamedQueries.class);
if (namedQueries != null) {
for (NamedQuery namedQuery : namedQueries.value()) {
descriptor.addNamedQuery(namedQuery.name(), namedQuery.query());
}
}
NamedQuery namedQuery = AnnotationBase.findAnnotation(cls,NamedQuery.class);
if (namedQuery != null) {
Set<NamedQuery> namedQueries = AnnotationBase.findAnnotations(cls,NamedQuery.class);
for (NamedQuery namedQuery : namedQueries) {
descriptor.addNamedQuery(namedQuery.name(), namedQuery.query());
}
}
@@ -26,6 +26,7 @@ import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.sql.Types;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
@@ -251,18 +252,19 @@ public class AnnotationFields extends AnnotationParser {
if (validationAnnotations) {
NotNull notNull = get(prop, NotNull.class);
if (notNull != null && isNotNullOnAllValidationGroups(notNull.groups())) {
if (notNull != null && isEbeanValidationGroups(notNull.groups())) {
// Not null on all validation groups so enable
// DDL generation of Not Null Constraint
prop.setNullable(false);
}
Size size = get(prop, Size.class);
if (size != null) {
if (size.max() < Integer.MAX_VALUE) {
// explicitly specify a version column
prop.setDbLength(size.max());
}
// take the max size of all @Size annotations
int maxSize = -1;
for (Size size : getAll(prop, Size.class)) {
maxSize = Math.max(maxSize, size.max());
}
if (maxSize !=1 && maxSize < Integer.MAX_VALUE) {
prop.setDbLength(maxSize);
}
}
@@ -310,15 +312,8 @@ public class AnnotationFields extends AnnotationParser {
}
}
Indices indices = get(prop, Indices.class);
if (indices != null) {
for (Index index: indices.value()) {
addIndex(prop, index);
}
}
Index index = get(prop, Index.class);
if (index != null) {
Set<Index> indices = getAll(prop, Index.class);
for (Index index: indices) {
addIndex(prop, index);
}
}
@@ -378,14 +373,6 @@ public class AnnotationFields extends AnnotationParser {
get(prop, OneToOne.class) != null;
}
/**
* Return true if the validation is on all validation groups and hence
* can be applied to DDL generation.
*/
private boolean isNotNullOnAllValidationGroups(Class<?>[] groups) {
return groups.length == 0 || groups.length == 1 && javax.validation.groups.Default.class.isAssignableFrom(groups[0]);
}
private void setEncryption(DeployBeanProperty prop, boolean dbEncString, int dbLen) {
util.checkEncryptKeyManagerDefined(prop.getFullBeanName());
@@ -1,12 +1,14 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import java.util.HashMap;
import java.util.Set;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.validation.groups.Default;
import com.avaje.ebean.annotation.EbeanDDL;
import com.avaje.ebeaninternal.server.deploy.BeanCascadeInfo;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
@@ -52,17 +54,12 @@ public abstract class AnnotationParser extends AnnotationBase {
*/
protected void readEmbeddedAttributeOverrides(DeployBeanPropertyAssocOne<?> prop) {
AttributeOverrides attrOverrides = get(prop, AttributeOverrides.class);
if (attrOverrides != null) {
HashMap<String, String> propMap = new HashMap<String, String>();
AttributeOverride[] aoArray = attrOverrides.value();
for (int i = 0; i < aoArray.length; i++) {
String propName = aoArray[i].name();
String columnName = aoArray[i].column().name();
propMap.put(propName, columnName);
Set<AttributeOverride> attrOverrides = getAll(prop, AttributeOverride.class);
if (!attrOverrides.isEmpty()) {
HashMap<String, String> propMap = new HashMap<>();
for (AttributeOverride attrOverride : attrOverrides) {
propMap.put(attrOverride.name(), attrOverride.column().name());
}
prop.getDeployEmbedded().putAll(propMap);
}
@@ -95,4 +92,22 @@ public abstract class AnnotationParser extends AnnotationBase {
prop.setSecondaryTable(tableName);
}
}
/**
* Return true if the validation groups are {@link Default} (respectively empty) or contains {@link EbeanDDL}
* can be applied to DDL generation.
*/
protected boolean isEbeanValidationGroups(Class<?>[] groups) {
if (groups.length == 0
|| groups.length == 1 && javax.validation.groups.Default.class.isAssignableFrom(groups[0])) {
return true;
} else {
for (Class<?> group : groups) {
if (EbeanDDL.class.isAssignableFrom(group)) {
return true;
}
}
}
return false;
}
}