# EntityQueryCriteriaSupport.java

    package org.codefilarete.stalactite.engine.runtime.query;

    import java.util.Collection;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeSet;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    import org.codefilarete.reflection.AbstractReflector;
    import org.codefilarete.reflection.Accessor;
    import org.codefilarete.reflection.AccessorByMember;
    import org.codefilarete.reflection.AccessorByMethodReference;
    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.reflection.MethodReferenceDispatcher;
    import org.codefilarete.reflection.MutatorByMethodReference;
    import org.codefilarete.reflection.ReversibleMutator;
    import org.codefilarete.reflection.ValueAccessPoint;
    import org.codefilarete.stalactite.engine.EntityCriteria;
    import org.codefilarete.stalactite.engine.EntityPersister.ExecutableEntityQuery;
    import org.codefilarete.stalactite.engine.EntityCriteria.LimitAware;
    import org.codefilarete.stalactite.engine.EntityCriteria.OrderByChain;
    import org.codefilarete.stalactite.engine.EntityCriteria.OrderByChain.Order;
    import org.codefilarete.stalactite.engine.ExecutableQuery;
    import org.codefilarete.stalactite.engine.listener.PersisterListenerCollection;
    import org.codefilarete.stalactite.engine.runtime.RelationalEntityPersister.ExecutableEntityQueryCriteria;
    import org.codefilarete.stalactite.engine.runtime.query.EntityQueryCriteriaSupport.EntityQueryPageSupport.OrderByItem;
    import org.codefilarete.stalactite.query.ConfiguredEntityCriteria;
    import org.codefilarete.stalactite.query.EntityFinder;
    import org.codefilarete.stalactite.query.RelationalEntityCriteria;
    import org.codefilarete.stalactite.query.model.CriteriaChain;
    import org.codefilarete.stalactite.query.model.Limit;
    import org.codefilarete.stalactite.query.model.Operators;
    import org.codefilarete.stalactite.query.model.OrderBy;
    import org.codefilarete.stalactite.query.model.Selectable;
    import org.codefilarete.stalactite.sql.result.Accumulator;
    import org.codefilarete.tool.Nullable;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.collection.KeepOrderSet;
    import org.codefilarete.tool.function.SerializableTriFunction;
    import org.codefilarete.tool.function.ThrowingExecutable;
    import org.danekja.java.util.function.serializable.SerializableBiConsumer;
    import org.danekja.java.util.function.serializable.SerializableBiFunction;
    import org.danekja.java.util.function.serializable.SerializableFunction;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import static org.codefilarete.tool.Nullable.nullable;

    /**
     * <ul>
     * Class aimed at handling entity query configuration and execution triggering :
     * <li>query configuration will be done by redirecting {@link CriteriaChain} methods to an {@link EntityQueryCriteriaSupport}.</li>
     * <li>execution triggering calls {@link EntityFinder#select(ConfiguredEntityCriteria, Map, OrderBy, Limit)}
     * and wraps it into {@link PersisterListenerCollection#doWithSelectListener(Iterable, ThrowingExecutable)}</li>
     * </ul>
     *
     * @param <C> entity type
     * @param <I> identifier type
     * @author Guillaume Mary
     */
    public class EntityQueryCriteriaSupport<C, I> {

    	public static final Logger LOGGER = LoggerFactory.getLogger(EntityQueryCriteriaSupport.class);

    	/** Support for {@link EntityCriteria} query execution */
    	private final EntityFinder<C, I> entityFinder;

    	private final EntityCriteriaSupport<C> entityCriteriaSupport;

    	private final EntityQueryPageSupport<C> queryPageSupport;

    	public EntityQueryCriteriaSupport(EntityFinder<C, I> entityFinder,
    									  EntityCriteriaSupport<C> entityCriteriaSupport) {
    		this(entityFinder, entityCriteriaSupport, new EntityQueryPageSupport<>());
    	}

    	private EntityQueryCriteriaSupport(EntityFinder<C, I> entityFinder,
    									   EntityCriteriaSupport<C> entityCriteriaSupport,
    									   EntityQueryPageSupport<C> queryPageSupport) {
    		this.entityFinder = entityFinder;
    		this.entityCriteriaSupport = entityCriteriaSupport;
    		this.queryPageSupport = queryPageSupport;
    	}

    	/**
    	 * Makes a copy of this instance merged with given one
    	 * Made to handle Spring Data's different ways of sorting (should have been put closer to its usage, but was too complex)
    	 *
    	 * @param otherPageSupport some other paging options
    	 * @return a merge of this instance with given page options
    	 */
    	public EntityQueryCriteriaSupport<C, I> copyFor(EntityQueryPageSupport<C> otherPageSupport) {
    		return new EntityQueryCriteriaSupport<>(entityFinder, entityCriteriaSupport, queryPageSupport.merge(otherPageSupport));
    	}

    	public EntityCriteriaSupport<C> getEntityCriteriaSupport() {
    		return entityCriteriaSupport;
    	}

    	public EntityQueryPageSupport<C> getQueryPageSupport() {
    		return queryPageSupport;
    	}

    	public ExecutableEntityQueryCriteria<C, ?> wrapIntoExecutable() {
    		Map<String, Object> values = new HashMap<>();
    		MethodReferenceDispatcher methodDispatcher = new MethodReferenceDispatcher();
    		return methodDispatcher
    				.redirect((SerializableBiFunction<ExecutableQuery<C>, Accumulator<C, ? extends Collection<C>, Object>, Object>) ExecutableQuery::execute,
    						wrapGraphLoad(values))
    				.redirect((SerializableTriFunction<ExecutableEntityQuery<?, ?>, String, Object, Object>) ExecutableEntityQuery::set,
    						// Don't use "values::put" because its signature returns previous value, which means it is a Function
    						// and dispatch to redirect(..) that takes a Function as argument, which, at runtime,
    						// will create some ClassCastException due to incompatible type between ExecutableEntityQuery
    						// and values contained in the Map (because ExecutableEntityQuery::set returns ExecutableEntityQuery)
    						(s, object) -> { values.put(s, object); }
    				)
    				.redirect(OrderByChain.class, queryPageSupport, true)
    				.redirect(LimitAware.class, queryPageSupport, true)
    				.redirect(RelationalEntityCriteria.class, entityCriteriaSupport, true)
    				// making an exception for 2 of the methods that can't return the proxy
    				.redirect((SerializableFunction<ConfiguredEntityCriteria, CriteriaChain>) ConfiguredEntityCriteria::getCriteria, entityCriteriaSupport::getCriteria)
    				.redirect((SerializableFunction<ConfiguredEntityCriteria, Boolean>) ConfiguredEntityCriteria::hasCollectionCriteria, entityCriteriaSupport::hasCollectionCriteria)
    				.build((Class<ConfiguredExecutableEntityQueryCriteria<C>>) (Class) ConfiguredExecutableEntityQueryCriteria.class);
    	}

    	/**
    	 * A mashup to redirect all {@link ExecutableEntityQueryCriteria} methods being redirected to {@link EntityCriteriaSupport} while redirecting
    	 * {@link ConfiguredEntityCriteria} methods to some specific methods of {@link EntityCriteriaSupport}.
    	 * Made as such to avoid to expose internal / implementation methods "getCriteria" and "hasCollectionCriteria" to the
    	 * configuration API ({@link ExecutableEntityQueryCriteria})
    	 *
    	 * @param <C>
    	 * @author Guillaume Mary
    	 */
    	private interface ConfiguredExecutableEntityQueryCriteria<C> extends ConfiguredEntityCriteria, ExecutableEntityQueryCriteria<C, ConfiguredExecutableEntityQueryCriteria<C>> {

    	}

    	public <R> Function<Accumulator<C, ? extends Collection<C>, R>, R> wrapGraphLoad(Map<String, Object> values) {
    		if (queryPageSupport.getLimit() != null && entityCriteriaSupport.hasCollectionProperty()) {
    			throw new UnsupportedOperationException("Can't limit query when entity graph contains Collection relations");
    		}
    		if (entityCriteriaSupport.hasCollectionCriteria() && !queryPageSupport.getOrderBy().isEmpty()) {
    			// a collection property in criteria will trigger a 2-phases load (ids, then entities)
    			// which is no compatible with an SQL "order by" clause, therefore, we sort the result in memory
    			// and we don't ask for SQL "order by" because it's useless
    			// Note that we must wrap this creation in an if statement due to that entities don't implement Comparable, we avoid a
    			// ClassCastException of the addAll(..) operation
    			return (Accumulator<C, ? extends Collection<C>, R> accumulatorParam) -> {
    				LOGGER.debug("Sorting loaded entities in memory");
    				Set<C> loadedEntities = entityFinder.select(
    						entityCriteriaSupport,
    						values,
    						new OrderBy(),
    						queryPageSupport.getLimit());
    				TreeSet<C> sortedResult = new TreeSet<>(buildComparator(queryPageSupport.getOrderBy()));
    				sortedResult.addAll(loadedEntities);
    				return accumulatorParam.collect(sortedResult);
    			};
    		} else {
    			// single query
    			return (Accumulator<C, ? extends Collection<C>, R> accumulatorParam) -> {
    				OrderBy orderBy = new OrderBy();
    				queryPageSupport.getOrderBy().forEach(duo -> {
    					Selectable column = entityCriteriaSupport.getAggregateColumnMapping().giveColumn(duo.getProperty());
    					orderBy.add(
    							duo.isIgnoreCase()
    									? Operators.lowerCase(column)
    									: column,
    							duo.getDirection() == Order.ASC
    									? org.codefilarete.stalactite.query.model.OrderByChain.Order.ASC
    									: org.codefilarete.stalactite.query.model.OrderByChain.Order.DESC);
    				});
    				Set<C> select = entityFinder.select(
    						entityCriteriaSupport,
    						values,
    						orderBy,
    						queryPageSupport.getLimit()
    				);
    				return accumulatorParam.collect(select);
    			};
    		}
    	}

    	@VisibleForTesting
    	static <C> Comparator<C> buildComparator(KeepOrderSet<OrderByItem> orderBy) {
    		Nullable<Comparator> result = nullable((Comparator) null);
    		orderBy.forEach(orderByPawn -> {
    			AccessorChain<Object, Comparable> propertyAccessor = orderByPawn.propertyAsAccessorChain();
    			Comparator comparator;
    			if (orderByPawn.isIgnoreCase()) {
    				AccessorChain<Object, String> stringAccessor = (AccessorChain<Object, String>) (AccessorChain) propertyAccessor;
    				comparator = Comparator.comparing(stringAccessor::get, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER));
    			} else {
    				comparator = Comparator.comparing(propertyAccessor::get, Comparator.nullsLast(Comparator.naturalOrder()));
    			}
    			if (orderByPawn.getDirection() == Order.DESC) {
    				comparator = comparator.reversed();
    			}
    			if (result.isPresent()) {
    				Comparator finalComparator = comparator;
    				result.map(c -> c.thenComparing(finalComparator));
    			} else {
    				result.set(comparator);
    			}
    		});
    		return result.get();
    	}

    	/**
    	 * Gives the {@link Accessor} underneath given {@link ValueAccessPoint}, either being itself or its mirror if it's a {@link ReversibleMutator}
    	 * @param valueAccessPoint a property accessor from which we need an {@link Accessor}
    	 * @param <C> declaring class type
    	 * @param <T> property type
    	 * @return given {@link ValueAccessPoint} as an {@link Accessor}
    	 */
    	private static <C, T> Accessor<C, T> toAccessor(ValueAccessPoint<C> valueAccessPoint) {
    		if (valueAccessPoint instanceof Accessor) {
    			return (Accessor<C, T>) valueAccessPoint;
    		} else if (valueAccessPoint instanceof ReversibleMutator) {
    			return ((ReversibleMutator<C, T>) valueAccessPoint).toAccessor();
    		} else {
    			AccessorDefinition accessorDefinition = AccessorDefinition.giveDefinition(valueAccessPoint);
    			AccessorByMember accessor = Accessors.accessor(accessorDefinition.getDeclaringClass(), accessorDefinition.getName(), accessorDefinition.getMemberType());
    			return (Accessor<C, T>) accessor;
    		}
    	}

    	/**
    	 * Simple class that stores paging options of the query
    	 * @author Guillaume Mary
    	 */
    	public static class EntityQueryPageSupport<C>
    			implements OrderByChain<C, EntityQueryPageSupport<C>>, LimitAware<EntityQueryPageSupport<C>> {

    		private Limit limit;
    		private final KeepOrderSet<OrderByItem> orderBy = new KeepOrderSet<>();

    		private Limit getLimit() {
    			return limit;
    		}

    		private KeepOrderSet<OrderByItem> getOrderBy() {
    			return orderBy;
    		}

    		@Override
    		public EntityQueryPageSupport<C> limit(int count) {
    			limit = new Limit(count);
    			return this;
    		}

    		@Override
    		public EntityQueryPageSupport<C> limit(int count, Integer offset) {
    			limit = new Limit(count, offset);
    			return this;
    		}

    		@Override
    		public EntityQueryPageSupport<C> orderBy(SerializableFunction<C, ?> getter, Order order) {
    			AccessorByMethodReference<C, ?> methodReference = new AccessorByMethodReference<>(getter);
    			orderBy.add(new OrderByItem(Arrays.asList(methodReference), order, false));
    			assertAccessorIsNotIterable(methodReference, methodReference.getPropertyType());
    			return this;
    		}

    		@Override
    		public EntityQueryPageSupport<C> orderBy(SerializableBiConsumer<C, ?> setter, Order order) {
    			MutatorByMethodReference<C, ?> methodReference = new MutatorByMethodReference<>(setter);
    			orderBy.add(new OrderByItem(Arrays.asList(methodReference), order, false));
    			assertAccessorIsNotIterable(methodReference, methodReference.getPropertyType());
    			return this;
    		}

    		@Override
    		public EntityQueryPageSupport<C> orderBy(AccessorChain<C, ?> getter, Order order) {
    			return orderBy(getter, order, false);
    		}

    		@Override
    		public EntityQueryPageSupport<C> orderBy(AccessorChain<C, ?> getter, Order order, boolean ignoreCase) {
    			orderBy.add(new OrderByItem(getter.getAccessors(), order, ignoreCase));
    			getter.getAccessors().forEach(accessor -> assertAccessorIsNotIterable(accessor, AccessorDefinition.giveDefinition(accessor).getMemberType()));
    			return this;
    		}

    		private void assertAccessorIsNotIterable(ValueAccessPoint valueAccessPoint, Class memberType) {
    			if (Iterable.class.isAssignableFrom(memberType)) {
    				throw new IllegalArgumentException("OrderBy clause on a Collection property is unsupported due to eventual inconsistency"
    						+ " with Collection nature : "
    						+ (valueAccessPoint instanceof AbstractReflector
    							? ((AbstractReflector<?>) valueAccessPoint).getDescription()
    							: AccessorDefinition.giveDefinition(valueAccessPoint)).toString());
    			}
    		}

    		/**
    		 * Creates a copy of this instance by merging its options with another.
    		 * Made to handle Spring Data's different ways of sorting (should have been put closer to its usage, but was too complex)
    		 *
    		 * @param other some other paging options
    		 * @return a merge of this instance with given one
    		 */
    		private EntityQueryPageSupport<C> merge(EntityQueryPageSupport<C> other) {
    			EntityQueryPageSupport<C> duplicate = new EntityQueryPageSupport<>();
    			// applying this instance's limit and orderBy options
    			if (this.getLimit() != null) {
    				duplicate.limit(this.getLimit().getCount(), this.getLimit().getOffset());
    			}
    			duplicate.orderBy.addAll(this.orderBy);
    			// adding other instance's limit and orderBy options (may overwrite info, but that's user responsibility, we can't do anything smart here)
    			if (other.getLimit() != null) {
    				duplicate.limit(other.getLimit().getCount(), other.getLimit().getOffset());
    			}
    			duplicate.orderBy.addAll(other.orderBy);
    			return duplicate;
    		}

    		public static class OrderByItem {

    			private final List<? extends ValueAccessPoint<?>> property;
    			private final Order direction;
    			private final boolean ignoreCase;

    			public OrderByItem(List<? extends ValueAccessPoint<?>> property, Order direction, boolean ignoreCase) {
    				this.property = property;
    				this.direction = direction;
    				this.ignoreCase = ignoreCase;
    			}

    			public List<? extends ValueAccessPoint<?>> getProperty() {
    				return property;
    			}

    			public Order getDirection() {
    				return direction;
    			}

    			public boolean isIgnoreCase() {
    				return ignoreCase;
    			}

    			public AccessorChain<Object, Comparable> propertyAsAccessorChain() {
    				AccessorChain<Object, Comparable> result;
    				if (property.size() == 1) {
    					ValueAccessPoint<?> valueAccessPoint = property.get(0);
    					if (valueAccessPoint instanceof Accessor) {
    						result = new AccessorChain<>((Accessor<?, Comparable>) valueAccessPoint);
    					} else {
    						AccessorDefinition accessorDefinition = AccessorDefinition.giveDefinition(valueAccessPoint);
    						result = new AccessorChain<>(Accessors.accessor(accessorDefinition.getDeclaringClass(), accessorDefinition.getName(), accessorDefinition.getMemberType()));
    					}
    				} else {
    					result = new AccessorChain<>(property.stream().map(EntityQueryCriteriaSupport::toAccessor).collect(Collectors.toList()));
    				}
    				result.setNullValueHandler(AccessorChain.RETURN_NULL);
    				return result;
    			}
    		}
    	}
    }