org.jbpm.query.jpa.impl.QueryCriteriaUtil Maven / Gradle / Ivy
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jbpm.query.jpa.impl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.AbstractQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import org.jbpm.query.jpa.data.QueryCriteria;
import org.jbpm.query.jpa.data.QueryWhere;
import org.jbpm.query.jpa.data.QueryWhere.QueryCriteriaType;
import org.kie.api.runtime.manager.audit.VariableInstanceLog;
import org.kie.internal.query.QueryParameterIdentifiers;
public abstract class QueryCriteriaUtil {
private Map> criteriaAttributes;
private final AtomicBoolean criteriaAttributesInitialized = new AtomicBoolean(false);
public QueryCriteriaUtil(Map> criteriaAttributes) {
initialize(criteriaAttributes);
}
protected QueryCriteriaUtil() {
// for the AbstractTaskQueryCriteriaUtil
}
protected void initialize(Map> criteriaAttributes) {
this.criteriaAttributes = criteriaAttributes;
}
protected Map> getCriteriaAttributes() {
if( ! criteriaAttributesInitialized.get() ) {
if( initializeCriteriaAttributes() ) {
criteriaAttributesInitialized.set(true);
} else {
throw new IllegalStateException("Queries can not be performed if no persistence unit has been initalized!");
}
}
return criteriaAttributes;
}
// List cast conversion methods -----------------------------------------------------------------------------------------------
@SuppressWarnings("unchecked")
public static List convertListToInterfaceList( ListinternalResult, Class interfaceType ) {
List result = new ArrayList(internalResult.size());
for( C element : internalResult ) {
result.add((I) element);
}
return result;
}
// constructor helper methods -------------------------------------------------------------------------------------------------
public static void addCriteria( Map> criteriaAttributes, String listId, Attribute attr ) {
Class table = attr.getJavaMember().getDeclaringClass();
addCriteria(criteriaAttributes, listId, table, attr);
}
public static void addCriteria( Map> criteriaAttributes, String listId, Class table, Attribute attr ) {
Map tableAttrs = criteriaAttributes.get(table);
if( tableAttrs == null ) {
tableAttrs = new ConcurrentHashMap(1);
criteriaAttributes.put(table, tableAttrs);
}
Attribute previousMapping = tableAttrs.put(listId, attr);
assert previousMapping == null : "Previous mapping existed for [" + listId + "]!";
}
// abstract methods -----------------------------------------------------------------------------------------------------------
/**
* The implementation of this method should be synchronized!
*/
protected abstract boolean initializeCriteriaAttributes();
protected abstract CriteriaBuilder getCriteriaBuilder();
// @formatter:on
// query logic ----------------------------------------------------------------------------------------------------------------
/**
* This method takes the high-level steps needed in order to create a JPA {@link CriteriaQuery}.
*
* - A {@link CriteriaBuilder} and {@link CriteriaQuery} instance are created.
* - The tables being selected from are defined in the query.
* - The {@link CriteriaQuery} instance is filled using the criteria in the {@link QueryWhere} instance
* - A JPA {@link Query} instance is created
* - The meta criteria (max results, offset) are applied to the query
* - The results are retrieved and returned
*
* @param queryWhere a {@link QueryWhere} instance containing the query criteria
* @param queryType The type ({@link Class}) of the result
* @return The result of the query, a {@link List}.
*/
public List doCriteriaQuery( QueryWhere queryWhere, Class queryType ) {
// 1. create builder and query instances
CriteriaBuilder builder = getCriteriaBuilder();
CriteriaQuery criteriaQuery = builder.createQuery(queryType);
// query base;
criteriaQuery.select(criteriaQuery.from(queryType));
fillCriteriaQuery(criteriaQuery, queryWhere, builder, queryType);
List result = createQueryAndCallApplyMetaCriteriaAndGetResult(queryWhere, criteriaQuery, builder);
return result;
}
// query logic ----------------------------------------------------------------------------------------------------------------
/**
* This is the main ("highest"? "most abstract"?) method that is used to create a {@link CriteriaQuery} from a {@link QueryWhere} instance.
*
* @param query The (empty) {@link CriteriaQuery} that will be filled using the {@link QueryCriteria} and other information in the {@link QueryWhere} instance
* @param queryWhere The {@link QueryWhere} instance, with abstract information that should be added to the {@link CriteriaQuery}
* @param builder The {@link CriteriaBuilder}, helpful when creating {@link Predicate}s to add to the {@link CriteriaQuery}
* @param queryType The {@link Class} indicating the main {@link Root} of the {@link CriteriaQuery}
*/
protected void fillCriteriaQuery( CriteriaQuery query, QueryWhere queryWhere, CriteriaBuilder builder, Class queryType ) {
Predicate queryPredicate = createPredicateFromCriteriaList(query, builder, queryType, queryWhere.getCriteria(), queryWhere );
if( queryPredicate != null ) {
query.where(queryPredicate);
}
if( queryWhere.getAscOrDesc() != null ) {
String orderByListId = queryWhere.getOrderByListId();
assert orderByListId != null : "Ascending boolean is set but no order by list Id has been specified!";
Expression orderByPath = getOrderByExpression(query, queryType, orderByListId);
Order order;
if( queryWhere.getAscOrDesc() ) {
order = builder.asc(orderByPath);
} else {
order = builder.desc(orderByPath);
}
query.orderBy(order);
}
}
/**
* This method is contains the setup steps for creating and assembling {@link Predicate} instances
* from the information in a {@link List} of {@link QueryCriteria} instances.
*
* The steps taken when assembling a {@link Predicate} are the following:
*
* - Separate the given {@link List} of {@link QueryCriteria} into an intersection and disjunction (union) list.
* - Combine separate "range" {@link QueryCriteria} that apply to the same listId
* - Call the {@link #createPredicateFromCriteriaList(CriteriaQuery, List, CriteriaBuilder, Class, boolean)}
* method on disjunction criteria list and on the intersection criteria list
* - Take the result of the previous step and appropriately combine the returned {@link Predicate} instances into a
* final {@link Predicate} instance that is then returned.
*
* @param query The {@link CriteriaQuery} instance that we're assembling {@link Predicate} instances for
* @param inputCriteriaList The list of {@link QueryCriteria} instances that will be processed
* @param builder A {@link CriteriaBuilder} instance to help us build {@link Predicate} instances
* @param resultType The {@link Class} (type) of the result, given so that later methods can use it
* @return A {@link Predicate} instance based on the given {@link QueryCriteria} list
*/
private Predicate createPredicateFromCriteriaList(
CriteriaQuery query, CriteriaBuilder builder,
Class resultType,
List inputCriteriaList, QueryWhere queryWhere ) {
Predicate queryPredicate = null;
if( inputCriteriaList.size() > 1 ) {
List predicateList = new LinkedList();
QueryCriteria previousCriteria = null;
QueryCriteria firstCriteria = null;
List currentIntersectingCriteriaList = new LinkedList();
int i = 0;
for( QueryCriteria criteria : inputCriteriaList ) {
assert i++ != 0 || criteria.isFirst() : "First criteria is not flagged as first!";
if( criteria.isFirst() ) {
firstCriteria = previousCriteria = criteria;
continue;
} else if( firstCriteria != null ) {
if( criteria.isUnion() ) {
Predicate predicate = createPredicateFromSingleOrGroupCriteria(query, builder, resultType, previousCriteria, queryWhere);
predicateList.add(predicate);
} else {
currentIntersectingCriteriaList.add(firstCriteria);
}
firstCriteria = null;
}
if( criteria.isUnion() ) {
// AND has precedence over OR:
// If 'criteria' is now OR and there was a list (currentIntersectingCriteriaList) of AND criteria before 'criteria'
// - create a predicate from the AND criteria
if( previousCriteria != null && ! previousCriteria.isUnion() && ! currentIntersectingCriteriaList.isEmpty() ) {
Predicate predicate
= createPredicateFromIntersectingCriteriaList(query, builder, resultType, currentIntersectingCriteriaList, queryWhere );
assert predicate != null : "Null predicate when evaluating intersecting criteria [" + criteria.toString() + "]";
predicateList.add(predicate);
// - new (empty) current intersecting criteria list
currentIntersectingCriteriaList = new LinkedList();
}
// Process the current union criteria
Predicate predicate = createPredicateFromSingleOrGroupCriteria(query, builder, resultType, criteria, queryWhere);
assert predicate != null : "Null predicate when evaluating union criteria [" + criteria.toString() + "]";
predicateList.add(predicate);
} else {
currentIntersectingCriteriaList.add(criteria);
}
previousCriteria = criteria;
}
if( ! currentIntersectingCriteriaList.isEmpty() ) {
Predicate predicate
= createPredicateFromIntersectingCriteriaList(query, builder, resultType, currentIntersectingCriteriaList, queryWhere );
predicateList.add(predicate);
}
assert ! predicateList.isEmpty() : "The predicate list should not (can not?) be empty here!";
if( predicateList.size() == 1 ) {
queryPredicate = predicateList.get(0);
} else {
Predicate [] predicates = predicateList.toArray(new Predicate[predicateList.size()]);
queryPredicate = builder.or(predicates);
}
} else if( inputCriteriaList.size() == 1 ) {
QueryCriteria singleCriteria = inputCriteriaList.get(0);
queryPredicate = createPredicateFromSingleOrGroupCriteria(query, builder, resultType, singleCriteria, queryWhere);
}
return queryPredicate;
}
/**
* This method is necessary because the AND operator in SQL has precedence over the OR operator.
*
* That means that intersecting criteria should always be grouped together (and processed first, basically), which is essentially
* what this method does.
*
* @param query The {@link CriteriaQuery} that is being built
* @param intersectingCriteriaList The list of intersecting (ANDed) {@link QueryCriteria}
* @param builder The {@link CriteriaBuilder} builder instance
* @param queryType The (persistent {@link Entity}) {@link Class} that we are querying on
* @return A {@link Predicate} created on the basis of the given {@link List} of {@link QueryCriteria}
*/
private Predicate createPredicateFromIntersectingCriteriaList(CriteriaQuery query, CriteriaBuilder builder, Class queryType, List intersectingCriteriaList, QueryWhere queryWhere ) {
combineIntersectingRangeCriteria(intersectingCriteriaList);
assert intersectingCriteriaList.size() > 0 : "Empty list of currently intersecting criteria!";
Predicate [] intersectingPredicates = new Predicate[intersectingCriteriaList.size()];
int i = 0;
for( QueryCriteria intersectingCriteria : intersectingCriteriaList ) {
Predicate predicate = createPredicateFromSingleOrGroupCriteria(query, builder, queryType, intersectingCriteria, queryWhere );
assert predicate != null : "Null predicate when evaluating individual intersecting criteria [" + intersectingCriteria.toString() + "]";
intersectingPredicates[i++] = predicate;
}
Predicate predicate;
if( intersectingPredicates.length > 1 ) {
predicate = builder.and(intersectingPredicates);
} else {
predicate = intersectingPredicates[0];
}
return predicate;
}
/**
* When there are multiple range criteria in a query (in the same group), it is more efficient to
* submit a JPA "between" criteria than 2 different criteria.
*
* @param intersectionCriteria A {@link List} of {@link QueryCriteria} instances that are range criteria
*/
@SuppressWarnings("unchecked")
private void combineIntersectingRangeCriteria(List intersectionCriteria) {
Map intersectingRangeCriteria = new HashMap();
Iterator iter = intersectionCriteria.iterator();
while( iter.hasNext() ) {
QueryCriteria criteria = iter.next();
if( QueryCriteriaType.RANGE.equals(criteria.getType()) ) {
QueryCriteria previousCriteria = intersectingRangeCriteria.put(criteria.getListId(), criteria);
if( previousCriteria != null ) {
Object [] prevCritValues, thisCritValues;
assert previousCriteria.hasValues() || previousCriteria.hasDateValues() :
"Previous criteria has neither values nor date values!";
assert !(previousCriteria.hasValues() && previousCriteria.hasDateValues()) :
"Previous criteria has BOTH values and date values!";
assert (previousCriteria.hasValues() && criteria.hasValues())
|| (previousCriteria.hasDateValues() && criteria.hasDateValues()) :
"Previous and current criteria should have either both have values or both have date values!";
boolean dateValues = false;
if( previousCriteria.hasValues() ) {
prevCritValues = previousCriteria.getValues().toArray();
thisCritValues = criteria.getValues().toArray();
} else {
dateValues = true;
prevCritValues = previousCriteria.getDateValues().toArray();
thisCritValues = criteria.getDateValues().toArray();
}
List values = dateValues ? previousCriteria.getDateValues() : previousCriteria.getValues();
if( prevCritValues[0] == null && thisCritValues[1] == null ) {
values.set(0, thisCritValues[0]);
intersectingRangeCriteria.put(previousCriteria.getListId(), previousCriteria);
iter.remove();
} else if( prevCritValues[1] == null && thisCritValues[0] == null ) {
values.set(1, thisCritValues[1]);
intersectingRangeCriteria.put(previousCriteria.getListId(), previousCriteria);
iter.remove();
}
}
}
}
}
/**
* Depending on whether or not the given {@link QueryCriteria} is a group criteria (which then contains a {@link List}<{@link QueryCriteria}>)
* or a single {@link QueryCriteria}, the correct method to process the given {@link QueryCriteria} is called.
*
* @param query The {@link CriteriaQuery} that is being built
* @param criteria The {@link QueryCriteria} instance
* @param builder The {@link CriteriaBuilder} builder instance
* @param queryType The (persistent {@link Entity}) {@link Class} that we are querying on
* @return A {@link Predicate} created on the basis of the given {@link QueryCriteria} instance
*/
private Predicate createPredicateFromSingleOrGroupCriteria(CriteriaQuery query, CriteriaBuilder builder, Class queryType, QueryCriteria criteria, QueryWhere queryWhere ) {
Predicate predicate;
if( criteria.isGroupCriteria() ) {
assert ! criteria.hasValues() : "Criteria has both subcriteria (group criteria) and values! [" + criteria.toString() + "]";
predicate = createPredicateFromCriteriaList(query, builder, queryType, criteria.getCriteria(), queryWhere );
} else {
assert ! criteria.hasCriteria() || Integer.parseInt(criteria.getListId()) < 0 : "Criteria has both values and subcriteria (group criteria)! [" + criteria.toString() + "]";
predicate = createPredicateFromSingleCriteria(query, builder, queryType, criteria, queryWhere);
}
return predicate;
}
/**
* This method is the main method for creating a {@link Predicate} from a (non-group) {@link QueryCriteria} instance.
*
* If it can not figure out how to create a {@link Predicate} from the given {@link QueryCriteria} instance,
* then the (abstract) {@link #implSpecificCreatePredicateFromSingleCriteria(CriteriaQuery, QueryCriteria, CriteriaBuilder, Root, Class)}
* method is called.
*
* @param query The {@link CriteriaQuery} that is being built
* @param criteria The given {@link QueryCriteria} instance
* @param builder The {@link CriteriaBuilder} builder instance
* @param queryType The (persistent {@link Entity}) {@link Class} that we are querying on
* @return A {@link Predicate} created on the basis of the given {@link QueryCriteria}
*/
private Predicate createPredicateFromSingleCriteria(
CriteriaQuery query, CriteriaBuilder builder,
Class queryType,
QueryCriteria criteria, QueryWhere queryWhere) {
Predicate predicate = null;
assert criteria.hasValues() || criteria.hasDateValues() || Integer.parseInt(criteria.getListId()) < 0
: "No values present for criteria with list id: [" + criteria.getListId() + "]";
String listId = criteria.getListId();
Attribute attr = getCriteriaAttributes().get(queryType).get(listId);
if( attr != null ) {
Expression entityField = getEntityField(query, listId, attr);
predicate = basicCreatePredicateFromSingleCriteria(builder, entityField, criteria);
} else {
predicate = implSpecificCreatePredicateFromSingleCriteria(query, builder, queryType, criteria, queryWhere );
}
return predicate;
}
/**
* This is a helper method to retrieve a particular {@link Root} from a {@link CriteriaQuery} instance
*
* @param query The {@link CriteriaQuery} instance that we're building
* @param queryType The {@link Class} matching the {@link Root} we want
* @return The {@link Root} matching the given {@link Class} or null if it's not in the query
*/
public static Root getRoot(AbstractQuery query, Class queryType) {
Root> table = null;
for( Root> root : query.getRoots() ) {
if( root.getJavaType().equals(queryType) ) {
table = root;
break;
}
}
return table;
}
/**
* This method retrieves the entity "field" that can be used as the LHS of a {@link Predicate}
*
* This method is overridden in some extended {@link QueryCriteriaUtil} implementations
*
* @param query The {@link CriteriaQuery} that we're building
* @param listId The list id of the given {@link QueryCriteria}
* @return An {@link Expression} with the {@link Path} to the field represented by the {@link QueryCriteria#getListId()} value
*/
protected Expression getEntityField(CriteriaQuery query, String listId, Attribute attr) {
return defaultGetEntityField(query, listId, attr);
}
@SuppressWarnings("unchecked")
public static Expression defaultGetEntityField(CriteriaQuery query, String listId, Attribute attr) {
Expression entityField = null;
if( attr != null ) {
Class attrType = attr.getDeclaringType().getJavaType();
for( From from : query.getRoots() ) {
if( from.getJavaType().equals(attrType) ) {
if( attr != null ) {
if( attr instanceof SingularAttribute ) {
entityField = from.get((SingularAttribute) attr);
} else if( attr instanceof PluralAttribute ) {
entityField = from.get((PluralAttribute) attr);
} else {
throw new IllegalStateException("Unexpected attribute type when processing criteria with list id " + listId + ": " + attr.getClass().getName() );
}
break;
}
}
}
}
// if entityField == null, this is because this QueryCriteria is a implementation specific criteria, such as "LAST_VARIABLE_LIST"
return entityField;
}
/**
* This method creates the basic types of {@link Predicate} from trivial {@link QueryCriteria} (NORMAL/REGEXP/RANGE).
*
* @param builder The {@link CriteriaBuilder}, helpful when creating {@link Predicate}s to add to the {@link CriteriaQuery}
* @param entityField The {@link Expression} representing a field/column in an entity/table.
* @param criteria The {@link QueryCriteria} with the values to use as the RHS of a {@link Predicate}
* @return The created {@link Predicate}
*/
@SuppressWarnings("unchecked")
public static Predicate basicCreatePredicateFromSingleCriteria(CriteriaBuilder builder, Expression entityField, QueryCriteria criteria) {
Predicate predicate = null;
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy