diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java new file mode 100644 index 000000000..fdc276a6c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -0,0 +1,690 @@ +/* + * Copyright 2011-2020 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.context; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; +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.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.KotlinDetector; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPaths; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; +import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.InstantiationAwarePropertyAccessorFactory; +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.mapping.model.StaticPropertyAccessorFactory; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.KotlinReflectionUtils; +import org.springframework.data.util.Optionals; +import org.springframework.data.util.StaticTypeInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; +import org.springframework.util.ReflectionUtils.FieldFilter; + +/** + * Base class to build mapping metadata and thus create instances of {@link PersistentEntity} and + * {@link PersistentProperty}. + *

+ * The implementation uses a {@link ReentrantReadWriteLock} to make sure {@link PersistentEntity} are completely + * populated before accessing them from outside. + * + * @param the concrete {@link PersistentEntity} type the {@link MappingContext} implementation creates + * @param

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, P extends PersistentProperty

> + implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean { + + private static final boolean IN_NATIVE_IMAGE = System.getProperty("org.graalvm.nativeimage.imagecode") != null; + + private final Optional NONE = Optional.empty(); + private final Map, Optional> persistentEntities = new HashMap<>(); + private final PersistentPropertyAccessorFactory persistentPropertyAccessorFactory; + private final PersistentPropertyPathFactory persistentPropertyPathFactory; + + private @Nullable ApplicationEventPublisher applicationEventPublisher; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; + + private Set> 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(); + + protected AbstractMappingContext() { + + this.persistentPropertyPathFactory = new PersistentPropertyPathFactory<>(this); + + EntityInstantiators instantiators = new EntityInstantiators(); + PersistentPropertyAccessorFactory accessorFactory = IN_NATIVE_IMAGE ? BeanWrapperPropertyAccessorFactory.INSTANCE + : new ClassGeneratingPropertyAccessorFactory(); + + this.persistentPropertyAccessorFactory = new InstantiationAwarePropertyAccessorFactory(accessorFactory, + instantiators); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.evaluationContextProvider = new ExtensionAwareEvaluationContextProvider(applicationContext); + + if (applicationEventPublisher == null) { + this.applicationEventPublisher = applicationContext; + } + } + + /** + * Sets the {@link Set} of types to populate the context initially. + * + * @param initialEntitySet + */ + public void setInitialEntitySet(Set> 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 analyzed. + * + * @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() + */ + @Override + public Collection 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) + */ + @Nullable + public E getPersistentEntity(Class type) { + return getPersistentEntity(ClassTypeInformation.from(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!"); + + Optional entity = persistentEntities.get(ClassTypeInformation.from(type)); + + return entity == null ? false : entity.isPresent(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Nullable + @Override + public E getPersistentEntity(TypeInformation type) { + + Assert.notNull(type, "Type must not be null!"); + + try { + + read.lock(); + + Optional entity = persistentEntities.get(type); + + if (entity != null) { + return entity.orElse(null); + } + + } finally { + read.unlock(); + } + + if (!shouldCreatePersistentEntityFor(type)) { + + try { + write.lock(); + persistentEntities.put(type, NONE); + } finally { + write.unlock(); + } + + return null; + } + + if (strict) { + throw new MappingException("Unknown persistent entity " + type); + } + + return addPersistentEntity(type).orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.MappingContext#getPersistentEntity(org.springframework.data.mapping.PersistentProperty) + */ + @Nullable + @Override + public E getPersistentEntity(P persistentProperty) { + + Assert.notNull(persistentProperty, "PersistentProperty must not be null!"); + + if (!persistentProperty.isEntity()) { + return null; + } + + TypeInformation typeInfo = persistentProperty.getTypeInformation(); + return getPersistentEntity(typeInfo.getRequiredActualType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.MappingContext#getPersistentPropertyPath(java.lang.Class, java.lang.String) + */ + @Override + public PersistentPropertyPath

getPersistentPropertyPath(PropertyPath propertyPath) { + return persistentPropertyPathFactory.from(propertyPath); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.MappingContext#getPersistentPropertyPath(java.lang.String, java.lang.Class) + */ + @Override + public PersistentPropertyPath

getPersistentPropertyPath(String propertyPath, Class type) { + return persistentPropertyPathFactory.from(type, propertyPath); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.MappingContext#findPersistentPropertyPath(java.lang.Class, java.util.function.Predicate) + */ + @Override + public PersistentPropertyPaths findPersistentPropertyPaths(Class type, Predicate predicate) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(predicate, "Selection predicate must not be null!"); + + return doFindPersistentPropertyPaths(type, predicate, it -> !it.isAssociation()); + } + + /** + * Actually looks up the {@link PersistentPropertyPaths} for the given type, selection predicate and traversal guard. + * Primary purpose is to allow sub-types to alter the default traversal guard, e.g. used by + * {@link #findPersistentPropertyPaths(Class, Predicate)}. + * + * @param type will never be {@literal null}. + * @param predicate will never be {@literal null}. + * @param traversalGuard will never be {@literal null}. + * @return will never be {@literal null}. + * @see #findPersistentPropertyPaths(Class, Predicate) + */ + protected final PersistentPropertyPaths doFindPersistentPropertyPaths(Class type, + Predicate predicate, Predicate

traversalGuard) { + return persistentPropertyPathFactory.from(ClassTypeInformation.from(type), predicate, traversalGuard); + } + + /** + * Adds the given type to the {@link MappingContext}. + * + * @param type must not be {@literal null}. + * @return + */ + protected Optional 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 addPersistentEntity(TypeInformation typeInformation) { + + Assert.notNull(typeInformation, "TypeInformation must not be null!"); + + try { + + read.lock(); + + Optional persistentEntity = persistentEntities.get(typeInformation); + + if (persistentEntity != null) { + return persistentEntity; + } + + } finally { + read.unlock(); + } + + Class type = typeInformation.getType(); + E entity = null; + + try { + + write.lock(); + + entity = createPersistentEntity(typeInformation); + + entity.setEvaluationContextProvider(evaluationContextProvider); + + // Eagerly cache the entity as we might have to find it during recursive lookups. + persistentEntities.put(typeInformation, Optional.of(entity)); + + if (typeInformation instanceof StaticTypeInformation) { + + // ((StaticTypeInformation)typeInformation).doWithProperties() + + Map> properties = ((StaticTypeInformation) typeInformation).getProperties(); + for (Entry> entry : properties.entrySet()) { + + P target = createPersistentProperty(Property.of(typeInformation, entry.getKey()), entity, simpleTypeHolder); + entity.addPersistentProperty(target); + + } + entity.setPersistentPropertyAccessorFactory(new StaticPropertyAccessorFactory()); + return Optional.of(entity); + } + + PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(type); + + final Map 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 (RuntimeException e) { + persistentEntities.remove(typeInformation); + throw e; + } + + } catch (BeansException e) { + throw new MappingException(e.getMessage(), e); + } finally { + write.unlock(); + } + + // Inform listeners + if (applicationEventPublisher != null && entity != null) { + applicationEventPublisher.publishEvent(new MappingContextEvent<>(this, entity)); + } + + return Optional.of(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.PersistentEntityAware#getManagedTypes() + */ + @Override + public Collection> getManagedTypes() { + + try { + + read.lock(); + return Collections.unmodifiableSet(new HashSet<>(persistentEntities.keySet())); + + } finally { + read.unlock(); + } + } + + /** + * Creates the concrete {@link PersistentEntity} instance. + * + * @param + * @param typeInformation + * @return + */ + protected abstract E createPersistentEntity(TypeInformation typeInformation); + + /** + * Creates the concrete instance of {@link PersistentProperty}. + * + * @param property + * @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 all types considered simple and non-supported Kotlin classes, 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) { + + if (simpleTypeHolder.isSimpleType(type.getType())) { + return false; + } + + return !KotlinDetector.isKotlinType(type.getType()) || KotlinReflectionUtils.isSupportedKotlinClass(type.getType()); + } + + /** + * {@link FieldCallback} to create {@link PersistentProperty} instances. + * + * @author Oliver Gierke + */ + private final class PersistentPropertyCreator implements FieldCallback { + + private final E entity; + private final Map descriptors; + private final Map remainingDescriptors; + + public PersistentPropertyCreator(E entity, Map descriptors) { + this(entity, descriptors, descriptors); + } + + private PersistentPropertyCreator(E entity, Map descriptors, + Map remainingDescriptors) { + this.entity = entity; + this.descriptors = descriptors; + this.remainingDescriptors = remainingDescriptors; + } + + /* + * (non-Javadoc) + * @see org.springframework.util.ReflectionUtils.FieldCallback#doWith(java.lang.reflect.Field) + */ + public void doWith(Field field) { + + String fieldName = field.getName(); + TypeInformation type = entity.getTypeInformation(); + + ReflectionUtils.makeAccessible(field); + + Property property = Optional.ofNullable(descriptors.get(fieldName))// + .map(it -> Property.of(type, field, it))// + .orElseGet(() -> Property.of(type, 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() // + .filter(Property::supportsStandalone) // + .map(it -> Property.of(entity.getTypeInformation(), it)) // + .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); + + if (property.isAssociation()) { + entity.addAssociation(property.getRequiredAssociation()); + } + + if (entity.getType().equals(property.getRawType())) { + return; + } + + property.getPersistentEntityTypes().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 UNMAPPED_PROPERTIES; + + static { + + Set 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 property 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 @Nullable String namePattern; + private final @Nullable String typeName; + + /** + * Creates a new {@link PropertyMatch} for the given name pattern and type name. At least one of the parameters + * 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(@Nullable String namePattern, @Nullable String typeName) { + + Assert.isTrue(!(namePattern == null && typeName == null), "Either name pattern or type name must be given!"); + + this.namePattern = namePattern; + this.typeName = typeName; + } + + /** + * Returns whether the given {@link Field} matches the defined {@link PropertyMatch}. + * + * @param name must not be {@literal null}. + * @param type must not be {@literal null}. + * @return + */ + public boolean matches(String name, Class type) { + + Assert.notNull(name, "Name must not be null!"); + Assert.notNull(type, "Type must not be null!"); + + if (namePattern != null && !name.matches(namePattern)) { + return false; + } + + if (typeName != null && !type.getName().equals(typeName)) { + return false; + } + + return true; + } + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java new file mode 100644 index 000000000..5218e6c3a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -0,0 +1,640 @@ +/* + * Copyright 2011-2020 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.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +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.StaticTypeInformation; +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; + +/** + * Simple value object to capture information of {@link PersistentEntity}s. + * + * @author Oliver Gierke + * @author Jon Brisbin + * @author Patryk Wasik + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + */ +public class BasicPersistentEntity> implements MutablePersistentEntity { + + private static final String TYPE_MISMATCH = "Target bean of type %s is not of type of the persistent entity (%s)!"; + + private final @Nullable PreferredConstructor constructor; + private final TypeInformation information; + private final List

properties; + private final List

persistentPropertiesCache; + private final @Nullable Comparator

comparator; + private final Set> associations; + + private final Map propertyCache; + private final Map, Optional> annotationCache; + private final MultiValueMap, P> propertyAnnotationCache; + + private @Nullable P idProperty; + private @Nullable P versionProperty; + private PersistentPropertyAccessorFactory propertyAccessorFactory; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; + + private final Lazy typeAlias; + private final Lazy isNewStrategy; + private final Lazy isImmutable; + private final Lazy requiresPropertyPopulation; + + /** + * Creates a new {@link BasicPersistentEntity} from the given {@link TypeInformation}. + * + * @param information must not be {@literal null}. + */ + public BasicPersistentEntity(TypeInformation information) { + this(information, null); + } + + /** + * Creates a new {@link BasicPersistentEntity} for the given {@link TypeInformation} and {@link Comparator}. The given + * {@link Comparator} will be used to define the order of the {@link PersistentProperty} instances added to the + * entity. + * + * @param information must not be {@literal null}. + * @param comparator can be {@literal null}. + */ + public BasicPersistentEntity(TypeInformation information, @Nullable Comparator

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 = information instanceof StaticTypeInformation ? null : 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()))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPersistenceConstructor() + */ + @Nullable + public PreferredConstructor getPersistenceConstructor() { + return constructor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isConstructorArgument(org.springframework.data.mapping.PersistentProperty) + */ + public boolean isConstructorArgument(PersistentProperty property) { + return constructor != null && constructor.isConstructorParameter(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isIdProperty(org.springframework.data.mapping.PersistentProperty) + */ + public boolean isIdProperty(PersistentProperty property) { + return idProperty != null && idProperty.equals(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isVersionProperty(org.springframework.data.mapping.PersistentProperty) + */ + public boolean isVersionProperty(PersistentProperty property) { + return versionProperty != null && versionProperty.equals(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getName() + */ + public String getName() { + return getType().getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getIdProperty() + */ + @Nullable + public P getIdProperty() { + return idProperty; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getVersionProperty() + */ + @Nullable + public P getVersionProperty() { + return versionProperty; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#hasIdProperty() + */ + public boolean hasIdProperty() { + return idProperty != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#hasVersionProperty() + */ + public boolean hasVersionProperty() { + return versionProperty != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#addPersistentProperty(P) + */ + 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; + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#setEvaluationContextProvider(org.springframework.data.spel.EvaluationContextProvider) + */ + @Override + public void setEvaluationContextProvider(EvaluationContextProvider provider) { + this.evaluationContextProvider = provider; + } + + /** + * Returns the given property if it is a better candidate for the id property than the current id property. + * + * @param property the new id property candidate, will never be {@literal null}. + * @return the given id property or {@literal null} if the given property is not an id property. + */ + @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; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#addAssociation(org.springframework.data.mapping.model.Association) + */ + public void addAssociation(Association

association) { + + Assert.notNull(association, "Association must not be null!"); + + associations.add(association); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPersistentProperty(java.lang.String) + */ + @Override + @Nullable + public P getPersistentProperty(String name) { + return propertyCache.get(name); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPersistentProperties(java.lang.String) + */ + @Override + public Iterable

getPersistentProperties(Class annotationType) { + + Assert.notNull(annotationType, "Annotation type must not be null!"); + return propertyAnnotationCache.computeIfAbsent(annotationType, this::doFindPersistentProperty); + } + + private List

doFindPersistentProperty(Class annotationType) { + + List

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()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getType() + */ + public Class getType() { + return information.getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getTypeAlias() + */ + public Alias getTypeAlias() { + return typeAlias.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getTypeInformation() + */ + public TypeInformation getTypeInformation() { + return information; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#doWithProperties(org.springframework.data.mapping.PropertyHandler) + */ + public void doWithProperties(PropertyHandler

handler) { + + Assert.notNull(handler, "PropertyHandler must not be null!"); + + for (P property : persistentPropertiesCache) { + handler.doWithPersistentProperty(property); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#doWithProperties(org.springframework.data.mapping.PropertyHandler.Simple) + */ + @Override + public void doWithProperties(SimplePropertyHandler handler) { + + Assert.notNull(handler, "Handler must not be null!"); + + for (PersistentProperty property : persistentPropertiesCache) { + handler.doWithPersistentProperty(property); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#doWithAssociations(org.springframework.data.mapping.AssociationHandler) + */ + public void doWithAssociations(AssociationHandler

handler) { + + Assert.notNull(handler, "Handler must not be null!"); + + for (Association

association : associations) { + handler.doWithAssociation(association); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#doWithAssociations(org.springframework.data.mapping.SimpleAssociationHandler) + */ + public void doWithAssociations(SimpleAssociationHandler handler) { + + Assert.notNull(handler, "Handler must not be null!"); + + for (Association> association : associations) { + handler.doWithAssociation(association); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#findAnnotation(java.lang.Class) + */ + @Nullable + @Override + public A findAnnotation(Class annotationType) { + return doFindAnnotation(annotationType).orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isAnnotationPresent(java.lang.Class) + */ + @Override + public boolean isAnnotationPresent(Class annotationType) { + return doFindAnnotation(annotationType).isPresent(); + } + + @SuppressWarnings("unchecked") + private Optional doFindAnnotation(Class annotationType) { + + return (Optional) annotationCache.computeIfAbsent(annotationType, + it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(getType(), it))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#verify() + */ + public void verify() { + + if (comparator != null) { + properties.sort(comparator); + persistentPropertiesCache.sort(comparator); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.MutablePersistentEntity#setPersistentPropertyAccessorFactory(org.springframework.data.mapping.model.PersistentPropertyAccessorFactory) + */ + @Override + public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) { + this.propertyAccessorFactory = factory; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPropertyAccessor(java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(B bean) { + + verifyBeanType(bean); + + return propertyAccessorFactory.getPropertyAccessor(this, bean); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getPropertyPathAccessor(java.lang.Object) + */ + @Override + public PersistentPropertyPathAccessor getPropertyPathAccessor(B bean) { + return new SimplePersistentPropertyPathAccessor<>(getPropertyAccessor(bean)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#getIdentifierAccessor(java.lang.Object) + */ + @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); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isNew(java.lang.Object) + */ + @Override + public boolean isNew(Object bean) { + + verifyBeanType(bean); + + return isNewStrategy.get().isNew(bean); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#isImmutable() + */ + @Override + public boolean isImmutable() { + return isImmutable.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentEntity#requiresPropertyPopulation() + */ + @Override + public boolean requiresPropertyPopulation() { + return requiresPropertyPopulation.get(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator

iterator() { + + Iterator

iterator = properties.iterator(); + + return new Iterator

() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public P next() { + return iterator.next(); + } + }; + } + + protected EvaluationContext getEvaluationContext(Object rootObject) { + return evaluationContextProvider.getEvaluationContext(rootObject); + } + + /** + * Returns the default {@link IsNewStrategy} to be used. Will be a {@link PersistentEntityIsNewStrategy} by default. + * Note, that this strategy only gets used if the entity doesn't implement {@link Persistable} as this indicates the + * user wants to be in control over whether an entity is new or not. + * + * @return + * @since 2.1 + */ + protected IsNewStrategy getFallbackIsNewStrategy() { + return PersistentEntityIsNewStrategy.of(this); + } + + /** + * Verifies the given bean type to no be {@literal null} and of the type of the current {@link PersistentEntity}. + * + * @param bean must not be {@literal null}. + */ + private 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())); + } + + /** + * Calculates the {@link Alias} to be used for the given type. + * + * @param type must not be {@literal null}. + * @return + */ + private static Alias getAliasFromAnnotation(Class type) { + + Optional typeAliasValue = Optional + .ofNullable(AnnotatedElementUtils.findMergedAnnotation(type, TypeAlias.class))// + .map(TypeAlias::value)// + .filter(StringUtils::hasText); + + return Alias.ofNullable(typeAliasValue.orElse(null)); + } + + /** + * A null-object implementation of {@link IdentifierAccessor} to be able to return an accessor for entities that do + * not have an identifier property. + * + * @author Oliver Gierke + */ + private static class AbsentIdentifierAccessor extends TargetAwareIdentifierAccessor { + + public AbsentIdentifierAccessor(Object target) { + super(target); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.IdentifierAccessor#getIdentifier() + */ + @Override + @Nullable + public Object getIdentifier() { + return null; + } + } + + /** + * Simple {@link Comparator} adaptor to delegate ordering to the inverse properties of the association. + * + * @author Oliver Gierke + */ + private static final class AssociationComparator

> + implements Comparator>, Serializable { + + private static final long serialVersionUID = 4508054194886854513L; + private final Comparator

delegate; + + AssociationComparator(Comparator

delegate) { + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + public int compare(@Nullable Association

left, @Nullable Association

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()); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java new file mode 100644 index 000000000..bc67e50cb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 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.util.Collections; +import java.util.Map; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.util.StaticTypeInformation; +import org.springframework.util.Assert; + +/** + * Simple value object allowing access to {@link EntityInstantiator} instances for a given type falling back to a + * default one. + * + * @author Oliver Drotbohm + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.3 + */ +public class EntityInstantiators { + + private final EntityInstantiator fallback; + private final Map, EntityInstantiator> customInstantiators; + + /** + * Creates a new {@link EntityInstantiators} using the default fallback instantiator and no custom ones. + */ + public EntityInstantiators() { + this(Collections.emptyMap()); + } + + /** + * Creates a new {@link EntityInstantiators} using the given {@link EntityInstantiator} as fallback. + * + * @param fallback must not be {@literal null}. + */ + public EntityInstantiators(EntityInstantiator fallback) { + this(fallback, Collections.emptyMap()); + } + + /** + * Creates a new {@link EntityInstantiators} using the default fallback instantiator and the given custom ones. + * + * @param customInstantiators must not be {@literal null}. + */ + public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { + this(new KotlinClassGeneratingEntityInstantiator(), customInstantiators); + } + + /** + * Creates a new {@link EntityInstantiator} using the given fallback {@link EntityInstantiator} and the given custom + * ones. + * + * @param defaultInstantiator must not be {@literal null}. + * @param customInstantiators must not be {@literal null}. + */ + public EntityInstantiators(EntityInstantiator defaultInstantiator, + Map, EntityInstantiator> customInstantiators) { + + Assert.notNull(defaultInstantiator, "DefaultInstantiator must not be null!"); + Assert.notNull(customInstantiators, "CustomInstantiators must not be null!"); + + this.fallback = defaultInstantiator; + this.customInstantiators = customInstantiators; + } + + /** + * Returns the {@link EntityInstantiator} to be used to create the given {@link PersistentEntity}. + * + * @param entity must not be {@literal null}. + * @return will never be {@literal null}. + */ + public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { + + Assert.notNull(entity, "Entity must not be null!"); + Class type = entity.getType(); + + if (!customInstantiators.containsKey(type)) { + + if(entity.getTypeInformation() instanceof StaticTypeInformation) { + EntityInstantiator instantiator = ((StaticTypeInformation)entity.getTypeInformation()).getInstantiator(); + return instantiator != null ? instantiator : fallback; + } + return fallback; + } + + EntityInstantiator instantiator = customInstantiators.get(entity.getType()); + return instantiator == null ? fallback : instantiator; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/Property.java b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/Property.java new file mode 100644 index 000000000..a45f52477 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/Property.java @@ -0,0 +1,337 @@ +/* + * Copyright 2016-2020 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.beans.FeatureDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.springframework.data.util.Lazy; +import org.springframework.data.util.Optionals; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Value object to abstract the concept of a property backed by a {@link Field} and / or a {@link PropertyDescriptor}. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Mark Paluch + */ +public class Property { + + private @Nullable TypeInformation typeInformation; + private final Optional field; + private final Optional descriptor; + + private final Class rawType; + private final Lazy hashCode; + private final Optional getter; + private final Optional setter; + + private final Lazy name; + private final Lazy toString; + private final Lazy> wither; + + private Property(String name, TypeInformation typeInformation) { + + this.typeInformation = typeInformation; + this.field = Optional.empty(); + this.descriptor = Optional.empty(); + + this.rawType = typeInformation.getType(); + this.hashCode = Lazy.of( () -> typeInformation.hashCode() + name.hashCode()); + this.getter = Optional.empty(); + this.setter = Optional.empty(); + this.name = Lazy.of(name); + this.toString = Lazy.of(() -> typeInformation.toString() + name); + this.wither = Lazy.of(() -> findWither(typeInformation, getName(), getType())); + } + + private Property(TypeInformation type, Optional field, Optional descriptor) { + + Assert.notNull(type, "Type must not be null!"); + Assert.isTrue(Optionals.isAnyPresent(field, descriptor), "Either field or descriptor has to be given!"); + + this.field = field; + this.descriptor = descriptor; + + this.rawType = withFieldOrDescriptor( // + it -> type.getRequiredProperty(it.getName()).getType(), // + it -> type.getRequiredProperty(it.getName()).getType() // + ); + this.hashCode = Lazy.of(() -> withFieldOrDescriptor(Object::hashCode)); + this.name = Lazy.of(() -> withFieldOrDescriptor(Field::getName, FeatureDescriptor::getName)); + this.toString = Lazy.of(() -> withFieldOrDescriptor(Object::toString, + it -> String.format("%s.%s", type.getType().getName(), it.getDisplayName()))); + + this.getter = descriptor.map(PropertyDescriptor::getReadMethod)// + .filter(it -> getType() != null)// + .filter(it -> getType().isAssignableFrom(type.getReturnType(it).getType())); + + this.setter = descriptor.map(PropertyDescriptor::getWriteMethod)// + .filter(it -> getType() != null)// + .filter(it -> type.getParameterTypes(it).get(0).getType().isAssignableFrom(getType())); + + this.wither = Lazy.of(() -> findWither(type, getName(), getType())); + } + + /** + * Creates a new {@link Property} backed by the given field. + * + * @param type the owning type, must not be {@literal null}. + * @param field must not be {@literal null}. + * @return + */ + public static Property of(TypeInformation type, Field field) { + + Assert.notNull(field, "Field must not be null!"); + + return new Property(type, Optional.of(field), Optional.empty()); + } + + /** + * Creates a new {@link Property} backed by the given {@link Field} and {@link PropertyDescriptor}. + * + * @param type the owning type, must not be {@literal null}. + * @param field must not be {@literal null}. + * @param descriptor must not be {@literal null}. + * @return + */ + public static Property of(TypeInformation type, Field field, PropertyDescriptor descriptor) { + + Assert.notNull(field, "Field must not be null!"); + Assert.notNull(descriptor, "PropertyDescriptor must not be null!"); + + return new Property(type, Optional.of(field), Optional.of(descriptor)); + } + + /** + * Creates a new {@link Property} backed by the given {@link Field} and {@link PropertyDescriptor}. + * + * @param type the owning type, must not be {@literal null}. + * @param field must not be {@literal null}. + * @param descriptor must not be {@literal null}. + * @return + */ + public static Property of(TypeInformation type, String name) { + return new Property(name, type); + } + + /** + * Creates a new {@link Property} for the given {@link PropertyDescriptor}. The creation might fail if the given + * property is not representing a proper property. + * + * @param type the owning type, must not be {@literal null}. + * @param descriptor must not be {@literal null}. + * @return + * @see #supportsStandalone(PropertyDescriptor) + */ + public static Property of(TypeInformation type, PropertyDescriptor descriptor) { + + Assert.notNull(descriptor, "PropertyDescriptor must not be null!"); + + return new Property(type, Optional.empty(), Optional.of(descriptor)); + } + + /** + * Returns whether the given {@link PropertyDescriptor} is supported in for standalone creation of a {@link Property} + * instance. + * + * @param descriptor + * @return + */ + public static boolean supportsStandalone(PropertyDescriptor descriptor) { + + Assert.notNull(descriptor, "PropertyDescriptor must not be null!"); + + return descriptor.getPropertyType() != null; + } + + /** + * Returns whether the property is backed by a field. + * + * @return + */ + public boolean isFieldBacked() { + return field.isPresent(); + } + + /** + * Returns the getter of the property if available and if it matches the type of the property. + * + * @return will never be {@literal null}. + */ + public Optional getGetter() { + return getter; + } + + /** + * Returns the setter of the property if available and if its first (only) parameter matches the type of the property. + * + * @return will never be {@literal null}. + */ + public Optional getSetter() { + return setter; + } + + /** + * Returns the wither of the property if available and if its first (only) parameter matches the type of the property. + * + * @return will never be {@literal null}. + */ + public Optional getWither() { + return wither.get(); + } + + /** + * Returns the field of the property if available and if its first (only) parameter matches the type of the property. + * + * @return will never be {@literal null}. + */ + public Optional getField() { + return this.field; + } + + /** + * Returns whether the property exposes a getter or a setter. + * + * @return + */ + public boolean hasAccessor() { + return getGetter().isPresent() || getSetter().isPresent(); + } + + /** + * Returns the name of the property. + * + * @return will never be {@literal null}. + */ + public String getName() { + return this.name.get(); + } + + /** + * Returns the type of the property. + * + * @return will never be {@literal null}. + */ + public Class getType() { + return rawType; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(@Nullable Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Property)) { + return false; + } + + Property that = (Property) obj; + if(this.typeInformation != null && that.typeInformation != null) { + if(this.typeInformation != that.typeInformation) { + return false; + } + if(!this.name.get().equals(that.name.get())) { + return false; + } + } + + return this.field.isPresent() ? this.field.equals(that.field) : this.descriptor.equals(that.descriptor); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return hashCode.get(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return toString.get(); + } + + /** + * Maps the backing {@link Field} or {@link PropertyDescriptor} using the given {@link Function}. + * + * @param function must not be {@literal null}. + * @return + */ + private T withFieldOrDescriptor(Function function) { + return withFieldOrDescriptor(function, function); + } + + /** + * Maps the backing {@link Field} or {@link PropertyDescriptor} using the given functions. + * + * @param field must not be {@literal null}. + * @param descriptor must not be {@literal null}. + * @return + */ + private T withFieldOrDescriptor(Function field, + Function descriptor) { + + return Optionals.firstNonEmpty(// + () -> this.field.map(field), // + () -> this.descriptor.map(descriptor))// + .orElseThrow(() -> new IllegalStateException("Should not occur! Either field or descriptor has to be given")); + } + + private static Optional findWither(TypeInformation owner, String propertyName, Class rawType) { + + AtomicReference resultHolder = new AtomicReference<>(); + String methodName = String.format("with%s", StringUtils.capitalize(propertyName)); + + ReflectionUtils.doWithMethods(owner.getType(), it -> { + + if (owner.isAssignableFrom(owner.getReturnType(it))) { + resultHolder.set(it); + } + }, it -> isMethodWithSingleParameterOfType(it, methodName, rawType)); + + Method method = resultHolder.get(); + return method != null ? Optional.of(method) : Optional.empty(); + } + + private static boolean isMethodWithSingleParameterOfType(Method method, String name, Class type) { + + return method.getParameterCount() == 1 // + && method.getName().equals(name) // + && method.getParameterTypes()[0].equals(type); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/StaticPropertyAccessorFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/StaticPropertyAccessorFactory.java new file mode 100644 index 000000000..a579d72a0 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mapping/model/StaticPropertyAccessorFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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 java.util.function.BiFunction; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.util.StaticTypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class StaticPropertyAccessorFactory implements PersistentPropertyAccessorFactory { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#getPropertyAccessor(org.springframework.data.mapping.PersistentEntity, java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { + return new StaticPropertyAccessor<>((StaticTypeInformation) entity.getTypeInformation(), bean); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#isSupported(org.springframework.data.mapping.PersistentEntity) + */ + @Override + public boolean isSupported(PersistentEntity entity) { + + boolean isStaticTypedEntity = entity.getTypeInformation() instanceof StaticTypeInformation; + System.out.println(entity.getName() + " isStaticTypedEntity: " + isStaticTypedEntity); + return isStaticTypedEntity; + } + + static class StaticPropertyAccessor implements PersistentPropertyAccessor { + + T bean; + StaticTypeInformation typeInformation; + + public StaticPropertyAccessor(StaticTypeInformation typeInformation, T bean) { + this.bean = bean; + this.typeInformation = typeInformation; + } + + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + + BiFunction setFunction = typeInformation.getSetter().get(property.getName()); + if (setFunction == null) { + return; + } + this.bean = setFunction.apply(bean, value); + } + + @Nullable + @Override + public Object getProperty(PersistentProperty property) { + + Function getFunction = typeInformation.getGetter().get(property.getName()); + if (getFunction == null) { + return null; + } + return getFunction.apply(bean); + } + + @Override + public T getBean() { + return this.bean; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/Address.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/Address.java new file mode 100644 index 000000000..7681797a2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/Address.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class Address { + + String city; + String street; + + public Address(String city, String street) { + this.city = city; + this.street = street; + } + + public String getCity() { + return city; + } + + public String getStreet() { + return street; + } + + public void setCity(String city) { + this.city = city; + } + + public void setStreet(String street) { + this.street = street; + } + + @Override + public String toString() { + return "Address{" + + "city='" + city + '\'' + + ", street='" + street + '\'' + + '}'; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/AddressTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/AddressTypeInformation.java new file mode 100644 index 000000000..0f8790370 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/AddressTypeInformation.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.ParameterValueProvider; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class AddressTypeInformation extends StaticTypeInformation

{ + + public AddressTypeInformation() { + super(Address.class); + } + + @Override + protected Map> computePropertiesMap() { + + Map> properties = new LinkedHashMap<>(); + properties.put("city", new StringTypeInformation()); + properties.put("street", new StringTypeInformation()); + return properties; + } + + @Override + protected Map> computeGetter() { + Map> getters = new LinkedHashMap<>(); + getters.put("city", Address::getCity); + getters.put("street", Address::getStreet); + + return getters; + } + + @Override + protected Map> computeSetter() { + Map> setter = new LinkedHashMap<>(); + setter.put("city", (bean, id) -> {bean.setCity((String)id); return bean;}); + setter.put("street", (bean, id) -> {bean.setStreet((String)id); return bean;}); + return setter; + } + + @Override + protected EntityInstantiator computeEntityInstantiator() { + return new EntityInstantiator() { + + @Override + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + String city = (String) provider + .getParameterValue(new Parameter("city", new StringTypeInformation(), new Annotation[]{}, entity)); + String street = (String) provider + .getParameterValue(new Parameter("street", new StringTypeInformation(), new Annotation[]{}, entity)); + + return (T) new Address(city, street); + } + }; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/ClassTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/ClassTypeInformation.java new file mode 100644 index 000000000..6ea0fdc05 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/ClassTypeInformation.java @@ -0,0 +1,191 @@ +/* + * Copyright 2011-2020 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.util; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; + +/** + * {@link TypeInformation} for a plain {@link Class}. + * + * @author Oliver Gierke + * @author Christoph Strobl + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class ClassTypeInformation extends TypeDiscoverer { + + public static final ClassTypeInformation COLLECTION = new ClassTypeInformation(Collection.class); + public static final ClassTypeInformation LIST = new ClassTypeInformation(List.class); + public static final ClassTypeInformation SET = new ClassTypeInformation(Set.class); + public static final ClassTypeInformation MAP = new ClassTypeInformation(Map.class); + public static final ClassTypeInformation OBJECT = new ClassTypeInformation(Object.class); + + private static final Map, ClassTypeInformation> cache = new ConcurrentReferenceHashMap<>(64, + ReferenceType.WEAK); + + static { + Arrays.asList(COLLECTION, LIST, SET, MAP, OBJECT).forEach(it -> cache.put(it.getType(), it)); + } + + private final Class type; + + public static void warmCache(ClassTypeInformation... typeInformations) { + for(ClassTypeInformation information : typeInformations) { + cache.put(information.getType(), information); + } + } + + /** + * Simple factory method to easily create new instances of {@link ClassTypeInformation}. + * + * @param + * @param type must not be {@literal null}. + * @return + */ + public static ClassTypeInformation from(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return (ClassTypeInformation) cache.computeIfAbsent(type, ClassTypeInformation::new); + } + + /** + * Creates a {@link TypeInformation} from the given method's return type. + * + * @param method must not be {@literal null}. + * @return + */ + public static TypeInformation fromReturnTypeOf(Method method) { + + Assert.notNull(method, "Method must not be null!"); + return (TypeInformation) ClassTypeInformation.from(method.getDeclaringClass()) + .createInfo(method.getGenericReturnType()); + } + + /** + * Creates {@link ClassTypeInformation} for the given type. + * + * @param type + */ + ClassTypeInformation(Class type) { + super(ProxyUtils.getUserClass(type), getTypeVariableMap(type)); + this.type = type; + } + + ClassTypeInformation(Class type, TypeInformation componentType, TypeInformation keyType) { + super(type, componentType, keyType); + this.type = type; + } + + /** + * Little helper to allow us to create a generified map, actually just to satisfy the compiler. + * + * @param type must not be {@literal null}. + * @return + */ + private static Map, Type> getTypeVariableMap(Class type) { + return getTypeVariableMap(type, new HashSet<>()); + } + + private static Map, Type> getTypeVariableMap(Class type, Collection visited) { + + if (visited.contains(type)) { + return Collections.emptyMap(); + } else { + visited.add(type); + } + + Map source = GenericTypeResolver.getTypeVariableMap(type); + Map, Type> map = new HashMap<>(source.size()); + + for (Entry entry : source.entrySet()) { + + Type value = entry.getValue(); + map.put(entry.getKey(), entry.getValue()); + + if (value instanceof Class) { + + for (Entry, Type> nestedEntry : getTypeVariableMap((Class) value, visited).entrySet()) { + if (!map.containsKey(nestedEntry.getKey())) { + map.put(nestedEntry.getKey(), nestedEntry.getValue()); + } + } + } + } + + return map; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#getType() + */ + @Override + public Class getType() { + return type; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#getRawTypeInformation() + */ + @Override + public ClassTypeInformation getRawTypeInformation() { + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#isAssignableFrom(org.springframework.data.util.TypeInformation) + */ + @Override + public boolean isAssignableFrom(TypeInformation target) { + return getType().isAssignableFrom(target.getType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#specialize(org.springframework.data.util.ClassTypeInformation) + */ + @Override + public TypeInformation specialize(ClassTypeInformation type) { + return (TypeInformation) type; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return type.getName(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/ListTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/ListTypeInformation.java new file mode 100644 index 000000000..49e09019a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/ListTypeInformation.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +import java.util.List; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class ListTypeInformation extends StaticTypeInformation { + + public ListTypeInformation(TypeInformation componentType) { + super(List.class, componentType, null); + } + + @Override + public boolean isCollectionLike() { + return true; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/Person.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/Person.java new file mode 100644 index 000000000..f0177ef1c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/Person.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +import java.util.List; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class Person { + + private long id; + private String firstname, lastname; // TODO: we need a persistence constructor to resolve this here. + private int age; + private Address address; + private List nicknames; + + public Person(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public long getId() { + return id; + } + + public void setId(long id) { + id = id; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public List getNicknames() { + return nicknames; + } + + public void setNicknames(List nicknames) { + this.nicknames = nicknames; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + + ", age=" + age + + ", address=" + address + + ", nicknames=" + nicknames + + '}'; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/PersonTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/PersonTypeInformation.java new file mode 100644 index 000000000..71aa82079 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/PersonTypeInformation.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.ParameterValueProvider; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class PersonTypeInformation extends StaticTypeInformation { + + public PersonTypeInformation() { + super(Person.class); + } + + @Override + protected Map> computePropertiesMap() { + + LinkedHashMap> properties = new LinkedHashMap<>(); + properties.put("firstname", new StringTypeInformation()); + properties.put("lastname", new StringTypeInformation()); + properties.put("id", new StaticTypeInformation<>(Long.class)); + properties.put("age", new StaticTypeInformation<>(int.class)); + properties.put("address", new AddressTypeInformation()); + properties.put("nicknames", new ListTypeInformation(new StringTypeInformation())); + + return properties; + } + + @Override + protected Map> computeSetter() { + + + Map> setter = new LinkedHashMap<>(); + setter.put("id", (bean, id) -> {bean.setId((Long)id); return bean;}); + setter.put("age", (bean, id) -> {bean.setAge((int)id); return bean;}); + setter.put("firstname", (bean, id) -> {bean.setFirstname((String)id); return bean;}); + setter.put("lastname", (bean, id) -> {bean.setLastname((String)id); return bean;}); + setter.put("address", (bean, id) -> {bean.setAddress((Address) id); return bean;}); + setter.put("nicknames", (bean, id) -> {bean.setNicknames((List) id); return bean;}); + + return setter; + } + + @Override + protected Map> computeGetter() { + + Map> getter = new LinkedHashMap<>(); + + getter.put("firstname", Person::getFirstname); + getter.put("lastname", Person::getLastname); + getter.put("id", Person::getId); + getter.put("address", Person::getAddress); + getter.put("nicknames", Person::getNicknames); + getter.put("age", Person::getAge); + + return getter; + } + + @Override + protected EntityInstantiator computeEntityInstantiator() { + + return new EntityInstantiator() { + + @Override + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + String firstname = (String) provider + .getParameterValue(new Parameter("firstanme", new StringTypeInformation(), new Annotation[] {}, entity)); + String lastname = (String) provider + .getParameterValue(new Parameter("firstanme", new StringTypeInformation(), new Annotation[] {}, entity)); + + return (T) new Person(firstname, lastname); + } + }; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformation.java new file mode 100644 index 000000000..cd8073384 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformation.java @@ -0,0 +1,245 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class StaticTypeInformation extends ClassTypeInformation { + + private final Class type; + + @Nullable private final TypeInformation componentType; + @Nullable private final TypeInformation keyType; + + private StaticTypeInformation superTypeInformation; + private List> typeArguments; + private final Map> properties; + private final Map> setter; + private final Map> getter; + + private EntityInstantiator instantiator; + + public StaticTypeInformation(Class type) { + this(type, null, null); + } + + public StaticTypeInformation(Class type, @Nullable TypeInformation componentType, + @Nullable TypeInformation keyType) { + + super(type, componentType, keyType); + this.type = type; + this.componentType = componentType; + this.keyType = keyType; + this.properties = computePropertiesMap(); + this.typeArguments = computeTypeArguments(); + this.instantiator = computeEntityInstantiator(); + this.setter = computeSetter(); + this.getter = computeGetter(); + } + + protected Map> computePropertiesMap() { + return Collections.emptyMap(); + }; + + protected List> computeTypeArguments() { + return Collections.emptyList(); + } + + protected EntityInstantiator computeEntityInstantiator() { + return null; + } + + protected Map> computeSetter() { + return Collections.emptyMap(); + } + + protected Map> computeGetter() { + return Collections.emptyMap(); + } + + public Map> getProperties() { + return properties; + } + + public Map> getSetter() { + return setter; + } + + public Map> getGetter() { + return getter; + } + + public EntityInstantiator getInstantiator() { + return instantiator; + } + + @Override + public List> getParameterTypes(Constructor constructor) { + return null; + } + + @Nullable + @Override + public TypeInformation getProperty(String property) { + return properties.get(property); + } + + @Override + public boolean isCollectionLike() { + return false; + } + + @Override + public boolean isMap() { + return false; + } + + @Nullable + @Override + public TypeInformation getMapValueType() { + return componentType; + } + + @Override + public Class getType() { + return type; + } + + @Override + public ClassTypeInformation getRawTypeInformation() { + return this; + } + + @Nullable + @Override + public TypeInformation getActualType() { + return componentType != null ? componentType : this; + } + + @Override + public TypeInformation getReturnType(Method method) { + return null; + } + + @Override + public List> getParameterTypes(Method method) { + return Collections.emptyList(); + } + + @Nullable + @Override + public TypeInformation getSuperTypeInformation(Class superType) { + return superTypeInformation; + } + + @Override + public boolean isAssignableFrom(TypeInformation target) { + return this.type.isAssignableFrom(target.getType()); + } + + @Override + public List> getTypeArguments() { + return typeArguments; + } + + @Override + public TypeInformation specialize(ClassTypeInformation type) { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + StaticTypeInformation that = (StaticTypeInformation) o; + + if (!ObjectUtils.nullSafeEquals(type, that.type)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(componentType, that.componentType)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(keyType, that.keyType)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(superTypeInformation, that.superTypeInformation)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(typeArguments, that.typeArguments)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(properties, that.properties)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(setter, that.setter)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(getter, that.getter)) { + return false; + } + return ObjectUtils.nullSafeEquals(instantiator, that.instantiator); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(type); + result = 31 * result + ObjectUtils.nullSafeHashCode(componentType); + result = 31 * result + ObjectUtils.nullSafeHashCode(keyType); + result = 31 * result + ObjectUtils.nullSafeHashCode(superTypeInformation); + result = 31 * result + ObjectUtils.nullSafeHashCode(typeArguments); + result = 31 * result + ObjectUtils.nullSafeHashCode(properties); + result = 31 * result + ObjectUtils.nullSafeHashCode(setter); + result = 31 * result + ObjectUtils.nullSafeHashCode(getter); + result = 31 * result + ObjectUtils.nullSafeHashCode(instantiator); + return result; + } + + PreferredConstructor +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformationProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformationProvider.java new file mode 100644 index 000000000..263a74692 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/StaticTypeInformationProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class StaticTypeInformationProvider { + + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/StringTypeInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/StringTypeInformation.java new file mode 100644 index 000000000..30378263c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/StringTypeInformation.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020. 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. + */ + +/* + * Copyright 2020 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.util; + +/** + * @author Christoph Strobl + * @since 2020/10 + */ +public class StringTypeInformation extends StaticTypeInformation { + + public StringTypeInformation() { + super(String.class); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/spring-data-mongodb/src/main/java/org/springframework/data/util/TypeDiscoverer.java new file mode 100644 index 000000000..048d12cc9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -0,0 +1,672 @@ +/* + * Copyright 2011-2020 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.util; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.GenericTypeResolver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Basic {@link TypeDiscoverer} that contains basic functionality to discover property types. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Mark Paluch + */ +class TypeDiscoverer implements TypeInformation { + + private static final Class[] MAP_TYPES; + + static { + + ClassLoader classLoader = TypeDiscoverer.class.getClassLoader(); + + Set> mapTypes = new HashSet<>(); + mapTypes.add(Map.class); + + try { + mapTypes.add(ClassUtils.forName("io.vavr.collection.Map", classLoader)); + } catch (ClassNotFoundException o_O) {} + + MAP_TYPES = mapTypes.toArray(new Class[0]); + } + + private final Type type; + private final Map, Type> typeVariableMap; + private final Map>> fieldTypes = new ConcurrentHashMap<>(); + private final int hashCode; + + private final Lazy> resolvedType; + private final Lazy> componentType; + private final Lazy> valueType; + + /** + * Creates a new {@link TypeDiscoverer} for the given type, type variable map and parent. + * + * @param type must not be {@literal null}. + * @param typeVariableMap must not be {@literal null}. + */ + protected TypeDiscoverer(Type type, Map, Type> typeVariableMap) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(typeVariableMap, "TypeVariableMap must not be null!"); + + this.type = type; + this.resolvedType = Lazy.of(() -> resolveType(type)); + this.componentType = Lazy.of(this::doGetComponentType); + this.valueType = Lazy.of(this::doGetMapValueType); + this.typeVariableMap = typeVariableMap; + this.hashCode = 17 + 31 * type.hashCode() + 31 * typeVariableMap.hashCode(); + } + + protected TypeDiscoverer(Class type, TypeInformation componentType, TypeInformation keyType) { + + this.type = null; + this.typeVariableMap = Collections.emptyMap(); + this.hashCode = 17 + 31 * type.hashCode(); + this.resolvedType = Lazy.of((Class) type); + this.componentType = componentType == null ? Lazy.empty() : Lazy.of(componentType); + this.valueType = keyType == null ? Lazy.empty() : Lazy.of(keyType); + } + + /** + * Returns the type variable map. + * + * @return + */ + protected Map, Type> getTypeVariableMap() { + return typeVariableMap; + } + + /** + * Creates {@link TypeInformation} for the given {@link Type}. + * + * @param fieldType must not be {@literal null}. + * @return + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TypeInformation createInfo(Type fieldType) { + + Assert.notNull(fieldType, "Field type must not be null!"); + + if (fieldType.equals(this.type)) { + return this; + } + + if (fieldType instanceof Class) { + return ClassTypeInformation.from((Class) fieldType); + } + + if (fieldType instanceof ParameterizedType) { + + ParameterizedType parameterizedType = (ParameterizedType) fieldType; + return new ParameterizedTypeInformation(parameterizedType, this); + } + + if (fieldType instanceof TypeVariable) { + + TypeVariable variable = (TypeVariable) fieldType; + return new TypeVariableTypeInformation(variable, this); + } + + if (fieldType instanceof GenericArrayType) { + return new GenericArrayTypeInformation((GenericArrayType) fieldType, this); + } + + if (fieldType instanceof WildcardType) { + + WildcardType wildcardType = (WildcardType) fieldType; + Type[] bounds = wildcardType.getLowerBounds(); + + if (bounds.length > 0) { + return createInfo(bounds[0]); + } + + bounds = wildcardType.getUpperBounds(); + + if (bounds.length > 0) { + return createInfo(bounds[0]); + } + } + + throw new IllegalArgumentException(); + } + + /** + * Resolves the given type into a plain {@link Class}. + * + * @param type + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Class resolveType(Type type) { + + Map map = new HashMap<>(); + map.putAll(getTypeVariableMap()); + + return (Class) GenericTypeResolver.resolveType(type, map); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getParameterTypes(java.lang.reflect.Constructor) + */ + public List> getParameterTypes(Constructor constructor) { + + Assert.notNull(constructor, "Constructor must not be null!"); + + Type[] types = constructor.getGenericParameterTypes(); + List> result = new ArrayList<>(types.length); + + for (Type parameterType : types) { + result.add(createInfo(parameterType)); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getProperty(java.lang.String) + */ + @Nullable + public TypeInformation getProperty(String fieldname) { + + int separatorIndex = fieldname.indexOf('.'); + + if (separatorIndex == -1) { + return fieldTypes.computeIfAbsent(fieldname, this::getPropertyInformation).orElse(null); + } + + String head = fieldname.substring(0, separatorIndex); + TypeInformation info = getProperty(head); + + if (info == null) { + return null; + } + + return info.getProperty(fieldname.substring(separatorIndex + 1)); + } + + /** + * Returns the {@link TypeInformation} for the given atomic field. Will inspect fields first and return the type of a + * field if available. Otherwise it will fall back to a {@link PropertyDescriptor}. + * + * @see #getGenericType(PropertyDescriptor) + * @param fieldname + * @return + */ + @SuppressWarnings("null") + private Optional> getPropertyInformation(String fieldname) { + + Class rawType = getType(); + Field field = ReflectionUtils.findField(rawType, fieldname); + + if (field != null) { + return Optional.of(createInfo(field.getGenericType())); + } + + return findPropertyDescriptor(rawType, fieldname).map(it -> createInfo(getGenericType(it))); + } + + /** + * Finds the {@link PropertyDescriptor} for the property with the given name on the given type. + * + * @param type must not be {@literal null}. + * @param fieldname must not be {@literal null} or empty. + * @return + */ + private static Optional findPropertyDescriptor(Class type, String fieldname) { + + PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(type, fieldname); + + if (descriptor != null) { + return Optional.of(descriptor); + } + + List> superTypes = new ArrayList<>(); + superTypes.addAll(Arrays.asList(type.getInterfaces())); + superTypes.add(type.getSuperclass()); + + return Streamable.of(type.getInterfaces()).stream()// + .flatMap(it -> Optionals.toStream(findPropertyDescriptor(it, fieldname)))// + .findFirst(); + } + + /** + * Returns the generic type for the given {@link PropertyDescriptor}. Will inspect its read method followed by the + * first parameter of the write method. + * + * @param descriptor must not be {@literal null} + * @return + */ + @Nullable + private static Type getGenericType(PropertyDescriptor descriptor) { + + Method method = descriptor.getReadMethod(); + + if (method != null) { + return method.getGenericReturnType(); + } + + method = descriptor.getWriteMethod(); + + if (method == null) { + return null; + } + + Type[] parameterTypes = method.getGenericParameterTypes(); + return parameterTypes.length == 0 ? null : parameterTypes[0]; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getType() + */ + public Class getType() { + return resolvedType.get(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getRawTypeInformation() + */ + @Override + public ClassTypeInformation getRawTypeInformation() { + return ClassTypeInformation.from(getType()).getRawTypeInformation(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getActualType() + */ + @Nullable + public TypeInformation getActualType() { + + if (isMap()) { + return getMapValueType(); + } + + if (isCollectionLike()) { + return getComponentType(); + } + + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#isMap() + */ + public boolean isMap() { + + Class type = getType(); + + for (Class mapType : MAP_TYPES) { + if (mapType.isAssignableFrom(type)) { + return true; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getMapValueType() + */ + @Nullable + public TypeInformation getMapValueType() { + return valueType.orElse(null); + } + + @Nullable + protected TypeInformation doGetMapValueType() { + return isMap() ? getTypeArgument(getBaseType(MAP_TYPES), 1) + : getTypeArguments().stream().skip(1).findFirst().orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#isCollectionLike() + */ + public boolean isCollectionLike() { + + Class rawType = getType(); + + return rawType.isArray() // + || Iterable.class.equals(rawType) // + || Collection.class.isAssignableFrom(rawType) // + || Streamable.class.isAssignableFrom(rawType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getComponentType() + */ + @Nullable + public final TypeInformation getComponentType() { + return componentType.orElse(null); + } + + @Nullable + protected TypeInformation doGetComponentType() { + + Class rawType = getType(); + + if (rawType.isArray()) { + return createInfo(rawType.getComponentType()); + } + + if (isMap()) { + return getTypeArgument(getBaseType(MAP_TYPES), 0); + } + + if (Iterable.class.isAssignableFrom(rawType)) { + return getTypeArgument(Iterable.class, 0); + } + + List> arguments = getTypeArguments(); + + return arguments.size() > 0 ? arguments.get(0) : null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getReturnType(java.lang.reflect.Method) + */ + public TypeInformation getReturnType(Method method) { + + Assert.notNull(method, "Method must not be null!"); + return createInfo(method.getGenericReturnType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getMethodParameterTypes(java.lang.reflect.Method) + */ + public List> getParameterTypes(Method method) { + + Assert.notNull(method, "Method most not be null!"); + + return Streamable.of(method.getGenericParameterTypes()).stream()// + .map(this::createInfo)// + .collect(Collectors.toList()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getSuperTypeInformation(java.lang.Class) + */ + @Nullable + public TypeInformation getSuperTypeInformation(Class superType) { + + Class rawType = getType(); + + if (!superType.isAssignableFrom(rawType)) { + return null; + } + + if (getType().equals(superType)) { + return this; + } + + List candidates = new ArrayList<>(); + Type genericSuperclass = rawType.getGenericSuperclass(); + + if (genericSuperclass != null) { + candidates.add(genericSuperclass); + } + + candidates.addAll(Arrays.asList(rawType.getGenericInterfaces())); + + for (Type candidate : candidates) { + + TypeInformation candidateInfo = createInfo(candidate); + + if (superType.equals(candidateInfo.getType())) { + return candidateInfo; + } else { + + TypeInformation nestedSuperType = candidateInfo.getSuperTypeInformation(superType); + + if (nestedSuperType != null) { + return nestedSuperType; + } + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getTypeParameters() + */ + public List> getTypeArguments() { + return Collections.emptyList(); + } + + /* (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#isAssignableFrom(org.springframework.data.util.TypeInformation) + */ + public boolean isAssignableFrom(TypeInformation target) { + + TypeInformation superTypeInformation = target.getSuperTypeInformation(getType()); + + return superTypeInformation == null ? false : superTypeInformation.equals(this); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#specialize(org.springframework.data.util.ClassTypeInformation) + */ + @Override + @SuppressWarnings("unchecked") + public TypeInformation specialize(ClassTypeInformation type) { + + Assert.notNull(type, "Type must not be null!"); + Assert.isTrue(getType().isAssignableFrom(type.getType()), + () -> String.format("%s must be assignable from %s", getType(), type.getType())); + + List> typeArguments = getTypeArguments(); + + return (TypeInformation) (typeArguments.isEmpty() // + ? type // + : type.createInfo(new SyntheticParamterizedType(type, getTypeArguments()))); + } + + @Nullable + private TypeInformation getTypeArgument(Class bound, int index) { + + Class[] arguments = GenericTypeResolver.resolveTypeArguments(getType(), bound); + + if (arguments != null) { + return createInfo(arguments[index]); + } + + return getSuperTypeInformation(bound) instanceof ParameterizedTypeInformation // + ? ClassTypeInformation.OBJECT // + : null; + } + + private Class getBaseType(Class[] candidates) { + + Class type = getType(); + + for (Class candidate : candidates) { + if (candidate.isAssignableFrom(type)) { + return candidate; + } + } + + throw new IllegalArgumentException(String.format("Type %s not contained in candidates %s!", type, candidates)); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(@Nullable Object obj) { + + if (obj == this) { + return true; + } + + if (obj == null) { + return false; + } + + if (!this.getClass().equals(obj.getClass())) { + return false; + } + + TypeDiscoverer that = (TypeDiscoverer) obj; + + if (!this.type.equals(that.type)) { + return false; + } + + if (this.typeVariableMap.isEmpty() && that.typeVariableMap.isEmpty()) { + return true; + } + + return this.typeVariableMap.equals(that.typeVariableMap); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return hashCode; + } + + /** + * A synthetic {@link ParameterizedType}. + * + * @author Oliver Gierke + * @since 1.11 + */ + private static class SyntheticParamterizedType implements ParameterizedType { + + private final ClassTypeInformation typeInformation; + private final List> typeParameters; + + public SyntheticParamterizedType(ClassTypeInformation typeInformation, List> typeParameters) { + this.typeInformation = typeInformation; + this.typeParameters = typeParameters; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getRawType() + */ + @Override + public Type getRawType() { + return typeInformation.getType(); + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getOwnerType() + */ + @Override + @Nullable + public Type getOwnerType() { + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getActualTypeArguments() + */ + @Override + public Type[] getActualTypeArguments() { + + Type[] result = new Type[typeParameters.size()]; + + for (int i = 0; i < typeParameters.size(); i++) { + result[i] = typeParameters.get(i).getType(); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.ParentTypeAwareTypeInformation#equals(java.lang.Object) + */ + @Override + public boolean equals(@Nullable Object o) { + + if (this == o) { + return true; + } + + if (!(o instanceof SyntheticParamterizedType)) { + return false; + } + + SyntheticParamterizedType that = (SyntheticParamterizedType) o; + + if (!ObjectUtils.nullSafeEquals(typeInformation, that.typeInformation)) { + return false; + } + + return ObjectUtils.nullSafeEquals(typeParameters, that.typeParameters); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(typeInformation); + result = 31 * result + ObjectUtils.nullSafeHashCode(typeParameters); + return result; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 58687d3e2..1279fd90d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -78,7 +78,11 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.PersonPojoStringId; import org.springframework.data.mongodb.core.mapping.TextScore; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; +import org.springframework.data.util.Address; +import org.springframework.data.util.AddressTypeInformation; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Person; +import org.springframework.data.util.PersonTypeInformation; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.BasicDBList; @@ -2178,6 +2182,34 @@ public class MappingMongoConverterUnitTests { assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L); } + + @Test + public void xxx() { + + ClassTypeInformation.warmCache(new PersonTypeInformation(), new AddressTypeInformation()); + + MongoMappingContext mappingContext = new MongoMappingContext(); + mappingContext.setInitialEntitySet(new LinkedHashSet<>(Arrays.asList(org.springframework.data.util.Person.class, org.springframework.data.util.Address.class))); + mappingContext.initialize(); + + org.springframework.data.util.Person source = new org.springframework.data.util.Person("spring", "data"); + source.setAddress(new org.springframework.data.util.Address("the city", "never sleeps")); + source.setAge(10); + source.setId(9876); + + + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + org.bson.Document targetDocument = new org.bson.Document(); + converter.write(source, targetDocument); + + System.out.println("target: " + targetDocument); + + org.springframework.data.util.Person targetEntity = converter.read(org.springframework.data.util.Person.class, targetDocument); + System.out.println("targetEntity: " + targetEntity); + + + } static class GenericType { T content;