All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.tennaito.rsql.jpa.PredicateBuilder Maven / Gradle / Ivy

/*
 * The MIT License
 *
 * Copyright 2013 Jakub Jirutka .
 * Copyright 2015 Antonio Rabelo.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.tennaito.rsql.jpa;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.Metamodel;

import com.github.tennaito.rsql.builder.BuilderTools;
import com.github.tennaito.rsql.parser.ast.ComparisonOperatorProxy;

import cz.jirutka.rsql.parser.ast.ComparisonNode;
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.LogicalNode;
import cz.jirutka.rsql.parser.ast.Node;

/**
 * PredicateBuilder
 *
 * Classe with utility methods for Predicate creation from RSQL AST nodes.
 *
 * @author AntonioRabelo
 *
 * Based from CriterionBuilders of rsql-hibernate created by Jakub Jirutka .
 *
 * @since 2015-02-05
 */
public final class PredicateBuilder {

	private static final Logger LOG = Logger.getLogger(PredicateBuilder.class.getName());

    public static final Character LIKE_WILDCARD = '*';

    /**
     * Private constructor.
     */
    private PredicateBuilder(){
    	super();
    }

    /**
     * Create a Predicate from the RSQL AST node.
     *
     * @param node      RSQL AST node.
     * @param entity    The main entity of the query.
     * @param manager   JPA EntityManager.
     * @param misc      Facade with all necessary tools for predicate creation.
     * @return 			Predicate a predicate representation of the Node.
     */
    public static  Predicate createPredicate(Node node, Class entity, EntityManager manager, BuilderTools misc) {

        LOG.log(Level.INFO, "Creating Predicate for: {0}", node);

        if (node instanceof LogicalNode) {
            return createPredicate((LogicalNode)node, entity, manager, misc);
        }
        
        if (node instanceof ComparisonNode) {
            return createPredicate((ComparisonNode)node, entity, manager, misc);
        }

        throw new IllegalArgumentException("Unknown expression type: " + node.getClass());
    }

    /**
     * Create a Predicate from the RSQL AST logical node.
     *
     * @param logical        RSQL AST logical node.
     * @param entity  		 The main entity of the query.
     * @param manager 		 JPA EntityManager.
     * @param misc      	 Facade with all necessary tools for predicate creation.
     * @return 				 Predicate a predicate representation of the Node.
     */
    public static  Predicate createPredicate(LogicalNode logical, Class entity, EntityManager entityManager, BuilderTools misc) {
        LOG.log(Level.INFO, "Creating Predicate for logical node: {0}", logical);

    	CriteriaBuilder builder = entityManager.getCriteriaBuilder();

    	List predicates = new ArrayList();

    	LOG.log(Level.INFO, "Creating Predicates from all children nodes.");
    	for (Node node : logical.getChildren()) {
    		predicates.add(createPredicate(node, entity, entityManager, misc));
		}

        switch (logical.getOperator()) {
            case AND : return builder.and(predicates.toArray(new Predicate[predicates.size()]));
            case OR : return builder.or(predicates.toArray(new Predicate[predicates.size()]));
        }

        throw new IllegalArgumentException("Unknown operator: " + logical.getOperator());
    }

