org.springframework.data.mapping.model.AnnotationBasedPersistentProperty Maven / Gradle / Ivy
/*
* Copyright 2011-2024 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
*
* https://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 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.ReflectionUtils;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Special {@link PersistentProperty} that takes annotations at a property into account.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
*/
public abstract class AnnotationBasedPersistentProperty>
extends AbstractPersistentProperty
{
private static final String SPRING_DATA_PACKAGE = "org.springframework.data";
private static final Class extends Annotation> IDENTITY_TYPE = loadIdentityType();
private final @Nullable String value;
private final Map, Optional extends Annotation>> annotationCache = new ConcurrentHashMap<>();
private final Lazy usePropertyAccess = Lazy.of(() -> {
AccessType accessType = findPropertyOrOwnerAnnotation(AccessType.class);
return accessType != null && Type.PROPERTY.equals(accessType.value()) || super.usePropertyAccess();
});
private final Lazy isTransient = Lazy.of(() -> super.isTransient() || isAnnotationPresent(Transient.class)
|| isAnnotationPresent(Value.class) || isAnnotationPresent(Autowired.class)
|| isAnnotationPresent(jakarta.persistence.Transient.class));
private final Lazy isWritable = Lazy
.of(() -> !isTransient() && !isAnnotationPresent(ReadOnlyProperty.class));
private final Lazy isReference = Lazy.of(() -> !isTransient() //
&& (isAnnotationPresent(Reference.class) || super.isAssociation()));
private final Lazy isId = Lazy
.of(() -> isAnnotationPresent(Id.class) || (IDENTITY_TYPE != null && isAnnotationPresent(IDENTITY_TYPE))
|| isAnnotationPresent(jakarta.persistence.Id.class));
private final Lazy isVersion = Lazy.of(() -> isAnnotationPresent(Version.class)
|| isAnnotationPresent(jakarta.persistence.Version.class));
private final Lazy> associationTargetType = Lazy.of(() -> {
if (!isAssociation()) {
return null;
}
return Optional.of(Reference.class) //
.map(this::findAnnotation) //
.map(Reference::to) //
.map(it -> !Class.class.equals(it) ? TypeInformation.of(it) : getActualTypeInformation()) //
.orElseGet(() -> super.getAssociationTargetTypeInformation());
});
/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
*
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
*/
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();
}
/**
* Populates the annotation cache by eagerly accessing the annotations directly annotated to the accessors (if
* available) and the backing field. Annotations override annotations found on field.
*
* @param property
* @throws MappingException in case we find an ambiguous mapping on the accessor methods
*/
private void populateAnnotationCache(Property property) {
Optionals.toStream(property.getGetter(), property.getSetter()).forEach(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class extends Annotation> annotationType = annotation.annotationType();
Annotation mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(it, annotationType);
validateAnnotation(mergedAnnotation,
"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.of(mergedAnnotation));
}
});
property.getField().ifPresent(it -> {
for (Annotation annotation : it.getAnnotations()) {
Class extends Annotation> annotationType = annotation.annotationType();
Annotation mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(it, annotationType);
validateAnnotation(mergedAnnotation,
"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.of(mergedAnnotation));
}
});
}
/**
* Verifies the given annotation candidate detected. Will be rejected if it's a Spring Data annotation and we already
* found another one with a different configuration setup (i.e. other attribute values).
*
* @param candidate must not be {@literal null}.
* @param message must not be {@literal null}.
* @param arguments must not be {@literal null}.
*/
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));
}
}
/**
* Inspects a potentially available {@link Value} annotation at the property and returns the {@link String} value of
* it.
*
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#getSpelExpression()
*/
@Nullable
@Override
public String getSpelExpression() {
return value;
}
/**
* Considers plain transient fields, fields annotated with {@link Transient}, {@link Value} or {@link Autowired} as
* transient.
*
* @see org.springframework.data.mapping.PersistentProperty#isTransient()
*/
@Override
public boolean isTransient() {
return isTransient.get();
}
@Override
public boolean isIdProperty() {
return isId.get();
}
@Override
public boolean isVersionProperty() {
return isVersion.get();
}
/**
* Considers the property an {@link Association} if it is annotated with {@link Reference}.
*/
@Override
public boolean isAssociation() {
return isReference.get();
}
@Override
public boolean isWritable() {
return isWritable.get();
}
/**
* Returns the annotation found for the current {@link AnnotationBasedPersistentProperty}. Will prefer getters or
* setters annotations over ones found at the backing field as the former can be used to reconfigure the metadata in
* subclasses.
*
* @param annotationType must not be {@literal null}.
* @return {@literal null} if annotation type not found on property.
*/
@Override
@Nullable
public A findAnnotation(Class annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
return doFindAnnotation(annotationType).orElse(null);
}
@SuppressWarnings("unchecked")
private Optional doFindAnnotation(Class annotationType) {
Optional extends Annotation> annotation = annotationCache.get(annotationType);
if (annotation != null) {
return (Optional) annotation;
}
return (Optional) annotationCache.computeIfAbsent(annotationType, type -> {
return getAccessors() //
.map(it -> AnnotatedElementUtils.findMergedAnnotation(it, type)) //
.flatMap(StreamUtils::fromNullable) //
.findFirst();
});
}
@Nullable
@Override
public A findPropertyOrOwnerAnnotation(Class annotationType) {
A annotation = findAnnotation(annotationType);
return annotation != null ? annotation : getOwner().findAnnotation(annotationType);
}
/**
* Returns whether the property carries the an annotation of the given type.
*
* @param annotationType the annotation type to look up.
* @return
*/
@Override
public boolean isAnnotationPresent(Class extends Annotation> annotationType) {
return doFindAnnotation(annotationType).isPresent();
}
@Override
public boolean usePropertyAccess() {
return usePropertyAccess.get();
}
@Nullable
@Override
public TypeInformation> getAssociationTargetTypeInformation() {
return associationTargetType.getNullable();
}
@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()));
}
/**
* Load jMolecules' {@code @Identity} type if present on the classpath. Dedicated method instead of a simple static
* initializer to be able to suppress the compiler warning.
*
* @return can be {@literal null}.
*/
@Nullable
@SuppressWarnings("unchecked")
private static Class extends Annotation> loadIdentityType() {
return (Class extends Annotation>) ReflectionUtils.loadIfPresent("org.jmolecules.ddd.annotation.Identity",
AbstractPersistentProperty.class.getClassLoader());
}
}