/*
* Copyright 2011-2017 by the original author(s).
*
* 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.context;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;
import org.springframework.util.StringUtils;
/**
* Base class to build mapping metadata and thus create instances of {@link PersistentEntity} and
* {@link PersistentProperty}.
* <p>
* The implementation uses a {@link ReentrantReadWriteLock} to make sure {@link PersistentEntity} are completely
* populated before accessing them from outside.
*
* @param E the concrete {@link PersistentEntity} type the {@link MappingContext} implementation creates
* @param P the concrete {@link PersistentProperty} type the {@link MappingContext} implementation creates
* @author Jon Brisbin
* @author Oliver Gierke
* @author Michael Hunger
* @author Thomas Darimont
* @author Tomasz Wysocki
* @author Mark Paluch
* @author Mikael Klamra
* @author Christoph Strobl
*/
public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?, P>, P extends PersistentProperty<P>>
implements MappingContext<E, P>, ApplicationEventPublisherAware, InitializingBean {
private final Optional<E> NONE = Optional.empty();
private final Map<TypeInformation<?>, Optional<E>> persistentEntities = new HashMap<>();
private final PersistentPropertyAccessorFactory persistentPropertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory();
private ApplicationEventPublisher applicationEventPublisher;
private Set<? extends Class<?>> initialEntitySet = new HashSet<>();
private boolean strict = false;
private SimpleTypeHolder simpleTypeHolder = SimpleTypeHolder.DEFAULT;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher)
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Sets the {@link Set} of types to populate the context initially.
*
* @param initialEntitySet
*/
public void setInitialEntitySet(Set<? extends Class<?>> initialEntitySet) {
this.initialEntitySet = initialEntitySet;
}
/**
* Configures whether the {@link MappingContext} is in strict mode which means, that it will throw
* {@link MappingException}s in case one tries to lookup a {@link PersistentEntity} not already in the context. This
* defaults to {@literal false} so that unknown types will be transparently added to the MappingContext if not known
* in advance.
*
* @param strict
*/
public void setStrict(boolean strict) {
this.strict = strict;
}
/**
* Configures the {@link SimpleTypeHolder} to be used by the {@link MappingContext}. Allows customization of what
* types will be regarded as simple types and thus not recursively analysed.
*
* @param simpleTypes must not be {@literal null}.
*/
public void setSimpleTypeHolder(SimpleTypeHolder simpleTypes) {
Assert.notNull(simpleTypes, "SimpleTypeHolder must not be null!");
this.simpleTypeHolder = simpleTypes;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.MappingContext#getPersistentEntities()
*/
public Collection<E> getPersistentEntities() {
try {
read.lock();
return persistentEntities.values().stream()//
.flatMap(Optionals::toStream)//
.collect(Collectors.toSet());
} finally {
read.unlock();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(java.lang.Class)
*/
public Optional<E> getPersistentEntity(Class<?> type) {
return getPersistentEntity(ClassTypeInformation.from(type));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getRequiredPersistentEntity(java.lang.Class)
*/
@Override
public E getRequiredPersistentEntity(Class<?> type) {
return getPersistentEntity(type).orElseThrow(
() -> new IllegalArgumentException(String.format("Couldn't find PersistentEntity for type %s!", type)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#hasPersistentEntityFor(java.lang.Class)
*/
@Override
public boolean hasPersistentEntityFor(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return persistentEntities.containsKey(ClassTypeInformation.from(type));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation)
*/
public Optional<E> getPersistentEntity(TypeInformation<?> type) {
Assert.notNull(type, "Type must not be null!");
try {
read.lock();
Optional<E> entity = persistentEntities.get(type);
if (entity != null) {
return entity;
}
} finally {
read.unlock();
}
if (!shouldCreatePersistentEntityFor(type)) {
try {
write.lock();
persistentEntities.put(type, NONE);
} finally {
write.unlock();
}
return NONE;
}
if (strict) {
throw new MappingException("Unknown persistent entity " + type);
}
return addPersistentEntity(type);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getRequiredPersistentEntity(org.springframework.data.util.TypeInformation)
*/
@Override
public E getRequiredPersistentEntity(TypeInformation<?> type) {
return getPersistentEntity(type)
.orElseThrow(() -> new MappingException(String.format("Couldn't find PersistentEntity for type %s!", type)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getPersistentEntity(org.springframework.data.mapping.PersistentProperty)
*/
public Optional<E> getPersistentEntity(P persistentProperty) {
Assert.notNull(persistentProperty, "PersistentProperty must not be null!");
TypeInformation<?> typeInfo = persistentProperty.getTypeInformation();
return getPersistentEntity(typeInfo.getActualType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getRequiredPersistentEntity(org.springframework.data.mapping.PersistentProperty)
*/
@Override
public E getRequiredPersistentEntity(P persistentProperty) {
return getPersistentEntity(persistentProperty).orElseThrow(() -> new IllegalArgumentException(
String.format("Couldn't find PersistentEntity for type %s!", persistentProperty)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getPersistentPropertyPath(java.lang.Class, java.lang.String)
*/
public PersistentPropertyPath<P> getPersistentPropertyPath(PropertyPath propertyPath) {
Assert.notNull(propertyPath, "Property path must not be null!");
return getPersistentPropertyPath(propertyPath.toDotPath(), propertyPath.getOwningType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getPersistentPropertyPath(java.lang.String, java.lang.Class)
*/
public PersistentPropertyPath<P> getPersistentPropertyPath(String propertyPath, Class<?> type) {
Assert.notNull(propertyPath, "Property path must not be null!");
Assert.notNull(type, "Type must not be null!");
return getPersistentPropertyPath(propertyPath, ClassTypeInformation.from(type));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.MappingContext#getPersistentPropertyPath(org.springframework.data.mapping.context.InvalidPersistentPropertyPath)
*/
@Override
public PersistentPropertyPath<P> getPersistentPropertyPath(InvalidPersistentPropertyPath invalidPath) {
return getPersistentPropertyPath(invalidPath.getResolvedPath(), invalidPath.getType());
}
private PersistentPropertyPath<P> getPersistentPropertyPath(String propertyPath, TypeInformation<?> type) {
return getPersistentPropertyPath(Arrays.asList(propertyPath.split("\\.")), type);
}
/**
* Creates a {@link PersistentPropertyPath} for the given parts and {@link TypeInformation}.
*
* @param parts must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @return
*/
private PersistentPropertyPath<P> getPersistentPropertyPath(Collection<String> parts, TypeInformation<?> type) {
DefaultPersistentPropertyPath<P> path = DefaultPersistentPropertyPath.empty();
Iterator<String> iterator = parts.iterator();
E current = getRequiredPersistentEntity(type);
while (iterator.hasNext()) {
String segment = iterator.next();
final DefaultPersistentPropertyPath<P> foo = path;
final E bar = current;
Pair<DefaultPersistentPropertyPath<P>, E> pair = getPair(path, iterator, segment, current).orElseThrow(() -> {
String source = StringUtils.collectionToDelimitedString(parts, ".");
String resolvedPath = foo.toDotPath();
return new InvalidPersistentPropertyPath(source, type, segment, resolvedPath,
String.format("No property %s found on %s!", segment, bar.getName()));
});
path = pair.getFirst();
current = pair.getSecond();
}
return path;
}
private Optional<Pair<DefaultPersistentPropertyPath<P>, E>> getPair(DefaultPersistentPropertyPath<P> path,
Iterator<String> iterator, String segment, E entity) {
Optional<P> persistentProperty = entity.getPersistentProperty(segment);
return persistentProperty.map(it -> {
TypeInformation<?> type = it.getTypeInformation().getActualType();
return Pair.of(path.append(it), iterator.hasNext() ? getRequiredPersistentEntity(type) : entity);
});
}
/**
* Adds the given type to the {@link MappingContext}.
*
* @param type must not be {@literal null}.
* @return
*/
protected Optional<E> addPersistentEntity(Class<?> type) {
return addPersistentEntity(ClassTypeInformation.from(type));
}
/**
* Adds the given {@link TypeInformation} to the {@link MappingContext}.
*
* @param typeInformation must not be {@literal null}.
* @return
*/
protected Optional<E> addPersistentEntity(TypeInformation<?> typeInformation) {
Assert.notNull(typeInformation, "TypeInformation must not be null!");
try {
read.lock();
Optional<E> persistentEntity = persistentEntities.get(typeInformation);
if (persistentEntity != null) {
return persistentEntity;
}
} finally {
read.unlock();
}
Class<?> type = typeInformation.getType();
try {
write.lock();
final E entity = createPersistentEntity(typeInformation);
// Eagerly cache the entity as we might have to find it during recursive lookups.
persistentEntities.put(typeInformation, Optional.of(entity));
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(type);
final Map<String, PropertyDescriptor> descriptors = new HashMap<>();
for (PropertyDescriptor descriptor : pds) {
descriptors.put(descriptor.getName(), descriptor);
}
try {
PersistentPropertyCreator persistentPropertyCreator = new PersistentPropertyCreator(entity, descriptors);
ReflectionUtils.doWithFields(type, persistentPropertyCreator, PersistentPropertyFilter.INSTANCE);
persistentPropertyCreator.addPropertiesForRemainingDescriptors();
entity.verify();
if (persistentPropertyAccessorFactory.isSupported(entity)) {
entity.setPersistentPropertyAccessorFactory(persistentPropertyAccessorFactory);
}
} catch (MappingException e) {
persistentEntities.remove(typeInformation);
throw e;
}
// Inform listeners
if (null != applicationEventPublisher) {
applicationEventPublisher.publishEvent(new MappingContextEvent<>(this, entity));
}
return Optional.of(entity);
} catch (BeansException e) {
throw new MappingException(e.getMessage(), e);
} finally {
write.unlock();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.PersistentEntityAware#getManagedTypes()
*/
@Override
public Collection<TypeInformation<?>> getManagedTypes() {
try {
read.lock();
return Collections.unmodifiableSet(new HashSet<>(persistentEntities.keySet()));
} finally {
read.unlock();
}
}
/**
* Creates the concrete {@link PersistentEntity} instance.
*
* @param <T>
* @param typeInformation
* @return
*/
protected abstract <T> E createPersistentEntity(TypeInformation<T> typeInformation);
/**
* Creates the concrete instance of {@link PersistentProperty}.
*
* @param field
* @param descriptor
* @param owner
* @param simpleTypeHolder
* @return
*/
protected abstract P createPersistentProperty(Property property, E owner, SimpleTypeHolder simpleTypeHolder);
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
initialize();
}
/**
* Initializes the mapping context. Will add the types configured through {@link #setInitialEntitySet(Set)} to the
* context.
*/
public void initialize() {
initialEntitySet.forEach(this::addPersistentEntity);
}
/**
* Returns whether a {@link PersistentEntity} instance should be created for the given {@link TypeInformation}. By
* default this will reject this for all types considered simple, but it might be necessary to tweak that in case you
* have registered custom converters for top level types (which renders them to be considered simple) but still need
* meta-information about them.
*
* @param type will never be {@literal null}.
* @return
*/
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
return !simpleTypeHolder.isSimpleType(type.getType());
}
/**
* {@link FieldCallback} to create {@link PersistentProperty} instances.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private final class PersistentPropertyCreator implements FieldCallback {
private final @NonNull E entity;
private final @NonNull Map<String, PropertyDescriptor> descriptors;
private final @NonNull Map<String, PropertyDescriptor> remainingDescriptors;
public PersistentPropertyCreator(E entity, Map<String, PropertyDescriptor> descriptors) {
this(entity, descriptors, descriptors);
}
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.FieldCallback#doWith(java.lang.reflect.Field)
*/
public void doWith(Field field) {
String fieldName = field.getName();
ReflectionUtils.makeAccessible(field);
Property property = Optional.ofNullable(descriptors.get(fieldName))//
.map(it -> Property.of(field, it))//
.orElseGet(() -> Property.of(field));
createAndRegisterProperty(property);
this.remainingDescriptors.remove(fieldName);
}
/**
* Adds {@link PersistentProperty} instances for all suitable {@link PropertyDescriptor}s without a backing
* {@link Field}.
*
* @see PersistentPropertyFilter
*/
public void addPropertiesForRemainingDescriptors() {
remainingDescriptors.values().stream()//
.map(Property::of)//
.filter(PersistentPropertyFilter.INSTANCE::matches)//
.forEach(this::createAndRegisterProperty);
}
private void createAndRegisterProperty(Property input) {
P property = createPersistentProperty(input, entity, simpleTypeHolder);
if (property.isTransient()) {
return;
}
if (!input.isFieldBacked() && !property.usePropertyAccess()) {
return;
}
entity.addPersistentProperty(property);
property.getAssociation().ifPresent(entity::addAssociation);
if (entity.getType().equals(property.getRawType())) {
return;
}
property.getPersistentEntityType().forEach(AbstractMappingContext.this::addPersistentEntity);
}
}
/**
* Filter rejecting static fields as well as artificially introduced ones. See
* {@link PersistentPropertyFilter#UNMAPPED_PROPERTIES} for details.
*
* @author Oliver Gierke
*/
static enum PersistentPropertyFilter implements FieldFilter {
INSTANCE;
private static final Streamable<PropertyMatch> UNMAPPED_PROPERTIES;
static {
Set<PropertyMatch> matches = new HashSet<>();
matches.add(new PropertyMatch("class", null));
matches.add(new PropertyMatch("this\\$.*", null));
matches.add(new PropertyMatch("metaClass", "groovy.lang.MetaClass"));
UNMAPPED_PROPERTIES = Streamable.of(matches);
}
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.FieldFilter#matches(java.lang.reflect.Field)
*/
public boolean matches(Field field) {
if (Modifier.isStatic(field.getModifiers())) {
return false;
}
return !UNMAPPED_PROPERTIES.stream()//
.anyMatch(it -> it.matches(field.getName(), field.getType()));
}
/**
* Returns whether the given {@link PropertyDescriptor} is one to create a {@link PersistentProperty} for.
*
* @param descriptor must not be {@literal null}.
* @return
*/
public boolean matches(Property property) {
Assert.notNull(property, "Property must not be null!");
if (!property.hasAccessor()) {
return false;
}
return !UNMAPPED_PROPERTIES.stream()//
.anyMatch(it -> it.matches(property.getName(), property.getType()));
}
/**
* Value object to help defining property exclusion based on name patterns and types.
*
* @since 1.4
* @author Oliver Gierke
*/
static class PropertyMatch {
private final String namePattern;
private final String typeName;
/**
* Creates a new {@link PropertyMatch} for the given name pattern and type name. At least one of the paramters
* must not be {@literal null}.
*
* @param namePattern a regex pattern to match field names, can be {@literal null}.
* @param typeName the name of the type to exclude, can be {@literal null}.
*/
public PropertyMatch(String namePattern, String typeName) {
Assert.isTrue(!(namePattern == null && typeName == null), "Either name patter or type name must be given!");
this.namePattern = namePattern;
this.typeName = typeName;
}
/**
* Returns whether the given {@link Field} matches the defined {@link PropertyMatch}.
*
* @param field must not be {@literal null}.
* @return
*/
public boolean matches(String name, Class<?> type) {
if (namePattern != null && !name.matches(namePattern)) {
return false;
}
if (typeName != null && !type.getName().equals(typeName)) {
return false;
}
return true;
}
}
}
}