package org.springframework.data.mapping.model;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.annotation.Immutable;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mapping.*;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.support.PersistableIsNewStrategy;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implements MutablePersistentEntity<T, P> {
private static final String TYPE_MISMATCH = "Target bean of type %s is not of type of the persistent entity (%s)!";
private final @Nullable PreferredConstructor<T, P> constructor;
private final TypeInformation<T> information;
private final List<P> properties;
private final List<P> persistentPropertiesCache;
private final @Nullable Comparator<P> comparator;
private final Set<Association<P>> associations;
private final Map<String, P> propertyCache;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
private final MultiValueMap<Class<? extends Annotation>, P> propertyAnnotationCache;
private @Nullable P idProperty;
private @Nullable P versionProperty;
private PersistentPropertyAccessorFactory propertyAccessorFactory;
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
private final Lazy<Alias> typeAlias;
private final Lazy<IsNewStrategy> isNewStrategy;
private final Lazy<Boolean> isImmutable;
private final Lazy<Boolean> requiresPropertyPopulation;
public BasicPersistentEntity(TypeInformation<T> information) {
this(information, null);
}
public BasicPersistentEntity(TypeInformation<T> information, @Nullable Comparator<P> comparator) {
Assert.notNull(information, "Information must not be null!");
this.information = information;
this.properties = new ArrayList<>();
this.persistentPropertiesCache = new ArrayList<>();
this.comparator = comparator;
this.constructor = PreferredConstructorDiscoverer.discover(this);
this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator));
this.propertyCache = new HashMap<>(16, 1f);
this.annotationCache = new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK);
this.propertyAnnotationCache = CollectionUtils
.toMultiValueMap(new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK));
this.propertyAccessorFactory = BeanWrapperPropertyAccessorFactory.INSTANCE;
this.typeAlias = Lazy.of(() -> getAliasFromAnnotation(getType()));
this.isNewStrategy = Lazy.of(() -> Persistable.class.isAssignableFrom(information.getType())
? PersistableIsNewStrategy.INSTANCE
: getFallbackIsNewStrategy());
this.isImmutable = Lazy.of(() -> isAnnotationPresent(Immutable.class));
this.requiresPropertyPopulation = Lazy.of(() -> !isImmutable() && properties.stream()
.anyMatch(it -> !(isConstructorArgument(it) || it.isTransient())));
}
@Nullable
public PreferredConstructor<T, P> getPersistenceConstructor() {
return constructor;
}
public boolean isConstructorArgument(PersistentProperty<?> property) {
return constructor != null && constructor.isConstructorParameter(property);
}
public boolean isIdProperty(PersistentProperty<?> property) {
return idProperty != null && idProperty.equals(property);
}
public boolean isVersionProperty(PersistentProperty<?> property) {
return versionProperty != null && versionProperty.equals(property);
}
public String getName() {
return getType().getName();
}
@Nullable
public P getIdProperty() {
return idProperty;
}
@Nullable
public P getVersionProperty() {
return versionProperty;
}
public boolean hasIdProperty() {
return idProperty != null;
}
public boolean hasVersionProperty() {
return versionProperty != null;
}
public void addPersistentProperty(P property) {
Assert.notNull(property, "Property must not be null!");
if (properties.contains(property)) {
return;
}
properties.add(property);
if (!property.isTransient() && !property.isAssociation()) {
persistentPropertiesCache.add(property);
}
propertyCache.computeIfAbsent(property.getName(), key -> property);
P candidate = returnPropertyIfBetterIdPropertyCandidateOrNull(property);
if (candidate != null) {
this.idProperty = candidate;
}
if (property.isVersionProperty()) {
P versionProperty = this.versionProperty;
if (versionProperty != null) {
throw new MappingException(
String.format(
"Attempt to add version property %s but already have property %s registered "
+ "as version. Check your mapping configuration!",
property.getField(), versionProperty.getField()));
}
this.versionProperty = property;
}
}
@Override
public void setEvaluationContextProvider(EvaluationContextProvider provider) {
this.evaluationContextProvider = provider;
}
@Nullable
protected P returnPropertyIfBetterIdPropertyCandidateOrNull(P property) {
if (!property.isIdProperty()) {
return null;
}
P idProperty = this.idProperty;
if (idProperty != null) {
throw new MappingException(String.format("Attempt to add id property %s but already have property %s registered "
+ "as id. Check your mapping configuration!", property.getField(), idProperty.getField()));
}
return property;
}
public void addAssociation(Association<P> association) {
Assert.notNull(association, "Association must not be null!");
associations.add(association);
}
@Override
@Nullable
public P getPersistentProperty(String name) {
return propertyCache.get(name);
}
@Override
public Iterable<P> getPersistentProperties(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null!");
return propertyAnnotationCache.computeIfAbsent(annotationType, this::doFindPersistentProperty);
}
private List<P> doFindPersistentProperty(Class<? extends Annotation> annotationType) {
List<P> annotatedProperties = properties.stream()
.filter(it -> it.isAnnotationPresent(annotationType))
.collect(Collectors.toList());
if (!annotatedProperties.isEmpty()) {
return annotatedProperties;
}
return associations.stream()
.map(Association::getInverse)
.filter(it -> it.isAnnotationPresent(annotationType)).collect(Collectors.toList());
}
public Class<T> getType() {
return information.getType();
}
public Alias getTypeAlias() {
return typeAlias.get();
}
public TypeInformation<T> getTypeInformation() {
return information;
}
public void doWithProperties(PropertyHandler<P> handler) {
Assert.notNull(handler, "PropertyHandler must not be null!");
for (P property : persistentPropertiesCache) {
handler.doWithPersistentProperty(property);
}
}
@Override
public void doWithProperties(SimplePropertyHandler handler) {
Assert.notNull(handler, "Handler must not be null!");
for (PersistentProperty<?> property : persistentPropertiesCache) {
handler.doWithPersistentProperty(property);
}
}
public void doWithAssociations(AssociationHandler<P> handler) {
Assert.notNull(handler, "Handler must not be null!");
for (Association<P> association : associations) {
handler.doWithAssociation(association);
}
}
public void doWithAssociations(SimpleAssociationHandler handler) {
Assert.notNull(handler, "Handler must not be null!");
for (Association<? extends PersistentProperty<?>> association : associations) {
handler.doWithAssociation(association);
}
}
@Nullable
@Override
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return doFindAnnotation(annotationType).orElse(null);
}
@Override
public <A extends Annotation> boolean isAnnotationPresent(Class<A> annotationType) {
return doFindAnnotation(annotationType).isPresent();
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(getType(), it)));
}
public void verify() {
if (comparator != null) {
properties.sort(comparator);
persistentPropertiesCache.sort(comparator);
}
}
@Override
public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) {
this.propertyAccessorFactory = factory;
}
@Override
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
verifyBeanType(bean);
return propertyAccessorFactory.getPropertyAccessor(this, bean);
}
@Override
public IdentifierAccessor getIdentifierAccessor(Object bean) {
verifyBeanType(bean);
if (Persistable.class.isAssignableFrom(getType())) {
return new PersistableIdentifierAccessor((Persistable<?>) bean);
}
return hasIdProperty() ? new IdPropertyIdentifierAccessor(this, bean) : new AbsentIdentifierAccessor(bean);
}
@Override
public boolean isNew(Object bean) {
verifyBeanType(bean);
return isNewStrategy.get().isNew(bean);
}
@Override
public boolean isImmutable() {
return isImmutable.get();
}
@Override
public boolean requiresPropertyPopulation() {
return requiresPropertyPopulation.get();
}
@Override
public Iterator<P> iterator() {
Iterator<P> iterator = properties.iterator();
return new Iterator<P>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public P next() {
return iterator.next();
}
};
}
protected EvaluationContext getEvaluationContext(Object rootObject) {
return evaluationContextProvider.getEvaluationContext(rootObject);
}
protected IsNewStrategy getFallbackIsNewStrategy() {
return PersistentEntityIsNewStrategy.of(this);
}
private final void verifyBeanType(Object bean) {
Assert.notNull(bean, "Target bean must not be null!");
Assert.isInstanceOf(getType(), bean,
() -> String.format(TYPE_MISMATCH, bean.getClass().getName(), getType().getName()));
}
private static Alias getAliasFromAnnotation(Class<?> type) {
Optional<String> typeAliasValue = Optional
.ofNullable(AnnotatedElementUtils.findMergedAnnotation(type, TypeAlias.class))
.map(TypeAlias::value)
.filter(StringUtils::hasText);
return Alias.ofNullable(typeAliasValue.orElse(null));
}
private static class AbsentIdentifierAccessor extends TargetAwareIdentifierAccessor {
public AbsentIdentifierAccessor(Object target) {
super(target);
}
@Override
@Nullable
public Object getIdentifier() {
return null;
}
}
@RequiredArgsConstructor
private static final class AssociationComparator<P extends PersistentProperty<P>>
implements Comparator<Association<P>>, Serializable {
private static final long serialVersionUID = 4508054194886854513L;
private final @NonNull Comparator<P> delegate;
public int compare(@Nullable Association<P> left, @Nullable Association<P> right) {
if (left == null) {
throw new IllegalArgumentException("Left argument must not be null!");
}
if (right == null) {
throw new IllegalArgumentException("Right argument must not be null!");
}
return delegate.compare(left.getInverse(), right.getInverse());
}
}
}