/*
* Copyright 2015 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.
*
* 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.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import org.kie.internal.query.QueryParameterIdentifiers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is a utility class for dynamically creating JPA queries.
* </p>
* See the jbpm-human-task-core and jbpm-audit *query() method logic.
* </p>
* This class is <em>not</em> thread-safe and should only be used locally in a method.
*/
public class QueryAndParameterAppender {
private static Logger logger = LoggerFactory.getLogger(QueryAndParameterAppender.class);
private boolean noWhereClauseYet = true;
private boolean noClauseAddedYet = true;
private int nestedParentheses = 0;
private boolean alreadyUsed = false;
private final StringBuilder queryBuilder;
private final Map<String, Object> queryParams;
private int queryParamId = 0;
public QueryAndParameterAppender(StringBuilder queryBuilder, Map<String, Object> params) {
this.queryBuilder = queryBuilder;
this.queryParams = params;
this.noWhereClauseYet = ! queryBuilder.toString().contains("WHERE");
}
public boolean hasBeenUsed() {
return ! this.noClauseAddedYet;
}
public void markAsUsed() {
this.noClauseAddedYet = false;
}
public void addNamedQueryParam(String name, Object value) {
queryParams.put(name, value);
}
public void openParentheses() {
++nestedParentheses;
queryBuilder.append(" ( ");
}
public void closeParentheses() {
queryBuilder.append(" ) ");
--nestedParentheses;
}
public int getParenthesesNesting() {
return nestedParentheses;
}
// "Normal" query parameters --------------------------------------------------------------------------------------------------
public <T> void addQueryParameters( List<? extends Object> paramList, String listId, Class<T> type, String fieldName,
String joinClause, boolean union ) {
List<T> listIdParams;
if( paramList != null && paramList.size() > 0 ) {
Object inputObject = paramList.get(0);
listIdParams = checkAndConvertListToType(paramList, inputObject, listId, type);
} else {
return;
}
String paramName = generateParamName();
StringBuilder queryClause = new StringBuilder("( " + fieldName + " IN (:" + paramName + ")");
if( joinClause != null ) {
queryClause.append(" AND " + joinClause);
}
queryClause.append(" )");
addToQueryBuilder(queryClause.toString(), union, paramName, listIdParams );
}
public <T> void addQueryParameters( Map<String, List<? extends Object>> inputParamsMap, String listId, Class<T> type,
String fieldName, boolean union, String joinClause ) {
List<? extends Object> inputParams = inputParamsMap.get(listId);
addQueryParameters(inputParams, listId, type, fieldName, joinClause, union );
}
public <T> void addQueryParameters( List<? extends Object> inputParams, String listId, Class<T> type, String fieldName,
boolean union ) {
addQueryParameters(inputParams, listId, type, fieldName, null, union );
}
public <T> void addQueryParameters( Map<String, List<? extends Object>> inputParamsMap, String listId, Class<T> type,
String fieldName, boolean union ) {
List<? extends Object> inputParams = inputParamsMap.get(listId);
addQueryParameters(inputParams, listId, type, fieldName, null, union );
}
// Range query parameters -----------------------------------------------------------------------------------------------------
public <T> void addRangeQueryParameters(List<? extends Object> paramList, String listId, Class<T> type, String fieldName, String joinClause, boolean union ) {
List<T> listIdParams;
if( paramList != null && paramList.size() > 0 ) {
Object inputObject = paramList.get(0);
if( inputObject == null ) {
inputObject = paramList.get(1);
if( inputObject == null ) {
return;
}
}
listIdParams = checkAndConvertListToType(paramList, inputObject, listId, type);
} else {
return;
}
T min = listIdParams.get(0);
T max = listIdParams.get(1);
Map<String, T> paramNameMinMaxMap = new HashMap<String, T>(2);
StringBuilder queryClause = new StringBuilder("( " );
if( joinClause != null ) {
queryClause.append("( ");
}
queryClause.append(fieldName);
if( min == null ) {
if( max == null ) {
return;
} else {
// only max
String maxParamName = generateParamName();
queryClause.append(" <= :" + maxParamName + " " );
paramNameMinMaxMap.put(maxParamName, max);
}
} else if( max == null ) {
// only min
String minParamName = generateParamName();
queryClause.append(" >= :" + minParamName + " ");
paramNameMinMaxMap.put(minParamName, min);
} else {
// both min and max
String minParamName = generateParamName();
String maxParamName = generateParamName();
if( union ) {
queryClause.append(" >= :" + minParamName + " OR " + fieldName + " <= :" + maxParamName + " " );
} else {
queryClause.append(" BETWEEN :" + minParamName + " AND :" + maxParamName + " " );
}
paramNameMinMaxMap.put(minParamName, min);
paramNameMinMaxMap.put(maxParamName, max);
}
if( joinClause != null ) {
queryClause.append(") and " + joinClause.trim() + " ");
}
queryClause.append(")");
// add query string to query builder and fill params map
internalAddToQueryBuilder(queryClause.toString(), union);
for( Entry<String, T> nameMinMaxEntry : paramNameMinMaxMap.entrySet() ) {
addNamedQueryParam(nameMinMaxEntry.getKey(), nameMinMaxEntry.getValue());
}
queryBuilderModificationCleanup();
}
public <T> void addRangeQueryParameters( Map<String, List<? extends Object>> inputParamsMap, String listId, Class<T> type,
String fieldName, boolean union, String joinClause ) {
List<? extends Object> inputParams = inputParamsMap.get(listId);
addRangeQueryParameters(inputParams, listId, type, fieldName, joinClause, union );
}
public <T> void addRangeQueryParameters( List<? extends Object> inputParams, String listId, Class<T> type, String fieldName,
boolean union ) {
addRangeQueryParameters(inputParams, listId, type, fieldName, null, union);
}
public <T> void addRangeQueryParameters( Map<String, List<? extends Object>> inputParamsMap, String listId, Class<T> type,
String fieldName, boolean union ) {
List<? extends Object> inputParams = inputParamsMap.get(listId);
addRangeQueryParameters(inputParams, listId, type, fieldName, null, union);
}
// Regex query parameters -----------------------------------------------------------------------------------------------------
public void addRegexQueryParameters( List<String> inputParams, String listId, String fieldName, boolean union ) {
addRegexQueryParameters(inputParams, listId, fieldName, null, union);
}
public void addRegexQueryParameters( List<String> paramValList, String listId, String fieldName, String joinClause,
boolean union) {
// setup
if( paramValList == null || paramValList.isEmpty() ) {
return;
}
List<String> regexList = new ArrayList<String>(paramValList.size());
for( String input : paramValList ) {
if( input == null || input.isEmpty() ) {
continue;
}
String regex = input.replace('*', '%').replace('.', '_');
regexList.add(regex);
}
// build query string
Map<String, String> paramNameRegexMap = new HashMap<String, String>();
StringBuilder queryClause = new StringBuilder("( ");
if( joinClause != null ) {
queryClause.append("( ");
}
for( int i = 0; i < regexList.size(); ++i ) {
String paramName = generateParamName();
queryClause.append(fieldName + " LIKE :" + paramName + " " );
paramNameRegexMap.put(paramName, regexList.get(i));
if( i + 1 < regexList.size() ) {
queryClause.append(union ? "OR" : "AND").append(" ");
}
}
if( joinClause != null ) {
queryClause.append(") AND " + joinClause.trim() + " ");
}
queryClause.append(")");
// add query string to query builder and fill params map
internalAddToQueryBuilder(queryClause.toString(), union);
for( Entry<String, String> nameRegexEntry : paramNameRegexMap.entrySet() ) {
addNamedQueryParam(nameRegexEntry.getKey(), nameRegexEntry.getValue());
}
queryBuilderModificationCleanup();
}
public void addToQueryBuilder( String query, boolean union ) {
// modify query builder
internalAddToQueryBuilder(query, union);
// cleanup
queryBuilderModificationCleanup();
}
public <T> void addToQueryBuilder( String query, boolean union, String paramName, List<T> paramValList ) {
// modify query builder
internalAddToQueryBuilder(query, union);
// add query parameters
Set<T> paramVals = new HashSet<T>(paramValList);
addNamedQueryParam(paramName, paramVals);
// cleanup
queryBuilderModificationCleanup();
}
private void internalAddToQueryBuilder( String query, boolean union ) {
if( this.noClauseAddedYet ) {
if( noWhereClauseYet ) {
queryBuilder.append(" WHERE ");
} else {
queryBuilder.append(" AND ");
}
this.noClauseAddedYet = false;
} else if( this.alreadyUsed ) {
queryBuilder.append(union ? "\nOR " : "\nAND ");
}
queryBuilder.append(query);
}
public void queryBuilderModificationCleanup() {
this.alreadyUsed = true;
}
public boolean whereClausePresent() {
return ! noWhereClauseYet;
}
@SuppressWarnings("unchecked")
private <T> List<T> checkAndConvertListToType( List<?> inputList, Object inputObject, String listId, Class<T> type ) {
if( logger.isDebugEnabled() ) {
debugQueryParametersIdentifiers();
}
assert type != null : listId + ": type is null!";
assert inputObject != null : listId + ": input object is null!";
if( type.equals(inputObject.getClass()) ) {
return (List<T>) inputList;
} else {
throw new IllegalArgumentException(listId + " parameter is an instance of " + "List<"
+ inputObject.getClass().getSimpleName() + "> instead of " + "List<" + type.getSimpleName() + ">");
}
}
public String generateParamName() {
int id = queryParamId++ % 26;
char first = (char) ('A' + id);
return new String(first + String.valueOf(((id + 1) / 26) + 1));
}
public StringBuilder getQueryBuilder() {
return queryBuilder;
}
public static void debugQueryParametersIdentifiers() {
try {
Field [] fields = QueryParameterIdentifiers.class.getDeclaredFields();
Map<String, String> fieldValueMap = new TreeMap<String, String>(new Comparator<String>() {
@Override
public int compare( String o1, String o2 ) {
int int1 = -1;
try {
int1 = Integer.parseInt(o1);
} catch(Exception e) {
// no op
}
int int2 = -1;
try {
int2 = Integer.parseInt(o2);
} catch(Exception e) {
// no op
}
if( int1 > -1 && int2 > -1 ) {
return new Integer(int1).compareTo(int2);
}
if( int1 > -1 && int2 == -1 ) {
return -1;
}
if( int1 == -1 && int2 > -1 ) {
return 1;
}
return o1.compareTo(o2);
}
});
for( Field field : fields ) {
fieldValueMap.put(field.get(null).toString(), field.getName());
}
for( Entry<String, String> entry : fieldValueMap.entrySet() ) {
logger.debug(String.format("%-12s : %s", entry.getKey(), entry.getValue()));
}
} catch( Exception e ) {
// ignore
}
}
}