DATACASS-259 - Allow usage of Spring 4.2 @AliasFor for Cassandra annotations.

We now support @AliasFor to build composed annotations with @Table, @UserDefinedType, @PrimaryKey, @PrimaryKeyClass, @PrimaryKeyColumn, @Column, @Query, @CassandraType.

Original pull request: #90.
This commit is contained in:
Mark Paluch
2016-11-28 15:02:36 +01:00
committed by John Blum
parent 3b717be06a
commit 847db1a52b
9 changed files with 307 additions and 35 deletions

View File

@@ -98,13 +98,10 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
protected CqlIdentifier determineTableName() {
Table tableAnnotation = getType().getAnnotation(Table.class);
Table tableAnnotation = findAnnotation(Table.class);
if (tableAnnotation == null) {
return determineDefaultName();
}
return determineName(tableAnnotation.value(), tableAnnotation.forceQuote());
return tableAnnotation == null ? determineDefaultName()
: determineName(tableAnnotation.value(), tableAnnotation.forceQuote());
}
@Override
@@ -119,7 +116,7 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
@Override
public boolean isCompositePrimaryKey() {
return getType().isAnnotationPresent(PrimaryKeyClass.class);
return findAnnotation(PrimaryKeyClass.class) != null;
}
@Override

View File

@@ -22,12 +22,14 @@ import java.lang.annotation.Target;
import org.springframework.cassandra.core.Ordering;
import org.springframework.cassandra.core.PrimaryKeyType;
import org.springframework.core.annotation.AliasFor;
/**
* Identifies the annotated field of a composite primary key class as a primary key field that is either a partition or
* cluster key field.
*
* @author Matthew T. Adams
* @author Mark Paluch
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@@ -36,12 +38,19 @@ public @interface PrimaryKeyColumn {
/**
* The name of the column in the table.
*/
@AliasFor(attribute = "name")
String value() default "";
/**
* The name of the column in the table.
*/
@AliasFor(attribute = "value")
String name() default "";
/**
* The order of this column relative to other primary key columns.
*/
int ordinal();
int ordinal() default Integer.MIN_VALUE;
/**
* The type of this key column. Default is {@link PrimaryKeyType#CLUSTERED}.

View File

@@ -28,9 +28,10 @@ import org.springframework.data.annotation.QueryAnnotation;
*
* @author Alex Shvid
* @author Matthew T. Adams
* @author Mark Paluch
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@QueryAnnotation
public @interface Query {

View File

@@ -15,11 +15,14 @@
*/
package org.springframework.data.cassandra.repository.query;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.mapping.CassandraType;
import org.springframework.data.cassandra.repository.query.CassandraParameters.CassandraParameter;
import org.springframework.data.repository.query.Parameter;
@@ -79,8 +82,11 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
super(parameter);
if (parameter.hasParameterAnnotation(CassandraType.class)) {
CassandraType cassandraType = parameter.getParameterAnnotation(CassandraType.class);
AnnotatedParameter annotatedParameter = new AnnotatedParameter(parameter);
if (AnnotatedElementUtils.hasAnnotation(annotatedParameter, CassandraType.class)) {
CassandraType cassandraType = AnnotatedElementUtils.findMergedAnnotation(annotatedParameter,
CassandraType.class);
Assert.notNull(cassandraType.type(),
String.format("You must specify the type() when annotating method parameters with @%s",
@@ -117,7 +123,6 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
* unwrapped.
*
* @param parameter must not be {@literal null}.
* @return
*/
private static Class<?> potentiallyUnwrapParameterType(MethodParameter parameter) {
@@ -134,7 +139,6 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
* Returns whether the {@link MethodParameter} is wrapped in a wrapper type.
*
* @param parameter must not be {@literal null}.
* @return
* @see QueryExecutionConverters
*/
private static boolean isWrapped(MethodParameter parameter) {
@@ -145,7 +149,6 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
* Returns whether the {@link MethodParameter} should be unwrapped.
*
* @param parameter must not be {@literal null}.
* @return
* @see QueryExecutionConverters
*/
private static boolean shouldUnwrap(MethodParameter parameter) {
@@ -153,4 +156,42 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
|| ReactiveWrappers.supports(parameter.getParameterType());
}
}
/**
* {@link AnnotatedElement} implementation as annotation source for {@link AnnotatedElementUtils}.
*
* @author Mark Paluch
*/
static class AnnotatedParameter implements AnnotatedElement {
private final MethodParameter methodParameter;
AnnotatedParameter(MethodParameter methodParameter) {
this.methodParameter = methodParameter;
}
/* (non-Javadoc)
* @see java.lang.reflect.AnnotatedElement#getAnnotation(java.lang.Class)
*/
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return methodParameter.getParameterAnnotation(annotationClass);
}
/* (non-Javadoc)
* @see java.lang.reflect.AnnotatedElement#getAnnotations()
*/
@Override
public Annotation[] getAnnotations() {
return methodParameter.getParameterAnnotations();
}
/* (non-Javadoc)
* @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotations()
*/
@Override
public Annotation[] getDeclaredAnnotations() {
return methodParameter.getParameterAnnotations();
}
}
}

View File

@@ -18,12 +18,18 @@ package org.springframework.data.cassandra.mapping;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.util.ClassTypeInformation;
/**
@@ -113,6 +119,30 @@ public class BasicCassandraPersistentEntityUnitTests {
assertThat(entity.isUserDefinedType()).isFalse();
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedTableAnnotation() {
BasicCassandraPersistentEntity<TableWithComposedAnnotation> entity = new BasicCassandraPersistentEntity<TableWithComposedAnnotation>(
ClassTypeInformation.from(TableWithComposedAnnotation.class));
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.cqlId("mytable", true));
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedPrimaryKeyClassAnnotation() {
BasicCassandraPersistentEntity<PrimaryKeyClassWithComposedAnnotation> entity = new BasicCassandraPersistentEntity<PrimaryKeyClassWithComposedAnnotation>(
ClassTypeInformation.from(PrimaryKeyClassWithComposedAnnotation.class));
assertThat(entity.isCompositePrimaryKey()).isTrue();
}
@Table("messages")
static class Message {}
@@ -132,4 +162,25 @@ public class BasicCassandraPersistentEntityUnitTests {
return tableName;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Table(forceQuote = true)
@interface ComposedTableAnnotation {
@AliasFor(annotation = Table.class)
String value() default "mytable";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@PrimaryKeyClass
@interface ComposedPrimaryKeyClass {
}
@ComposedTableAnnotation()
static class TableWithComposedAnnotation {}
@ComposedPrimaryKeyClass()
static class PrimaryKeyClassWithComposedAnnotation {}
}

View File

@@ -17,21 +17,103 @@ package org.springframework.data.cassandra.mapping;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.util.ReflectionUtils;
import com.datastax.driver.core.DataType.Name;
/**
* Unit tests for {@link BasicCassandraPersistentProperty}.
*
* @author Alex Shvid
* @author Mark Paluch
*/
public class BasicCassandraPersistentPropertyUnitTests {
@Test
public void usesAnnotatedColumnName() {
assertThat(getPropertyFor(Timeline.class, "text").getColumnName().toCql()).isEqualTo("message");
}
@Test
public void checksIdProperty() {
CassandraPersistentProperty property = getPropertyFor(Timeline.class, "id");
assertThat(property.isIdProperty()).isTrue();
}
@Test
public void returnsPropertyNameForUnannotatedProperty() {
assertThat(getPropertyFor(Timeline.class, "time").getColumnName().toCql()).isEqualTo("time");
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedColumnAnnotation() {
CassandraPersistentProperty persistentProperty = getPropertyFor(TypeWithComposedColumnAnnotation.class, "column");
assertThat(persistentProperty.getColumnName()).isEqualTo(CqlIdentifier.cqlId("mycolumn", true));
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedPrimaryKeyColumnAnnotation() {
CassandraPersistentProperty persistentProperty = getPropertyFor(TypeWithComposedPrimaryKeyColumnAnnotation.class,
"column");
assertThat(persistentProperty.getColumnName()).isEqualTo(CqlIdentifier.cqlId("mycolumn", true));
assertThat(persistentProperty.isPrimaryKeyColumn()).isTrue();
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedPrimaryKeyAnnotation() {
CassandraPersistentProperty persistentProperty = getPropertyFor(TypeWithComposedPrimaryKeyAnnotation.class,
"column");
assertThat(persistentProperty.getColumnName()).isEqualTo(CqlIdentifier.cqlId("primary-key", true));
assertThat(persistentProperty.isIdProperty()).isTrue();
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedCassandraTypeAnnotation() {
CassandraPersistentProperty persistentProperty = getPropertyFor(TypeWithComposedCassandraTypeAnnotation.class,
"column");
assertThat(persistentProperty.getDataType().getName()).isEqualTo(Name.COUNTER);
assertThat(persistentProperty.findAnnotation(CassandraType.class)).isNotNull();
}
private CassandraPersistentProperty getPropertyFor(Class<?> type, String fieldName) {
Field field = ReflectionUtils.findField(type, fieldName);
return new BasicCassandraPersistentProperty(field, null, getEntity(type), new CassandraSimpleTypeHolder());
}
private <T> BasicCassandraPersistentEntity<T> getEntity(Class<T> type) {
return new BasicCassandraPersistentEntity<T>(ClassTypeInformation.from(type));
}
static class Timeline {
@PrimaryKey String id;
@@ -39,37 +121,53 @@ public class BasicCassandraPersistentPropertyUnitTests {
Date time;
@Column("message") String text;
}
CassandraPersistentEntity<Timeline> entity;
@Retention(RetentionPolicy.RUNTIME)
@Column(forceQuote = true)
@interface ComposedColumnAnnotation {
@Before
public void setup() {
entity = new BasicCassandraPersistentEntity<Timeline>(ClassTypeInformation.from(Timeline.class));
@AliasFor(annotation = Column.class)
String value();
}
@Test
public void usesAnnotatedColumnName() {
@Retention(RetentionPolicy.RUNTIME)
@PrimaryKeyColumn(forceQuote = true)
@interface ComposedPrimaryKeyColumnAnnotation {
Field field = ReflectionUtils.findField(Timeline.class, "text");
assertThat(getPropertyFor(field).getColumnName().toCql()).isEqualTo("message");
@AliasFor(annotation = PrimaryKeyColumn.class)
String value();
@AliasFor(annotation = PrimaryKeyColumn.class)
int ordinal() default 42;
}
@Test
public void checksIdProperty() {
Field field = ReflectionUtils.findField(Timeline.class, "id");
CassandraPersistentProperty property = getPropertyFor(field);
assertThat(property.isIdProperty()).isTrue();
@Retention(RetentionPolicy.RUNTIME)
@PrimaryKey(forceQuote = true)
@interface ComposedPrimaryKeyAnnotation {
@AliasFor(annotation = PrimaryKey.class)
String value() default "primary-key";
}
@Test
public void returnsPropertyNameForUnannotatedProperty() {
Field field = ReflectionUtils.findField(Timeline.class, "time");
assertThat(getPropertyFor(field).getColumnName().toCql()).isEqualTo("time");
@Retention(RetentionPolicy.RUNTIME)
@CassandraType(type = Name.COUNTER)
@interface ComposedCassandraTypeAnnotation {
}
private CassandraPersistentProperty getPropertyFor(Field field) {
return new BasicCassandraPersistentProperty(field, null, entity, new CassandraSimpleTypeHolder());
static class TypeWithComposedColumnAnnotation {
@ComposedColumnAnnotation("mycolumn") String column;
}
static class TypeWithComposedPrimaryKeyColumnAnnotation {
@ComposedPrimaryKeyColumnAnnotation("mycolumn") String column;
}
static class TypeWithComposedPrimaryKeyAnnotation {
@ComposedPrimaryKeyAnnotation String column;
}
static class TypeWithComposedCassandraTypeAnnotation {
@ComposedCassandraTypeAnnotation String column;
}
}

View File

@@ -17,11 +17,17 @@ package org.springframework.data.cassandra.mapping;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.util.ClassTypeInformation;
/**
@@ -82,6 +88,17 @@ public class CassandraUserTypePersistentEntityUnitTests {
assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("UpperCase", true));
}
/**
* @see DATACASS-259
*/
@Test
public void shouldConsiderComposedUserDefinedTypeAnnotation() {
CassandraUserTypePersistentEntity<TypeWithComposedAnnotation> type = getEntity(TypeWithComposedAnnotation.class);
assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("mytype", true));
}
private <T> CassandraUserTypePersistentEntity<T> getEntity(Class<T> entityClass) {
return new CassandraUserTypePersistentEntity<T>(ClassTypeInformation.from(entityClass), mappingContextMock, null,
userTypeResolverMock);
@@ -95,4 +112,16 @@ public class CassandraUserTypePersistentEntityUnitTests {
@UserDefinedType(value = "UpperCase", forceQuote = true)
static class WithForceQuote {}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@UserDefinedType(forceQuote = true)
@interface ComposedUserDefinedTypeAnnotation {
@AliasFor(annotation = UserDefinedType.class)
String value() default "mytype";
}
@ComposedUserDefinedTypeAnnotation()
static class TypeWithComposedAnnotation {}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.data.cassandra.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import org.junit.Test;
@@ -86,6 +88,18 @@ public class CassandraParametersUnitTests {
assertThat(cassandraParameters.getParameter(0).getCassandraType().type()).isEqualTo(Name.TIME);
}
/**
* @see DATACASS-296
*/
@Test
public void shouldReturnTypeForComposedAnnotationType() throws Exception {
Method method = PersonRepository.class.getMethod("findByComposedAnnotationObject", Object.class);
CassandraParameters cassandraParameters = new CassandraParameters(method);
assertThat(cassandraParameters.getParameter(0).getCassandraType().type()).isEqualTo(Name.BOOLEAN);
}
interface PersonRepository {
Person findByFirstname(String firstname);
@@ -95,5 +109,12 @@ public class CassandraParametersUnitTests {
Person findByObject(Object firstname);
Person findByAnnotatedObject(@CassandraType(type = Name.TIME) Object firstname);
Person findByComposedAnnotationObject(@ComposedCassandraTypeAnnotation Object firstname);
}
@Retention(RetentionPolicy.RUNTIME)
@CassandraType(type = Name.BOOLEAN)
@interface ComposedCassandraTypeAnnotation {
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.cassandra.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
@@ -125,6 +127,21 @@ public class StringBasedCassandraQueryUnitTests {
assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';");
}
/**
* @see DATACASS-259
*/
@Test
public void bindsIndexParameterForComposedQueryAnnotationCorrectly() {
StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByComposedQueryAnnotation", String.class);
CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(
cassandraQuery.getQueryMethod(), "Matthews");
String actual = cassandraQuery.createQuery(accessor);
assertThat(actual).isEqualTo("SELECT * FROM person WHERE lastname = 'Matthews';");
}
/**
* @see DATACASS-117
*/
@@ -462,5 +479,13 @@ public class StringBasedCassandraQueryUnitTests {
@Query("SELECT * FROM person WHERE address=?0;")
Person findByMainAddress(UDTValue udtValue);
@ComposedQueryAnnotation
Person findByComposedQueryAnnotation(String lastname);
}
@Retention(RetentionPolicy.RUNTIME)
@Query("SELECT * FROM person WHERE lastname = ?0;")
@interface ComposedQueryAnnotation {
}
}