    /**
     * Create a Predicate from the RSQL AST comparison node.
     *
     * @param comparison	 RSQL AST comparison node.
     * @param entity  		 The main entity of the query.
     * @param manager 		 JPA EntityManager.
     * @param misc      	 Facade with all necessary tools for predicate creation.
     * @return 				 Predicate a predicate representation of the Node.
     */
    public static  Predicate createPredicate(ComparisonNode comparison, Class entity, EntityManager entityManager, BuilderTools misc) {
    	LOG.log(Level.INFO, "Creating Predicate for comparison node: {0}", comparison);

    	Metamodel metaModel = entityManager.getMetamodel();
    	ManagedType classMetadata = metaModel.managedType(entity);

    	CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    	CriteriaQuery criteria = builder.createQuery(entity);
    	From root = criteria.from(entity);

    	Class argumentType = null;
    	Expression propertyPath = null;
    	String[] graph = comparison.getSelector().split("\\.");
        LOG.log(Level.INFO, "Property graph path : {0}", comparison.getSelector());
    	for (String property : graph) {
    		String mappedProperty = misc.getPropertiesMapper().translate(property, classMetadata.getJavaType());
    		if (hasPropertyName(mappedProperty, classMetadata)) {
    			argumentType = findPropertyType(mappedProperty, classMetadata);
    			if (isAssociationType(mappedProperty, classMetadata)) {
    				String previousClass = classMetadata.getJavaType().getName();
    				classMetadata = metaModel.managedType(argumentType);
    				LOG.log(Level.INFO, "Create a join between {0} and {1}.", new Object[] {previousClass, classMetadata.getJavaType().getName()});
    				root = root.join(mappedProperty);
    			} else {
    				LOG.log(Level.INFO, "Create property path for type {0} property {1}.", new Object[] {classMetadata.getJavaType().getName(), mappedProperty});
    				propertyPath = root.get(mappedProperty).as(argumentType);
    				break;
    			}
    		} else {
    			throw new IllegalArgumentException("Unknown property: " + mappedProperty + " from entity " + classMetadata.getJavaType().getName());
    		}
		}

		LOG.log(Level.INFO, "Cast all arguments to type {0}.", argumentType.getName());
    	List castedArguments = misc.getArgumentParser().parse(comparison.getArguments(), argumentType);

    	try {
    		// try to create a predicate
    		return PredicateBuilder.createPredicate(propertyPath, comparison.getOperator(), castedArguments, entityManager);
    	} catch (IllegalArgumentException e) {
    		// if operator dont exist try to delegate
            if (misc.getPredicateBuilder() != null) {
            	return misc.getPredicateBuilder().createPredicate(comparison, entity, entityManager, misc);
            }
            // if no strategy was defined then there are no more operators.
            throw e;
    	}
    }

    ///////////////  TEMPLATE METHODS  ///////////////

    /**
     * Create Predicate for comparison operators.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param operator      Comparison operator.
     * @param arguments     Arguments (1 for binary comparisons, n for multi-value comparisons [in, not in (out)])
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createPredicate(Expression propertyPath, ComparisonOperator operator, List arguments, EntityManager manager) {
    	LOG.log(Level.INFO, "Creating predicate: propertyPath {0} {1}", new Object[]{operator, arguments});

    	if (ComparisonOperatorProxy.asEnum(operator) != null) {
    		switch (ComparisonOperatorProxy.asEnum(operator)) {
	    		case EQUAL : {
	    			Object argument = arguments.get(0);
	    			if (argument instanceof String) {
	    				return createLike(propertyPath, (String) argument, manager);
	    			} else if (isNullArgument(argument)) {
	    				return createIsNull(propertyPath, manager);
	    			} else {
	    				return createEqual(propertyPath, argument, manager);
	    			}
	    		}
	    		case NOT_EQUAL : {
	    			Object argument = arguments.get(0);
	    			if (argument instanceof String) {
	    				return createNotLike(propertyPath, (String) argument, manager);
	    			} else if (isNullArgument(argument)) {
	    				return createIsNotNull(propertyPath, manager);
	    			} else {
	    				return createNotEqual(propertyPath, argument, manager);
	    			}
	    		}
	    		case GREATER_THAN : {
	    			Object argument = arguments.get(0);
	    			return createGreaterThan(propertyPath, (Number)argument, manager);
	    		}
	    		case GREATER_THAN_OR_EQUAL : {
	    			Object argument = arguments.get(0);
	    			return createGreaterEqual(propertyPath, (Number)argument, manager);
	    		}
	    		case LESS_THAN : {
	    			Object argument = arguments.get(0);
	    			return createLessThan(propertyPath, (Number)argument, manager);
	    		}
	    		case LESS_THAN_OR_EQUAL : {
	    			Object argument = arguments.get(0);
	    			return createLessEqual(propertyPath, (Number)argument, manager);
	    		}
	    		case IN : return createIn(propertyPath, arguments, manager);
	    		case NOT_IN : return createNotIn(propertyPath, arguments, manager);
    		}
    	}
        throw new IllegalArgumentException("Unknown operator: " + operator);
    }

    /**
     * Apply a case-insensitive "like" constraint to the property path. Value
     * should contains wildcards "*" (% in SQL) and "_".
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument with/without wildcards
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createLike(Expression propertyPath, String argument, EntityManager manager) {
        String like = argument.replace(LIKE_WILDCARD, '%');
        CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.like(builder.lower(propertyPath), like.toLowerCase());
    }

    /**
     * Apply an "is null" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createIsNull(Expression propertyPath, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
    	return builder.isNull(propertyPath);
    }

    /**
     * Apply an "equal" constraint to property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createEqual(Expression propertyPath, Object argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
    	return builder.equal(propertyPath, argument);
    }

    /**
     * Apply a "not equal" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createNotEqual(Expression propertyPath, Object argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.notEqual(propertyPath, argument);
    }

    /**
     * Apply a negative case-insensitive "like" constraint to the property path.
     * Value should contains wildcards "*" (% in SQL) and "_".
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument with/without wildcards
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createNotLike(Expression propertyPath, String argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.not(createLike(propertyPath, argument, manager));
    }

    /**
     * Apply an "is not null" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createIsNotNull(Expression propertyPath, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.isNotNull(propertyPath);
    }

    /**
     * Apply a "greater than" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument number.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createGreaterThan(Expression propertyPath, Number argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.gt(propertyPath, argument);
    }

    /**
     * Apply a "greater than or equal" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument number.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createGreaterEqual(Expression propertyPath, Number argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.ge(propertyPath, argument);
    }

    /**
     * Apply a "less than" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument number.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createLessThan(Expression propertyPath, Number argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.lt(propertyPath, argument);
    }

    /**
     * Apply a "less than or equal" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      Argument number.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createLessEqual(Expression propertyPath, Number argument, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
        return builder.le(propertyPath, argument);
    }

    /**
     * Apply a "in" constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      List of arguments.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createIn(Expression propertyPath, List arguments, EntityManager manager) {
    	return propertyPath.in(arguments);
    }

    /**
     * Apply a "not in" (out) constraint to the property path.
     *
     * @param propertyPath  Property path that we want to compare.
     * @param argument      List of arguments.
     * @param manager       JPA EntityManager.
     * @return              Predicate a predicate representation.
     */
    private static Predicate createNotIn(Expression propertyPath, List arguments, EntityManager manager) {
    	CriteriaBuilder builder = manager.getCriteriaBuilder();
    	return builder.not(createIn(propertyPath,arguments, manager));
    }

    /**
     * Verify if a property is an Association type.
     *
     * @param property       Property to verify.
     * @param classMetadata  Metamodel of the class we want to check.
     * @return               true if the property is an associantion, false otherwise.
     */
    private static  boolean isAssociationType(String property, ManagedType classMetadata){
    	return classMetadata.getAttribute(property).isAssociation();
    }

    /**
     * Verifies if a class metamodel has the specified property.
     *
     * @param property       Property name.
     * @param classMetadata  Class metamodel that may hold that property.
     * @return               true if the class has that property, false otherwise.
     */
    private static  boolean  hasPropertyName(String property, ManagedType classMetadata) {
        Set> names = classMetadata.getAttributes();
        for (Attribute name : names) {
            if (name.getName().equals(property)) return true;
        }
        return false;
    }

    /**
     * Get the property Type out of the metamodel.
     *
     * @param property       Property name for type extraction.
     * @param classMetadata  Reference class metamodel that holds property type.
     * @return               Class java type for the property.
     */
    private static  Class findPropertyType(String property, ManagedType classMetadata) {
        return classMetadata.getAttribute(property).getJavaType();
    }

    /**
     * Verifies if the argument is null.
     *
     * @param argument
     * @return true if argument is null, false otherwise
     */
    private static boolean isNullArgument(Object argument) {
        return argument == null;
    }
}