package org.springframework.data.mapping.model;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotatedElementUtils;
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.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.Version;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public abstract class AnnotationBasedPersistentProperty<P extends PersistentProperty<P>>
extends AbstractPersistentProperty<P> {
private static final String SPRING_DATA_PACKAGE = "org.springframework.data";
private final @Nullable String value;
private final Map<Class<? extends Annotation>, Optional<? extends Annotation>> annotationCache = new ConcurrentHashMap<>();
private final Lazy<Boolean> usePropertyAccess = Lazy.of(() -> {
AccessType accessType = findPropertyOrOwnerAnnotation(AccessType.class);
return accessType != null && Type.PROPERTY.equals(accessType.value()) || super.usePropertyAccess();
});
private final Lazy<Boolean> isTransient = Lazy.of(() -> super.isTransient() || isAnnotationPresent(Transient.class)
|| isAnnotationPresent(Value.class) || isAnnotationPresent(Autowired.class));
private final Lazy<Boolean> isWritable = Lazy
.of(() -> !isTransient() && !isAnnotationPresent(ReadOnlyProperty.class));
private final Lazy<Boolean> isReference = Lazy.of(() -> !isTransient() && isAnnotationPresent(Reference.class));
private final Lazy<Boolean> isId = Lazy.of(() -> isAnnotationPresent(Id.class));
private final Lazy<Boolean> isVersion = Lazy.of(() -> isAnnotationPresent(Version.class));
public AnnotationBasedPersistentProperty(Property property, PersistentEntity<?, P> owner,
SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
populateAnnotationCache(property);
Value value = findAnnotation(Value.class);
this.value = value == null ? null : value.value();
}
private void populateAnnotationCache(Property property) {
Optionals.toStream(property.getGetter(), property.getSetter()).forEach(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class<? extends Annotation> annotationType = annotation.annotationType();
validateAnnotation(annotation,
"Ambiguous mapping! Annotation %s configured "
+ "multiple times on accessor methods of property %s in class %s!",
annotationType.getSimpleName(), getName(), getOwner().getType().getSimpleName());
annotationCache.put(annotationType,
Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(it, annotationType)));
}
});
property.getField().ifPresent(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class<? extends Annotation> annotationType = annotation.annotationType();
validateAnnotation(annotation,
"Ambiguous mapping! Annotation %s configured " + "on field %s and one of its accessor methods in class %s!",
annotationType.getSimpleName(), it.getName(), getOwner().getType().getSimpleName());
annotationCache.put(annotationType,
Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(it, annotationType)));
}
});
}
private void validateAnnotation(Annotation candidate, String message, Object... arguments) {
Class<? extends Annotation> annotationType = candidate.annotationType();
if (!annotationType.getName().startsWith(SPRING_DATA_PACKAGE)) {
return;
}
if (annotationCache.containsKey(annotationType)
&& !annotationCache.get(annotationType).equals(Optional.of(candidate))) {
throw new MappingException(String.format(message, arguments));
}
}
@Nullable
@Override
public String getSpelExpression() {
return value;
}
@Override
public boolean isTransient() {
return isTransient.get();
}
public boolean isIdProperty() {
return isId.get();
}
public boolean isVersionProperty() {
return isVersion.get();
}
@Override
public boolean isAssociation() {
return isReference.get();
}
@Override
public boolean isWritable() {
return isWritable.get();
}
@Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null!");
return doFindAnnotation(annotationType).orElse(null);
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
Optional<? extends Annotation> annotation = annotationCache.get(annotationType);
if (annotation != null) {
return (Optional<A>) annotation;
}
return (Optional<A>) annotationCache.computeIfAbsent(annotationType, type -> {
return getAccessors()
.map(it -> AnnotatedElementUtils.findMergedAnnotation(it, type))
.flatMap(StreamUtils::fromNullable)
.findFirst();
});
}
@Nullable
@Override
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
A annotation = findAnnotation(annotationType);
return annotation != null ? annotation : getOwner().findAnnotation(annotationType);
}
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return doFindAnnotation(annotationType).isPresent();
}
@Override
public boolean usePropertyAccess() {
return usePropertyAccess.get();
}
@Nullable
@Override
public Class<?> getAssociationTargetType() {
Reference reference = findAnnotation(Reference.class);
if (reference == null) {
return isEntity() ? getActualType() : null;
}
Class<?> targetType = reference.to();
return Class.class.equals(targetType)
? isEntity() ? getActualType() : null
: targetType;
}
@Override
public String toString() {
if (annotationCache.isEmpty()) {
populateAnnotationCache(getProperty());
}
String builder = annotationCache.values().stream()
.flatMap(Optionals::toStream)
.map(Object::toString)
.collect(Collectors.joining(" "));
return builder + super.toString();
}
private Stream<? extends AnnotatedElement> getAccessors() {
return Optionals.toStream(Optional.ofNullable(getGetter()), Optional.ofNullable(getSetter()),
Optional.ofNullable(getField()));
}
}