/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mapping.model;
import static java.lang.annotation.ElementType.*;
import static org.assertj.core.api.Assertions.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.AccessType;
import org.springframework.data.annotation.AccessType.Type;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Unit tests for {@link AnnotationBasedPersistentProperty}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBasedPersistentProperty<P>> {
BasicPersistentEntity<Object, SamplePersistentProperty> entity;
SampleMappingContext context;
@Before
public void setUp() {
context = new SampleMappingContext();
entity = context.getRequiredPersistentEntity(Sample.class);
}
@Test // DATACMNS-269
public void discoversAnnotationOnField() {
assertAnnotationPresent(MyAnnotation.class, entity.getPersistentProperty("field"));
}
@Test // DATACMNS-269
public void discoversAnnotationOnGetters() {
assertAnnotationPresent(MyAnnotation.class, entity.getPersistentProperty("getter"));
}
@Test // DATACMNS-269
public void discoversAnnotationOnSetters() {
assertAnnotationPresent(MyAnnotation.class, entity.getPersistentProperty("setter"));
}
@Test // DATACMNS-269
public void findsMetaAnnotation() {
assertAnnotationPresent(MyId.class, entity.getPersistentProperty("id"));
assertAnnotationPresent(Id.class, entity.getPersistentProperty("id"));
}
@Test // DATACMNS-282
public void populatesAnnotationCacheWithDirectAnnotationsOnCreation() {
assertThat(entity.getPersistentProperty("meta")).hasValueSatisfying(property -> {
// Assert direct annotations are cached on construction
Map<Class<? extends Annotation>, Annotation> cache = getAnnotationCache(property);
assertThat(cache.containsKey(MyAnnotationAsMeta.class)).isTrue();
assertThat(cache.containsKey(MyAnnotation.class)).isFalse();
// Assert meta annotation is found and cached
assertThat(property.findAnnotation(MyAnnotation.class)).hasValueSatisfying(annotation -> assertThat(cache.containsKey(MyAnnotation.class)).isTrue());
});
}
@Test // DATACMNS-282
public void discoversAmbiguousMappingUsingDirectAnnotationsOnAccessors() {
try {
context.getPersistentEntity(InvalidSample.class);
fail("Expected MappingException!");
} catch (MappingException o_O) {
assertThat(context.hasPersistentEntityFor(InvalidSample.class)).isFalse();
}
}
@Test // DATACMNS-243
public void defaultsToFieldAccess() {
assertThat(getProperty(FieldAccess.class, "name")).hasValueSatisfying(it -> assertThat(it.usePropertyAccess()).isFalse());
}
@Test // DATACMNS-243
public void usesAccessTypeDeclaredOnTypeAsDefault() {
assertThat(getProperty(PropertyAccess.class, "firstname")).hasValueSatisfying(it -> assertThat(it.usePropertyAccess()).isTrue());
}
@Test // DATACMNS-243
public void propertyAnnotationOverridesTypeConfiguration() {
assertThat(getProperty(PropertyAccess.class, "lastname")).hasValueSatisfying(it -> assertThat(it.usePropertyAccess()).isFalse());
}
@Test // DATACMNS-243
public void fieldAnnotationOverridesTypeConfiguration() {
assertThat(getProperty(PropertyAccess.class, "emailAddress")).hasValueSatisfying(it -> assertThat(it.usePropertyAccess()).isFalse());
}
@Test // DATACMNS-243
public void doesNotRejectSameAnnotationIfItsEqualOnBothFieldAndAccessor() {
context.getPersistentEntity(AnotherInvalidSample.class);
}
@Test // DATACMNS-534
public void treatsNoAnnotationCorrectly() {
assertThat(getProperty(ClassWithReadOnlyProperties.class, "noAnnotations")).hasValueSatisfying(it -> assertThat(it.isWritable()).isTrue());
}
@Test // DATACMNS-534
public void treatsTransientAsNotExisting() {
assertThat(getProperty(ClassWithReadOnlyProperties.class, "transientProperty")).isEmpty();
}
@Test // DATACMNS-534
public void treatsReadOnlyAsNonWritable() {
assertThat(getProperty(ClassWithReadOnlyProperties.class, "readOnlyProperty")).hasValueSatisfying(it -> assertThat(it.isWritable()).isFalse());
}
@Test // DATACMNS-534
public void considersPropertyWithReadOnlyMetaAnnotationReadOnly() {
assertThat(getProperty(ClassWithReadOnlyProperties.class, "customReadOnlyProperty")).hasValueSatisfying(it -> assertThat(it.isWritable()).isFalse());
}
@Test // DATACMNS-556
public void doesNotRejectNonSpringDataAnnotationsUsedOnBothFieldAndAccessor() {
getProperty(TypeWithCustomAnnotationsOnBothFieldAndAccessor.class, "field");
}
@Test // DATACMNS-677
@SuppressWarnings("unchecked")
public void cachesNonPresenceOfAnnotationOnField() {
Optional<SamplePersistentProperty> property = getProperty(Sample.class, "getterWithoutField");
assertThat(property).hasValueSatisfying(it -> {
assertThat(it.findAnnotation(MyAnnotation.class)).isNotPresent();
Map<Class<?>, ?> field = (Map<Class<?>, ?>) ReflectionTestUtils.getField(it, "annotationCache");
assertThat(field.containsKey(MyAnnotation.class)).isTrue();
assertThat(field.get(MyAnnotation.class)).isEqualTo(Optional.empty());
});
}
@Test // DATACMNS-825
public void composedAnnotationWithAliasForGetCachedCorrectly() {
assertThat(entity.getPersistentProperty("metaAliased")).hasValueSatisfying(property -> {
// Assert direct annotations are cached on construction
Map<Class<? extends Annotation>, Annotation> cache = getAnnotationCache(property);
assertThat(cache.containsKey(MyComposedAnnotationUsingAliasFor.class)).isTrue();
assertThat(cache.containsKey(MyAnnotation.class)).isFalse();
// Assert meta annotation is found and cached
assertThat(property.findAnnotation(MyAnnotation.class)).hasValueSatisfying(it -> assertThat(cache.containsKey(MyAnnotation.class)).isTrue());
});
}
@Test // DATACMNS-825
public void composedAnnotationWithAliasShouldHaveSynthesizedAttributeValues() {
assertThat(entity.getPersistentProperty("metaAliased")).hasValueSatisfying(property -> assertThat(property.findAnnotation(MyAnnotation.class)).hasValueSatisfying(annotation -> assertThat(AnnotationUtils.getValue(annotation)).isEqualTo("spring")));
}
@Test // DATACMNS-867
public void revisedAnnotationWithAliasShouldHaveSynthesizedAttributeValues() {
assertThat(entity.getPersistentProperty("setter")).hasValueSatisfying(property -> assertThat(property.findAnnotation(RevisedAnnnotationWithAliasFor.class)).hasValueSatisfying(annotation -> {
assertThat(annotation.name()).isEqualTo("my-value");
assertThat(annotation.value()).isEqualTo("my-value");
}));
}
@SuppressWarnings("unchecked")
private Map<Class<? extends Annotation>, Annotation> getAnnotationCache(SamplePersistentProperty property) {
return (Map<Class<? extends Annotation>, Annotation>) ReflectionTestUtils.getField(property, "annotationCache");
}
private <A extends Annotation> A assertAnnotationPresent(Class<A> annotationType,
Optional<? extends AnnotationBasedPersistentProperty<?>> property) {
Optional<A> annotation = property.flatMap(it -> it.findAnnotation(annotationType));
assertThat(annotation).isPresent();
return annotation.get();
}
private Optional<SamplePersistentProperty> getProperty(Class<?> type, String name) {
return context.getRequiredPersistentEntity(type).getPersistentProperty(name);
}
static class Sample {
@MyId String id;
@MyAnnotation String field;
String getter;
@RevisedAnnnotationWithAliasFor(value = "my-value") String setter;
String doubleMapping;
@MyAnnotationAsMeta String meta;
@MyComposedAnnotationUsingAliasFor String metaAliased;
@MyAnnotation
public String getGetter() {
return getter;
}
@MyAnnotation
public void setSetter(String setter) {
this.setter = setter;
}
@MyAnnotation
public String getDoubleMapping() {
return doubleMapping;
}
@MyAnnotation
public void setDoubleMapping(String doubleMapping) {
this.doubleMapping = doubleMapping;
}
@AccessType(Type.PROPERTY)
public Object getGetterWithoutField() {
return null;
}
public void setGetterWithoutField(Object object) {}
}
static class InvalidSample {
String meta;
@MyAnnotation
public String getMeta() {
return meta;
}
@MyAnnotation("foo")
public void setMeta(String meta) {
this.meta = meta;
}
}
static class AnotherInvalidSample {
@MyAnnotation String property;
@MyAnnotation
public String getProperty() {
return property;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface MyAnnotation {
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD })
@MyAnnotation
public @interface MyAnnotationAsMeta {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD })
@MyAnnotation
public @interface MyComposedAnnotationUsingAliasFor {
@AliasFor(annotation = MyAnnotation.class, attribute = "value")
String name() default "spring";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD })
@interface RevisedAnnnotationWithAliasFor {
@AliasFor("value")
String name() default "";
@AliasFor("name")
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
@Id
public @interface MyId {
}
static class FieldAccess {
String name;
}
@AccessType(Type.PROPERTY)
static class PropertyAccess {
String firstname, lastname;
@AccessType(Type.FIELD) String emailAddress;
public String getFirstname() {
return firstname;
}
@AccessType(Type.FIELD)
public String getLastname() {
return lastname;
}
}
static class ClassWithReadOnlyProperties {
String noAnnotations;
@Transient String transientProperty;
@ReadOnlyProperty String readOnlyProperty;
@CustomReadOnly String customReadOnlyProperty;
}
static class TypeWithCustomAnnotationsOnBothFieldAndAccessor {
@Nullable String field;
@Nullable
public String getField() {
return field;
}
}
@ReadOnlyProperty
@Retention(RetentionPolicy.RUNTIME)
@Target(FIELD)
@interface CustomReadOnly {
}
}