/*
* The MIT License
*
* Copyright 2013 Jakub Jirutka <jakub@jirutka.cz>.
* 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 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;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.PluralAttribute;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* PredicateBuilder
*
* Classe with utility methods for Predicate creation from RSQL AST nodes.
*
* @author AntonioRabelo
*
* Based from CriterionBuilders of rsql-hibernate created by Jakub Jirutka <jakub@jirutka.cz>.
*
* @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 static final Date START_DATE;
private static final Date END_DATE;
static {
//
// Use a date range that Oracle can cope with - apparently the years around 1 BC and 1 AD are messed up in Oracle - known bug
//
Calendar cal = Calendar.getInstance();
cal.set( 9999, Calendar.DECEMBER, 31);
END_DATE = cal.getTime();
cal.set( 5, Calendar.JANUARY, 1); // Use Jan 1, 5 AD, since that's where the Roman's sort of got it together with leap years.
START_DATE = cal.getTime();
}
/**
* Private constructor.
*/
private PredicateBuilder(){
super();
}
/**
* Create a Predicate from the RSQL AST node.
*
* @param node RSQL AST node.
* @param root From that predicate expression paths depends on.
* @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 <T> Predicate createPredicate(Node node, From root, Class<T> entity, EntityManager manager, BuilderTools misc) {
LOG.log(Level.INFO, "Creating Predicate for: {0}", node);
if (node instanceof LogicalNode) {
return createPredicate((LogicalNode)node, root, entity, manager, misc);
}
if (node instanceof ComparisonNode) {
return createPredicate((ComparisonNode)node, root, 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 root From that predicate expression paths depends on.
* @param entity The main entity of the query.
* @param entityManager JPA EntityManager.
* @param misc Facade with all necessary tools for predicate creation.
* @return Predicate a predicate representation of the Node.
*/
public static <T> Predicate createPredicate(LogicalNode logical, From root, Class<T> entity, EntityManager entityManager, BuilderTools misc) {
LOG.log(Level.INFO, "Creating Predicate for logical node: {0}", logical);
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
List<Predicate> predicates = new ArrayList<Predicate>();
LOG.log(Level.INFO, "Creating Predicates from all children nodes.");
for (Node node : logical.getChildren()) {
predicates.add(createPredicate(node, root, 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 startRoot From that predicate expression paths depends on.
* @param entity The main entity of the query.
* @param entityManager JPA EntityManager.
* @param misc Facade with all necessary tools for predicate creation.
* @return Predicate a predicate representation of the Node.
*/
public static <T> Predicate createPredicate(ComparisonNode comparison, From startRoot, Class<T> entity, EntityManager entityManager, BuilderTools misc) {
if (startRoot == null) {
String msg = "From root node was undefined.";
LOG.log(Level.SEVERE, msg);
throw new IllegalArgumentException(msg);
}
LOG.log(Level.INFO, "Creating Predicate for comparison node: {0}", comparison);
LOG.log(Level.INFO, "Property graph path : {0}", comparison.getSelector());
Expression propertyPath = findPropertyPath(comparison.getSelector(), startRoot, entityManager, misc);
LOG.log(Level.INFO, "Cast all arguments to type {0}.", propertyPath.getJavaType().getName());
List<Object> castedArguments = misc.getArgumentParser().parse(comparison.getArguments(), propertyPath.getJavaType());
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, startRoot, entity, entityManager, misc);
}
// if no strategy was defined then there are no more operators.
throw e;
}
}
/**
* Find a property path in the graph from startRoot
*
* @param propertyPath The property path to find.
* @param startRoot From that property path depends on.
* @param entityManager JPA EntityManager.
* @param misc Facade with all necessary tools for predicate creation.
* @return The Path for the property path
* @throws IllegalArgumentException if attribute of the given property name does not exist
*/
public static <T> Path<?> findPropertyPath(String propertyPath, Path startRoot, EntityManager entityManager, BuilderTools misc) {
String[] graph = propertyPath.split("\\.");
Metamodel metaModel = entityManager.getMetamodel();
ManagedType<?> classMetadata = metaModel.managedType(startRoot.getJavaType());
Path<?> root = startRoot;
for (String property : graph) {
String mappedProperty = misc.getPropertiesMapper().translate(property, classMetadata.getJavaType());
if( !mappedProperty.equals( property) ) {
root = findPropertyPath( mappedProperty, root, entityManager, misc );
} else {
if (!hasPropertyName(mappedProperty, classMetadata)) {
throw new IllegalArgumentException("Unknown property: " + mappedProperty + " from entity " + classMetadata.getJavaType().getName());
}
if (isAssociationType(mappedProperty, classMetadata)) {
Class<?> associationType = findPropertyType(mappedProperty, classMetadata);
String previousClass = classMetadata.getJavaType().getName();
classMetadata = metaModel.managedType(associationType);
LOG.log(Level.INFO, "Create a join between {0} and {1}.", new Object[]{previousClass, classMetadata.getJavaType().getName()});
if (root instanceof Join) {
root = root.get(mappedProperty);
} else {
root = ((From) root).join(mappedProperty);
}
} else {
LOG.log(Level.INFO, "Create property path for type {0} property {1}.", new Object[]{classMetadata.getJavaType().getName(), mappedProperty});
root = root.get(mappedProperty);
if (isEmbeddedType(mappedProperty, classMetadata)) {
Class<?> embeddedType = findPropertyType(mappedProperty, classMetadata);
classMetadata = metaModel.managedType(embeddedType);
}
}
}
}
return root;
}
/////////////// 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<Object> 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);
Predicate predicate;
if (argument instanceof Date) {
int days = 1;
predicate = createBetweenThan(propertyPath, modifyDate(argument, days), END_DATE, manager);
} else if (argument instanceof Number || argument == null) {
predicate = createGreaterThan(propertyPath, (Number) argument, manager);
} else if (argument instanceof Comparable) {
predicate = createGreaterThanComparable(propertyPath, (Comparable) argument, manager);
} else {
throw new IllegalArgumentException(buildNotComparableMessage(operator, argument));
}
return predicate;
}
case GREATER_THAN_OR_EQUAL : {
Object argument = arguments.get(0);
Predicate predicate;
if (argument instanceof Date){
predicate = createBetweenThan(propertyPath, (Date)argument, END_DATE, manager);
} else if (argument instanceof Number || argument == null) {
predicate = createGreaterEqual(propertyPath, (Number)argument, manager);
} else if (argument instanceof Comparable) {
predicate = createGreaterEqualComparable(propertyPath, (Comparable) argument, manager);
} else {
throw new IllegalArgumentException(buildNotComparableMessage(operator, argument));
}
return predicate;
}
case LESS_THAN : {
Object argument = arguments.get(0);
Predicate predicate;
if (argument instanceof Date) {
int days = -1;
predicate = createBetweenThan(propertyPath, START_DATE, modifyDate(argument, days), manager);
} else if (argument instanceof Number || argument == null) {
predicate = createLessThan(propertyPath, (Number) argument, manager);
} else if (argument instanceof Comparable) {
predicate = createLessThanComparable(propertyPath, (Comparable) argument, manager);
} else {
throw new IllegalArgumentException(buildNotComparableMessage(operator, argument));
}
return predicate;
}
case LESS_THAN_OR_EQUAL : {
Object argument = arguments.get(0);
Predicate predicate;
if (argument instanceof Date){
predicate = createBetweenThan(propertyPath,START_DATE, (Date)argument, manager);
} else if (argument instanceof Number || argument == null) {
predicate = createLessEqual(propertyPath, (Number)argument, manager);
} else if (argument instanceof Comparable) {
predicate = createLessEqualComparable(propertyPath, (Comparable) argument, manager);
} else {
throw new IllegalArgumentException(buildNotComparableMessage(operator, argument));
}
return predicate;
}
case IN : return createIn(propertyPath, arguments, manager);
case NOT_IN : return createNotIn(propertyPath, arguments, manager);
}
}
throw new IllegalArgumentException("Unknown operator: " + operator);
}
/**
+ * Creates the between than.
+ *
+ * @param propertyPath the property path
+ * @param startDate the start date
+ * @param argument the argument
+ * @param manager the manager
+ * @return the predicate
+ */
private static Predicate createBetweenThan(Expression propertyPath, Date start, Date end, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.between(propertyPath, start, end);
}
/**
* 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<String> 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<String> 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<? extends Number> propertyPath, Number argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.gt(propertyPath, argument);
}
/**
* Apply a "greater than" 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 <Y extends Comparable<? super Y>> Predicate createGreaterThanComparable(Expression<? extends Y> propertyPath, Y argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.greaterThan(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<? extends Number> propertyPath, Number argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.ge(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.
* @param manager JPA EntityManager.
* @return Predicate a predicate representation.
*/
private static <Y extends Comparable<? super Y>> Predicate createGreaterEqualComparable(Expression<? extends Y> propertyPath, Y argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.greaterThanOrEqualTo(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<? extends Number> propertyPath, Number argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.lt(propertyPath, argument);
}
/**
* Apply a "less than" 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 <Y extends Comparable<? super Y>> Predicate createLessThanComparable(Expression<? extends Y> propertyPath, Y argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.lessThan(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<? extends Number> propertyPath, Number argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.le(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.
* @param manager JPA EntityManager.
* @return Predicate a predicate representation.
*/
private static <Y extends Comparable<? super Y>> Predicate createLessEqualComparable(Expression<? extends Y> propertyPath, Y argument, EntityManager manager) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
return builder.lessThanOrEqualTo(propertyPath, argument);
}
/**
* Apply a "in" constraint to the property path.
*
* @param propertyPath Property path that we want to compare.
* @param arguments 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 arguments 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 <tt>true</tt> if the property is an associantion, <tt>false</tt> otherwise.
*/
private static <T> boolean isAssociationType(String property, ManagedType<T> classMetadata){
return classMetadata.getAttribute(property).isAssociation();
}
/**
* Verify if a property is an Embedded type.
*
* @param property Property to verify.
* @param classMetadata Metamodel of the class we want to check.
* @return <tt>true</tt> if the property is an embedded attribute, <tt>false</tt> otherwise.
*/
private static <T> boolean isEmbeddedType(String property, ManagedType<T> classMetadata){
return classMetadata.getAttribute(property).getPersistentAttributeType() == PersistentAttributeType.EMBEDDED;
}
/**
* Verifies if a class metamodel has the specified property.
*
* @param property Property name.
* @param classMetadata Class metamodel that may hold that property.
* @return <tt>true</tt> if the class has that property, <tt>false</tt> otherwise.
*/
private static <T> boolean hasPropertyName(String property, ManagedType<T> classMetadata) {
Set<Attribute<? super T, ?>> names = classMetadata.getAttributes();
for (Attribute<? super T, ?> 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,
* if the property is a pluralAttribute it will take the bindable java type of that collection.
*/
private static <T> Class<?> findPropertyType(String property, ManagedType<T> classMetadata) {
Class<?> propertyType = null;
if (classMetadata.getAttribute(property).isCollection()) {
propertyType = ((PluralAttribute)classMetadata.getAttribute(property)).getBindableJavaType();
} else {
propertyType = classMetadata.getAttribute(property).getJavaType();
}
return propertyType;
}
/**
* Verifies if the argument is null.
*
* @param argument
* @return <tt>true</tt> if argument is null, <tt>false</tt> otherwise
*/
private static boolean isNullArgument(Object argument) {
return argument == null;
}
/**
* Get date regarding the operation (less then or greater than)
*
* @param argument Date to be modified
* @param days Days to be added or removed form argument;
*@return Date modified date
*/
private static Date modifyDate(Object argument, int days) {
Date date = (Date) argument;
Calendar c = Calendar.getInstance();
c.setTime(date);
c.add(Calendar.DATE, days);
date = c.getTime();
return date;
}
/**
* Builds an error message that reports that the argument is not suitable for use with the comparison operator.
* @param operator operator from the RSQL query
* @param argument actual argument produced from the ArgumentParser
* @return Error message for use in an Exception
*/
private static String buildNotComparableMessage(ComparisonOperator operator, Object argument) {
return String.format("Invalid type for comparison operator: %s type: %s must implement Comparable<%s>",
operator,
argument.getClass().getName(),
argument.getClass().getSimpleName());
}
}