# AbstractDerivedQuery.java

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

    import java.util.ArrayList;
    import java.util.List;

    import org.codefilarete.reflection.Accessor;
    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.stalactite.query.model.ConditionalOperator;
    import org.codefilarete.stalactite.query.model.operator.Between;
    import org.codefilarete.stalactite.query.model.operator.Equals;
    import org.codefilarete.stalactite.query.model.operator.Greater;
    import org.codefilarete.stalactite.query.model.operator.In;
    import org.codefilarete.stalactite.query.model.operator.InIgnoreCase;
    import org.codefilarete.stalactite.query.model.operator.IsNull;
    import org.codefilarete.stalactite.query.model.operator.Lesser;
    import org.codefilarete.stalactite.query.model.operator.Like;
    import org.codefilarete.tool.collection.Arrays;
    import org.springframework.data.domain.Sort.Order;
    import org.springframework.data.mapping.PropertyPath;
    import org.springframework.data.repository.query.parser.Part;
    import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
    import org.springframework.data.repository.query.parser.Part.Type;

    /**
     * Base class to build a query from a method.
     *
     * @param <T>
     * @author Guillaume Mary
     */
    public abstract class AbstractDerivedQuery<T> {

    	private final Class<T> entityType;

    	public AbstractDerivedQuery(Class<T> entityType) {
    		this.entityType = entityType;
    	}

    	public Criterion convertToCriterion(Part part) {
    		return convertToCriterion(part.getType(), part.shouldIgnoreCase() != IgnoreCaseType.NEVER);
    	}

    	private Criterion convertToCriterion(Type type, boolean ignoreCase) {
    		ConditionalOperator<?, ?> operator = null;
    		switch (type) {
    			case BETWEEN:
    				operator = new Between<>();
    				break;
    			case IS_NOT_NULL:
    				operator = new IsNull().not();
    				break;
    			case IS_NULL:
    				operator = new IsNull();
    				break;
    			case BEFORE:
    			case LESS_THAN:
    				operator = new Lesser<>();
    				break;
    			case LESS_THAN_EQUAL:
    				operator = new Lesser<>().equals();
    				break;
    			case AFTER:
    			case GREATER_THAN:
    				operator = new Greater<>();
    				break;
    			case GREATER_THAN_EQUAL:
    				operator = new Greater<>().equals();
    				break;
    			case NOT_LIKE:
    				operator = new Like<>(false, false).not();
    				if (ignoreCase) {
    					operator = ((Like) operator).ignoringCase();
    				}
    				break;
    			case LIKE:
    				operator = new Like<>(false, false);
    				if (ignoreCase) {
    					operator = ((Like) operator).ignoringCase();
    				}
    				break;
    			case STARTING_WITH:
    				operator = Like.startsWith();
    				break;
    			case ENDING_WITH:
    				operator = Like.endsWith();
    				break;
    			case IS_NOT_EMPTY:
    				break;
    			case IS_EMPTY:
    				break;
    			case NOT_CONTAINING:
    				operator = Like.contains().not();
    				break;
    			case CONTAINING:
    				operator = Like.contains();
    				break;
    			case NOT_IN:
    				operator = new In<>().not();
    				if (ignoreCase) {
    					operator = ((In) operator).ignoringCase();
    				}
    				break;
    			case IN:
    				operator = new In<>();
    				if (ignoreCase) {
    					operator = ((In) operator).ignoringCase();
    				}
    				break;
    			case TRUE:
    				operator = new Equals<>(true);
    				break;
    			case FALSE:
    				operator = new Equals<>(false);
    				break;
    			case NEGATING_SIMPLE_PROPERTY:
    				operator = new Equals<>().not();
    				if (ignoreCase) {
    					operator = ((Equals) operator).ignoringCase();
    				}
    				break;
    			case SIMPLE_PROPERTY:
    				operator = new Equals<>();
    				if (ignoreCase) {
    					operator = ((Equals) operator).ignoringCase();
    				}
    				break;
    			// Hereafter operators are not supported for different reasons :
    			// - JPA doesn't either,
    			// - too database-specific,
    			// - don't know on which operator to bind them (no javadoc)
    			case NEAR:
    			case WITHIN:
    			case REGEX:
    			case EXISTS:
    		}
    		if (operator == null) {
    			throw new UnsupportedOperationException("Unsupported operator type: " + type);
    		} else {
    			// Adapting result depending on kind of operator was instantiated
    			if (operator instanceof Between) {
    				return new BetweenCriterion((Between<?>) operator);
    			} else if (operator instanceof In || operator instanceof InIgnoreCase) {
    				return new InCriterion(operator);
    			} else if (operator instanceof IsNull) {
    				return new IsNullCriterion((IsNull) operator);
    			} else if (type == Type.TRUE || type == Type.FALSE) {
    				return new BooleanCriterion(operator);
    			} else {
    				return new Criterion(operator);
    			}
    		}
    	}

    	protected <O> AccessorChain<T, O> convertToAccessorChain(Order property) {
    		return this.convertToAccessorChain(PropertyPath.from(property.getProperty(), entityType));
    	}

    	protected <O> AccessorChain<T, O> convertToAccessorChain(PropertyPath property) {
    		List<Accessor<?, ?>> accessorChain = new ArrayList<>();
    		property.forEach(path ->
    				accessorChain.add(Accessors.accessor(path.getOwningType().getType(), path.getSegment())));
    		return new AccessorChain<>(accessorChain);
    	}

    	/**
    	 * Combination of an operator and its expected argument count.
    	 * Helps to consume runtime arguments, see {@link #consume(Object[])}
    	 *
    	 * @author Guillaume Mary
    	 */
    	public static class Criterion {

    		protected final ConditionalOperator<Object, Object> condition;
    		protected final int argumentCount;

    		private Criterion(ConditionalOperator<?, ?> condition) {
    			this(condition, 1);
    		}

    		public Criterion(ConditionalOperator<?, ?> condition, int argumentCount) {
    			this.condition = (ConditionalOperator<Object, Object>) condition;
    			this.argumentCount = argumentCount;
    		}

    		public void setValue(Object[] arguments, int argumentIndex) {
    			this.condition.setValue(convert(arguments, argumentIndex));
    		}

    		protected Object convert(Object[] arguments, int argumentIndex) {
    			return arguments[argumentIndex];
    		}
    	}

    	private static class BetweenCriterion extends Criterion {

    		public BetweenCriterion(Between<?> operator) {
    			super(operator, 2);
    		}

    		@Override
    		public Object convert(Object[] arguments, int argumentIndex) {
    			return new Between.Interval<>(arguments[argumentIndex], arguments[argumentIndex + 1]);
    		}
    	}

    	private static class IsNullCriterion extends Criterion {

    		public IsNullCriterion(IsNull operator) {
    			super(operator, 0);
    		}

    		@Override
    		public void setValue(Object[] arguments, int argumentIndex) {
    			// IsNull doesn't set any value (at least it does nothing)
    			// moreover default behavior will throw exception due to argument position computation:
    			// findByNameIsNull() has no arg => arguments is empty making arguments[0] throws an IndexOutOfBoundsException
    		}
    	}

    	private static class InCriterion extends Criterion {
    		public InCriterion(ConditionalOperator<?, ?> operator) {
    			super(operator, 2);
    		}

    		@Override
    		public Object convert(Object[] arguments, int argumentIndex) {
    			Object argument = arguments[argumentIndex];
    			if (argument instanceof Object[]) {
    				// converting varargs to an Iterable because it's what's expected by "In" class as value
    				return Arrays.asList((Object[]) argument);
    			} else {
    				return argument;
    			}
    		}
    	}

    	private static class BooleanCriterion extends Criterion {
    		public BooleanCriterion(ConditionalOperator<?, ?> operator) {
    			super(operator, 0);
    		}

    		@Override
    		public void setValue(Object[] arguments, int argumentIndex) {
    			// TRUE and FALSE don't set any value
    			// moreover default behavior will throw exception due to argument position computation:
    			// findByNameIsNull() has no arg => arguments is empty making arguments[0] throws an IndexOutOfBoundsException
    		}
    	}
    }