Merge pull request #814 from FOCONIS/master

ENHANCEMENT: multiple indices & meta-annotation support similar to spring-framework
This commit is contained in:
Rob Bygrave
2016-08-23 19:43:04 +12:00
committed by GitHub
14 changed files with 300 additions and 58 deletions
@@ -1,17 +1,19 @@
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;
/**
* An annotation for declaring an index on a single column.
* An annotation for declaring an index.
*
* @author rvbiljouw
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Indices.class)
public @interface Index {
/**
@@ -27,6 +29,9 @@ public @interface Index {
/**
* When placed on the class (rather than field) you can specify the columns
* to include in the index in order.
*
* Wnen placed on a field, and columnNames are specified, the field-column has to be included.
* You can use "${fa}" for alias
*/
String[] columnNames() default {};
@@ -0,0 +1,20 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation for declaring multiple indices at class or field level.
*
* @author Roland Praml, FOCONIS AG
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Indices {
/**
* Array with {@link Index} definitions.
*/
Index[] value() default {};
}
@@ -5,6 +5,7 @@ import com.avaje.ebean.annotation.ChangeLogInsertMode;
import com.avaje.ebean.event.BeanPersistRequest;
import com.avaje.ebean.event.changelog.ChangeLogFilter;
import com.avaje.ebean.event.changelog.ChangeLogRegister;
import com.avaje.ebeaninternal.server.deploy.parse.AnnotationBase;
import java.util.HashSet;
import java.util.Set;
@@ -52,14 +53,7 @@ public class DefaultChangeLogRegister implements ChangeLogRegister {
* Find and return the ChangeLog annotation in the inheritance hierarchy.
*/
private ChangeLog getChangeLog(Class<?> beanType) {
ChangeLog changeLog = beanType.getAnnotation(ChangeLog.class);
if (changeLog != null) {
return changeLog;
}
if (Object.class.equals(beanType.getSuperclass())) {
return null;
}
return getChangeLog(beanType.getSuperclass());
return AnnotationBase.findAnnotation(beanType, ChangeLog.class);
}
/**
@@ -38,6 +38,7 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanTable;
import com.avaje.ebeaninternal.server.deploy.meta.DeployTableJoin;
import com.avaje.ebeaninternal.server.deploy.parse.AnnotationBase;
import com.avaje.ebeaninternal.server.deploy.parse.DeployBeanInfo;
import com.avaje.ebeaninternal.server.deploy.parse.DeployCreateProperties;
import com.avaje.ebeaninternal.server.deploy.parse.DeployInherit;
@@ -1457,7 +1458,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
* If so it is ok for it not to be enhanced.
*/
private boolean isMappedSuperWithNoProperties(Class<?> beanClass) {
// Attention: do not use AnnotationBase.findAnnotation(cls,...) here.
MappedSuperclass annotation = beanClass.getAnnotation(MappedSuperclass.class);
if (annotation == null) {
return false;
@@ -18,6 +18,7 @@ import com.avaje.ebeaninternal.server.core.InternString;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.DeployDocPropertyOptions;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import com.avaje.ebeaninternal.server.deploy.parse.AnnotationBase;
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import com.avaje.ebeaninternal.server.properties.BeanPropertyGetter;
import com.avaje.ebeaninternal.server.properties.BeanPropertySetter;
@@ -233,29 +234,29 @@ public class DeployBeanProperty {
if (field == null) {
return 0;
}
if (field.getAnnotation(Id.class) != null) {
if (AnnotationBase.findAnnotation(field, Id.class) != null) {
return ID_ORDER;
} else if (field.getAnnotation(EmbeddedId.class) != null) {
} else if (AnnotationBase.findAnnotation(field, EmbeddedId.class) != null) {
return ID_ORDER;
} else if (undirectionalShadow) {
return UNIDIRECTIONAL_ORDER;
} else if (isAuditProperty()) {
return AUDITCOLUMN_ORDER;
} else if (field.getAnnotation(Version.class) != null) {
} else if (AnnotationBase.findAnnotation(field, Version.class) != null) {
return VERSIONCOLUMN_ORDER;
} else if (field.getAnnotation(SoftDelete.class) != null) {
} else if (AnnotationBase.findAnnotation(field, SoftDelete.class) != null) {
return VERSIONCOLUMN_ORDER;
}
return 0;
}
private boolean isAuditProperty() {
return (field.getAnnotation(WhenCreated.class) != null
|| field.getAnnotation(WhenModified.class) != null
|| field.getAnnotation(WhoModified.class) != null
|| field.getAnnotation(WhoCreated.class) != null
|| field.getAnnotation(UpdatedTimestamp.class) != null
|| field.getAnnotation(CreatedTimestamp.class) != null);
return (AnnotationBase.findAnnotation(field, WhenCreated.class) != null
|| AnnotationBase.findAnnotation(field, WhenModified.class) != null
|| AnnotationBase.findAnnotation(field, WhoModified.class) != null
|| AnnotationBase.findAnnotation(field, WhoCreated.class) != null
|| AnnotationBase.findAnnotation(field, UpdatedTimestamp.class) != null
|| AnnotationBase.findAnnotation(field, CreatedTimestamp.class) != null);
}
public String getFullBeanName() {
@@ -1,8 +1,11 @@
package com.avaje.ebeaninternal.server.deploy.parse;
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.Set;
import com.avaje.ebean.config.NamingConvention;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
@@ -46,12 +49,12 @@ public abstract class AnnotationBase {
T a = null;
Field field = prop.getField();
if (field != null) {
a = field.getAnnotation(annClass);
a = findAnnotation(field, annClass);
}
if (a == null) {
Method m = prop.getReadMethod();
if (m != null) {
a = m.getAnnotation(annClass);
a = findAnnotation(m, annClass);
}
}
return a;
@@ -66,10 +69,95 @@ public abstract class AnnotationBase {
protected <T extends Annotation> T find(DeployBeanProperty prop, Class<T> annClass) {
T a = get(prop, annClass);
if (a == null) {
a = prop.getOwningType().getAnnotation(annClass);
a = findAnnotation(prop.getOwningType(),annClass);
}
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!</p>
*/
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<Annotation>());
}
}
/**
* Find a single {@link Annotation} of {@code annotationType} on the
* supplied {@link clazz}.
* <p>Meta-annotations will be searched if the annotation is not
* <em>directly present</em> on the supplied element.
* <p><strong>Note</strong>: this method searches for annotations at
* class & superClass(es)!</p>
*/
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<Annotation>());
if (ann != null) {
return ann;
}
// not present at this class - traverse to superclass
clazz = clazz.getSuperclass();
}
return null;
}
}
/**
* Perform the search algorithm for {@link #findAnnotation(AnnotatedElement, Class)}
* avoiding endless recursion by tracking which annotations have already
* been <em>visited</em>.
*/
@SuppressWarnings("unchecked")
private static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> visited) {
Annotation[] anns = annotatedElement.getDeclaredAnnotations();
for (Annotation ann : anns) {
if (ann.annotationType() == annotationType) {
return (A) ann;
}
}
for (Annotation ann : anns) {
if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
A annotation = findAnnotation((AnnotatedElement) ann.annotationType(), annotationType, visited);
if (annotation != null) {
return annotation;
}
}
}
return null;
}
}
@@ -7,6 +7,7 @@ import com.avaje.ebean.annotation.Draftable;
import com.avaje.ebean.annotation.DraftableElement;
import com.avaje.ebean.annotation.History;
import com.avaje.ebean.annotation.Index;
import com.avaje.ebean.annotation.Indices;
import com.avaje.ebean.annotation.ReadAudit;
import com.avaje.ebean.annotation.UpdateMode;
import com.avaje.ebean.annotation.View;
@@ -66,7 +67,7 @@ public class AnnotationClass extends AnnotationParser {
public void parseAttributeOverride() {
Class<?> cls = descriptor.getBeanType();
AttributeOverride override = cls.getAnnotation(AttributeOverride.class);
AttributeOverride override = AnnotationBase.findAnnotation(cls,AttributeOverride.class);
if (override != null) {
String propertyName = override.name();
Column column = override.column();
@@ -103,7 +104,7 @@ public class AnnotationClass extends AnnotationParser {
private void read(Class<?> cls) {
Entity entity = cls.getAnnotation(Entity.class);
Entity entity = AnnotationBase.findAnnotation(cls,Entity.class);
if (entity != null) {
if (entity.name().equals("")) {
descriptor.setName(cls.getSimpleName());
@@ -112,27 +113,34 @@ public class AnnotationClass extends AnnotationParser {
}
}
Embeddable embeddable = cls.getAnnotation(Embeddable.class);
Embeddable embeddable = AnnotationBase.findAnnotation(cls,Embeddable.class);
if (embeddable != null) {
descriptor.setEntityType(EntityType.EMBEDDED);
descriptor.setName("Embeddable:" + cls.getSimpleName());
}
Index index = cls.getAnnotation(Index.class);
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) {
descriptor.addIndex(new IndexDefinition(index.columnNames(), index.name(), index.unique()));
}
UniqueConstraint uc = cls.getAnnotation(UniqueConstraint.class);
UniqueConstraint uc = AnnotationBase.findAnnotation(cls,UniqueConstraint.class);
if (uc != null) {
descriptor.addIndex(new IndexDefinition(uc.columnNames()));
}
View view = cls.getAnnotation(View.class);
View view = AnnotationBase.findAnnotation(cls,View.class);
if (view != null) {
descriptor.setView(view.name(), view.dependentTables());
}
Table table = cls.getAnnotation(Table.class);
Table table = AnnotationBase.findAnnotation(cls,Table.class);
if (table != null) {
UniqueConstraint[] uniqueConstraints = table.uniqueConstraints();
for (UniqueConstraint c : uniqueConstraints) {
@@ -140,54 +148,54 @@ public class AnnotationClass extends AnnotationParser {
}
}
Draftable draftable = cls.getAnnotation(Draftable.class);
Draftable draftable = AnnotationBase.findAnnotation(cls,Draftable.class);
if (draftable != null) {
descriptor.setDraftable();
}
DraftableElement draftableElement = cls.getAnnotation(DraftableElement.class);
DraftableElement draftableElement = AnnotationBase.findAnnotation(cls,DraftableElement.class);
if (draftableElement != null) {
descriptor.setDraftableElement();
}
ReadAudit readAudit = cls.getAnnotation(ReadAudit.class);
ReadAudit readAudit = AnnotationBase.findAnnotation(cls,ReadAudit.class);
if (readAudit != null) {
descriptor.setReadAuditing();
}
History history = cls.getAnnotation(History.class);
History history = AnnotationBase.findAnnotation(cls,History.class);
if (history != null) {
descriptor.setHistorySupport();
}
DbComment comment = cls.getAnnotation(DbComment.class);
DbComment comment = AnnotationBase.findAnnotation(cls,DbComment.class);
if (comment != null) {
descriptor.setDbComment(comment.value());
}
DocStore docStore = cls.getAnnotation(DocStore.class);
DocStore docStore = AnnotationBase.findAnnotation(cls,DocStore.class);
if (docStore != null) {
descriptor.readDocStore(docStore);
}
UpdateMode updateMode = cls.getAnnotation(UpdateMode.class);
UpdateMode updateMode = AnnotationBase.findAnnotation(cls,UpdateMode.class);
if (updateMode != null) {
descriptor.setUpdateChangesOnly(updateMode.updateChangesOnly());
}
Cache cache = cls.getAnnotation(Cache.class);
Cache cache = AnnotationBase.findAnnotation(cls,Cache.class);
if (cache != null && !disableL2Cache) {
descriptor.setCache(cache);
}
NamedQueries namedQueries = cls.getAnnotation(NamedQueries.class);
NamedQueries namedQueries = AnnotationBase.findAnnotation(cls,NamedQueries.class);
if (namedQueries != null) {
for (NamedQuery namedQuery : namedQueries.value()) {
descriptor.addNamedQuery(namedQuery.name(), namedQuery.query());
}
}
NamedQuery namedQuery = cls.getAnnotation(NamedQuery.class);
NamedQuery namedQuery = AnnotationBase.findAnnotation(cls,NamedQuery.class);
if (namedQuery != null) {
descriptor.addNamedQuery(namedQuery.name(), namedQuery.query());
}
@@ -310,15 +310,46 @@ 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) {
if (hasRelationshipItem(prop)) {
throw new RuntimeException("Can't use Index on foreign key relationships.");
}
descriptor.addIndex(new IndexDefinition(prop.getDbColumn(), index.name(), index.unique()));
addIndex(prop, index);
}
}
private void addIndex(DeployBeanProperty prop, Index index) {
String[] columnNames;
if (index.columnNames() == null || index.columnNames().length == 0) {
columnNames = new String[] {prop.getDbColumn()};
} else {
columnNames = new String[index.columnNames().length];
int i=0;
int found = 0;
for (String colName : index.columnNames()) {
if (colName.equals("${fa}") || colName.equals(prop.getDbColumn())) {
columnNames[i++] = prop.getDbColumn();
found++;
} else {
columnNames[i++] = colName;
}
}
if (found != 1) {
throw new RuntimeException("DB-columname has to be specified exactly one time in columnNames.");
}
}
if (columnNames.length == 1 && hasRelationshipItem(prop)) {
throw new RuntimeException("Can't use Index on foreign key relationships.");
}
descriptor.addIndex(new IndexDefinition(columnNames, index.name(), index.unique()));
}
private void readJsonAnnotations(DeployBeanProperty prop) {
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
@@ -14,7 +14,7 @@ public class AnnotationSql extends AnnotationParser {
public void parse() {
Class<?> cls = descriptor.getBeanType();
Sql sql = cls.getAnnotation(Sql.class);
Sql sql = AnnotationBase.findAnnotation(cls,Sql.class);
if (sql != null) {
descriptor.setEntityType(BeanDescriptor.EntityType.SQL);
}
@@ -223,7 +223,7 @@ public class DeployCreateProperties {
Class<?> propertyType = field.getType();
ManyToOne manyToOne = field.getAnnotation(ManyToOne.class);
ManyToOne manyToOne = AnnotationBase.findAnnotation(field,ManyToOne.class);
if (manyToOne != null){
Class<?> tt = manyToOne.targetEntity();
if (!tt.equals(void.class)){
@@ -240,7 +240,7 @@ public class DeployCreateProperties {
// List, Set or Map based object
Class<?> targetType = determineTargetType(field);
if (targetType == null) {
Transient transAnnotation = field.getAnnotation(Transient.class);
Transient transAnnotation = AnnotationBase.findAnnotation(field,Transient.class);
if (transAnnotation != null) {
// not supporting this field (generic type used)
return null;
@@ -298,15 +298,15 @@ public class DeployCreateProperties {
* Return true if the field has one of the special mappings.
*/
private boolean isSpecialScalarType(Field field) {
return (field.getAnnotation(DbJson.class) != null)
|| (field.getAnnotation(DbJsonB.class) != null)
|| (field.getAnnotation(DbArray.class) != null)
|| (field.getAnnotation(DbHstore.class) != null);
return (AnnotationBase.findAnnotation(field,DbJson.class) != null)
|| (AnnotationBase.findAnnotation(field,DbJsonB.class) != null)
|| (AnnotationBase.findAnnotation(field,DbArray.class) != null)
|| (AnnotationBase.findAnnotation(field,DbHstore.class) != null);
}
private boolean isTransientField(Field field) {
Transient t = field.getAnnotation(Transient.class);
Transient t = AnnotationBase.findAnnotation(field,Transient.class);
return (t != null);
}
@@ -111,11 +111,11 @@ public class DeployInherit {
info.setParent(parent);
}
Inheritance ia = cls.getAnnotation(Inheritance.class);
Inheritance ia = AnnotationBase.findAnnotation(cls,Inheritance.class);
if (ia != null) {
ia.strategy();
}
DiscriminatorColumn da = cls.getAnnotation(DiscriminatorColumn.class);
DiscriminatorColumn da = AnnotationBase.findAnnotation(cls,DiscriminatorColumn.class);
if (da != null) {
// lowercase the discriminator column for RawSql and JSON
info.setDiscriminatorColumn(da.name().toLowerCase());
@@ -128,7 +128,7 @@ public class DeployInherit {
info.setDiscriminatorLength(da.length());
}
DiscriminatorValue dv = cls.getAnnotation(DiscriminatorValue.class);
DiscriminatorValue dv = AnnotationBase.findAnnotation(cls,DiscriminatorValue.class);
if (dv != null) {
info.setDiscriminatorValue(dv.value());
}
@@ -149,7 +149,7 @@ public class DeployInherit {
if (cls.equals(Object.class)) {
return false;
}
Annotation a = cls.getAnnotation(Inheritance.class);
Annotation a = AnnotationBase.findAnnotation(cls,Inheritance.class);
if (a != null) {
return true;
}
@@ -0,0 +1,71 @@
package com.avaje.tests.basic;
import javax.persistence.PersistenceException;
import org.junit.Ignore;
import org.junit.Test;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.tests.model.basic.Address;
import com.avaje.tests.model.basic.metaannotation.SizeMedium;
/**
* Very simple test case to check if Ebean recognizes meta-annotation correctly.
*
* @author Roland Praml, FOCONIS AG
*/
public class TestMetaAnnotation extends BaseTestCase {
private static final String spaces100 = new String(new char[100]).replace('\0', ' ');
private static final String spaces101 = new String(new char[101]).replace('\0', ' ');
/**
* This test writes 100 spaces to "line1" which is annotated with &#64;Size(max=100)
*/
@Test
public void testWrite100SpacesToLine1() {
Ebean.createUpdate(Address.class, "delete from address");
Address address = new Address();
address.setLine1(spaces100);
Ebean.save(address);
}
/**
* This test writes 101 spaces to "line1" which is annotated with &#64;Size(max=100).
*/
@Test(expected=PersistenceException.class)
@Ignore // Unfortunately, this test does not work with PostGres as DB-Backend, as it does not honor the @Size annotation
public void testWrite101SpacesToLine1() {
Ebean.createUpdate(Address.class, "delete from address");
Address address = new Address();
address.setLine1(spaces101);
Ebean.save(address);
}
/**
* This test writes 100 spaces to "line1" which is meta-annotated with {@link SizeMedium}.
*/
@Test
public void testWrite100SpacesToLine2() {
Ebean.createUpdate(Address.class, "delete from address");
Address address = new Address();
address.setLine2(spaces100);
Ebean.save(address);
}
/**
* This test writes 101 spaces to "line1" which is meta-annotated with {@link SizeMedium}.
*/
@Test(expected=PersistenceException.class)
@Ignore // Unfortunately, this test does not work with PostGres as Backend
public void testWrite101SpacesToLine2() {
Ebean.createUpdate(Address.class, "delete from address");
Address address = new Address();
address.setLine2(spaces101);
Ebean.save(address);
}
}
@@ -7,6 +7,9 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Version;
import javax.validation.constraints.Size;
import com.avaje.tests.model.basic.metaannotation.SizeMedium;
import java.sql.Timestamp;
/**
@@ -23,11 +26,11 @@ public class Address {
@Column(name = "line_1")
String line1;
@Size(max = 100)
@SizeMedium
@Column(name = "line_2")
String line2;
@Size(max = 100)
@SizeMedium
String city;
Timestamp cretime;
@@ -0,0 +1,20 @@
package com.avaje.tests.model.basic.metaannotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.constraints.Size;
/**
* Meta-Annotation that defines &#64;Size(max=100)
* @author Roland Praml, FOCONIS AG
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Size(max=100)
public @interface SizeMedium {
}