# ProjectionQueryExecutor.java

    package org.codefilarete.stalactite.spring.repository.query.projection;

    import java.util.HashMap;
    import java.util.IdentityHashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.function.BiConsumer;
    import java.util.function.Function;
    import java.util.function.Supplier;

    import org.codefilarete.reflection.AccessorByMember;
    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.stalactite.engine.EntityCriteria.OrderByChain.Order;
    import org.codefilarete.stalactite.engine.EntityPersister.ExecutableProjectionQuery;
    import org.codefilarete.stalactite.engine.ExecutableProjection.ProjectionDataProvider;
    import org.codefilarete.stalactite.engine.runtime.projection.ProjectionQueryCriteriaSupport;
    import org.codefilarete.stalactite.engine.runtime.projection.ProjectionQueryPageSupport;
    import org.codefilarete.stalactite.query.api.JoinLink;
    import org.codefilarete.stalactite.query.model.Limit;
    import org.codefilarete.stalactite.spring.repository.query.StalactiteQueryMethod;
    import org.codefilarete.stalactite.spring.repository.query.execution.AbstractQueryExecutor;
    import org.codefilarete.stalactite.spring.repository.query.execution.StalactiteQueryMethodInvocationParameters;
    import org.codefilarete.stalactite.sql.result.Accumulator;
    import org.springframework.data.domain.Sort.Direction;
    import org.springframework.data.repository.query.Parameter;

    import static org.codefilarete.stalactite.spring.repository.query.execution.AbstractRepositoryQuery.buildAliases;

    /**
     * Implementation of {@link AbstractQueryExecutor} dedicated to projections.
     * Implementation is based on {@link ProjectionQueryCriteriaSupport}: a copy from the default one is made at construction time to have a dedicated
     * instance for each derived query.
     *
     * @param <C> domain entity type
     * @author Guillaume Mary
     */
    class ProjectionQueryExecutor<C> extends AbstractQueryExecutor<List<Object>, Object> {

    	private final ProjectionQueryCriteriaSupport<C, ?> projectionQueryCriteriaSupport;
    	private final Accumulator<ProjectionDataProvider, List<Map<String, Object>>, List<Map<String, Object>>> accumulator;

    	public ProjectionQueryExecutor(StalactiteQueryMethod method,
    								   ProjectionQueryCriteriaSupport<C, ?> defaultProjectionQueryCriteriaSupport,
    								   IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties) {
    		super(method);
    		IdentityHashMap<JoinLink<?, ?>, String> aliases = buildAliases(columnToProperties);
    		// we "clone" the default projection query to make our own, dedicated to the derived query
    		this.projectionQueryCriteriaSupport = defaultProjectionQueryCriteriaSupport.copyFor(select -> {
    			columnToProperties.keySet().forEach(selectable -> {
    				select.add(selectable, aliases.get(selectable));
    			});
    		});
    		this.accumulator = new TupleAccumulator(columnToProperties);
    	}

    	@Override
    	public Supplier<List<Object>> buildQueryExecutor(StalactiteQueryMethodInvocationParameters invocationParameters) {
    		return () -> {
    			ExecutableProjectionQuery<C, ?> projectionQuery = handleDynamicParameters(invocationParameters, projectionQueryCriteriaSupport);

    			int i = 1;
    			for (Parameter bindableParameter : invocationParameters.getParameters().getBindableParameters()) {
    				Object value = invocationParameters.getBindableValue(bindableParameter.getIndex());
    				// Note that we don't apply the value to the criteria of the derived query template, else we temper with it for next execution
    				// defaultDerivedQuery.criteriaChain.criteria.get(i).setValue(..);
    				projectionQuery.set(String.valueOf(i++), value);
    			}

    			return (List<Object>) (List) projectionQuery.execute(accumulator);
    		};
    	}

    	private ExecutableProjectionQuery<C, ?> handleDynamicParameters(StalactiteQueryMethodInvocationParameters invocationParameters,
    																	ProjectionQueryCriteriaSupport<C, ?> actualProjectionQueryCriteriaSupport) {
    		ProjectionQueryCriteriaSupport<C, ?> derivedQueryToUse;
    		// following code will manage both Sort as an argument, and Sort in a Pageable because getSort() handle both
    		if (invocationParameters.getSort().isSorted()) {
    			Class<?> declaringClass = method.getEntityInformation().getJavaType();
    			// Spring Sort class supports only first-level properties, in-depth ones seems not to be definable,
    			// therefore we create AccessorChain of only one property
    			ProjectionQueryPageSupport<C> dynamicSortSupport = new ProjectionQueryPageSupport<>();
    			invocationParameters.getSort().stream().forEachOrdered(order -> {
    				AccessorByMember<?, ?, ?> accessor = Accessors.accessor(declaringClass, order.getProperty());
    				dynamicSortSupport.orderBy(new AccessorChain<>(accessor),
    						order.getDirection() == Direction.ASC ? Order.ASC : Order.DESC,
    						order.isIgnoreCase());
    			});
    			derivedQueryToUse = actualProjectionQueryCriteriaSupport.copyFor(dynamicSortSupport);
    		} else {
    			derivedQueryToUse = actualProjectionQueryCriteriaSupport;
    		}
    		ExecutableProjectionQuery<C, ?> result = derivedQueryToUse.wrapIntoExecutable();
    		Limit limit = invocationParameters.getLimit();
    		if (limit != null) {
    			result.limit(limit.getCount(), limit.getOffset());
    		}
    		return result;
    	}

    	private static class TupleAccumulator<C> implements Accumulator<ProjectionDataProvider, List<Map<String, Object>>, List<Map<String, Object>>> {
    		private final IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties;

    		public TupleAccumulator(IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties) {
    			this.columnToProperties = columnToProperties;
    		}

    		@Override
    		public Supplier<List<Map<String, Object>>> supplier() {
    			return LinkedList::new;
    		}

    		@Override
    		public BiConsumer<List<Map<String, Object>>, ProjectionDataProvider> aggregator() {
    			return (finalResult, databaseRowDataProvider) -> {
    				Map<String, Object> row = new HashMap<>();
    				finalResult.add(row);
    				for (Entry<JoinLink<?, ?>, AccessorChain<C, ?>> entry : columnToProperties.entrySet()) {
    					PartTreeStalactiteProjection.buildHierarchicMap(entry.getValue(), databaseRowDataProvider.getValue(entry.getKey()), row);
    				}
    			};
    		}

    		@Override
    		public Function<List<Map<String, Object>>, List<Map<String, Object>>> finisher() {
    			return Function.identity();
    		}
    	}
    }