ProjectionMappingFinder.java
package org.codefilarete.stalactite.spring.repository.query.projection;
import java.util.IdentityHashMap;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.stalactite.engine.runtime.AdvancedEntityPersister;
import org.codefilarete.stalactite.engine.runtime.query.AggregateAccessPointToColumnMapping;
import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.query.model.Selectable;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
/**
* Helper class to provide the {@link Selectable}s necessary to build some projection instance from a database query (native or not).
* The columns are taken from the aggregate type.
*
* @param <C> the aggregate (root entity) type
* @author Guillaume Mary
*/
public class ProjectionMappingFinder<C> {
private final AggregateAccessPointToColumnMapping<C> aggregateColumnMapping;
private final EntityProjectionIntrospector entityProjectionIntrospector;
private final Class<C> aggregateType;
/**
* Initiates an instance capable of collecting aliases and their matching property of the aggregate of the given entityPersister
*
* @param factory
* @param entityPersister
*/
public ProjectionMappingFinder(ProjectionFactory factory, AdvancedEntityPersister<C, ?> entityPersister) {
// The projection is closed: it means there's not @Value on the interface, so we can use Spring property introspector to look up for
// properties to select in the query
// If the projection is open (any method as a @Value on it), then, because Spring can't know in advance which field will be required to
// evaluate the @Value expression, we must retrieve the whole aggregate as entities.
// se https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html
EntityProjectionIntrospector.ProjectionPredicate isProjectionTest = (returnType, domainType)
-> !returnType.isAssignableFrom(domainType);
// We create a dumb MappingContext (un-typed) to "just make it work". I'm unsure about the way it works, but implementing it is a bit
// complex for Stalactite due to the need to have PersistentEntity which requires also some concepts like PreferredConstructor or InstanceCreatorMetadata
// => to be implemented if this dumb implementation is unsufficient
MappingContext mappingContext = new RelationalMappingContext();
this.entityProjectionIntrospector = EntityProjectionIntrospector.create(factory, isProjectionTest, mappingContext);
this.aggregateColumnMapping = entityPersister.getEntityFinder().newCriteriaSupport().getEntityCriteriaSupport().getAggregateColumnMapping();
this.aggregateType = entityPersister.getClassToPersist();
}
/**
* Extracts the {@link JoinLink} and {@link PropertyPath} from the {@link ProjectionFactory} and {@link AdvancedEntityPersister} of construction time.
* The algorithm is based on Spring property introspection to make us match the way it detects the properties of a projection. Thus, we are much more
* compatible with Spring Data than if we re-invent the wheel. Meanwhile, the will to re-invent it is very tempting because the algorithm is unclear,
* not well-documented, with a lot of closed / private classes.
*
* @param projectionTypeToIntrospect the projection type to introspect
* @return a map of {@link JoinLink} to {@link PropertyPath}
*/
public IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> lookup(Class<?> projectionTypeToIntrospect) {
IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> result = new IdentityHashMap<>();
EntityProjection<?, C> projectionTypeIntrospection = entityProjectionIntrospector.introspect(projectionTypeToIntrospect, aggregateType);
projectionTypeIntrospection.forEachRecursive(projectionProperty -> {
AccessorChain accessorChain = convertToAccessorChain(projectionProperty.getPropertyPath());
try {
JoinLink<?, ?> selectable = aggregateColumnMapping.giveColumn(accessorChain.getAccessors());
result.put(selectable, accessorChain);
} catch (RuntimeException e) {
// MADE TO AVOID Error while looking for column of o.c.s.e.m.Republic.getPrimeMinister() : it is not declared in mapping of o.c.s.e.m.Republic
}
});
return result;
}
private AccessorChain<?, ?> convertToAccessorChain(PropertyPath propertyPath2) {
AccessorChain<?, ?> result = new AccessorChain<>();
propertyPath2.forEach(propertyPath -> {
result.add(Accessors.accessor(propertyPath.getOwningType().getType(), propertyPath.getSegment(), propertyPath.getType()));
});
return result;
}
}