org.dynamoframework.dao.impl.JpaQueryBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dynamo-impl Show documentation
Show all versions of dynamo-impl Show documentation
Dynamo Framework implementation project.
package org.dynamoframework.dao.impl;
/*-
* #%L
* Dynamo Framework
* %%
* Copyright (C) 2014 - 2024 Open Circle Solutions
* %%
* 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.
* #L%
*/
import jakarta.persistence.EntityManager;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import jakarta.persistence.metamodel.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dynamoframework.configuration.DynamoProperties;
import org.dynamoframework.constants.DynamoConstants;
import org.dynamoframework.dao.FetchJoinInformation;
import org.dynamoframework.dao.SortOrder;
import org.dynamoframework.dao.SortOrders;
import org.dynamoframework.filter.*;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.Map.Entry;
/**
* @author patrick.deenen
* @author bas.rutten Class for constructing JPA queries built on the criteria
* API
*/
@Slf4j
@Component
public final class JpaQueryBuilder {
static private DynamoProperties dynamoProperties;
@Autowired
public void init(DynamoProperties dynamoProperties) {
JpaQueryBuilder.dynamoProperties = dynamoProperties;
}
/**
* Adds fetch join information to a query root
*
* @param root the query root
* @param fetchJoins the fetch joins
* @return true
if the fetches include a collection,
* false
otherwise
*/
private static boolean addFetchJoins(FetchParent root, FetchJoinInformation... fetchJoins) {
boolean collection = false;
Map> fetchMap = new HashMap<>();
if (root != null && fetchJoins != null) {
for (FetchJoinInformation s : fetchJoins) {
// Support nested properties
FetchParent fetch = root;
String[] propertyPath = s.getProperty().split("\\.");
String prefix = "";
for (String property : propertyPath) {
if (prefix.length() > 0) {
prefix = prefix + ".";
}
prefix += property;
if (fetchMap.containsKey(prefix)) {
fetch = fetchMap.get(prefix);
} else {
fetch = fetch.fetch(property, translateJoinType(s.getJoinType()));
fetchMap.put(prefix, fetch);
}
}
}
// check if any collection is fetched. If so then the results need
// to be cleaned up using "distinct"
collection = isCollectionFetch(root);
}
return collection;
}
/**
* Adds the "order by" clause to a criteria query
*
* @param builder the criteria builder
* @param cq the criteria query
* @param root the query root
* @param distinct whether a "distinct" is applied to the query
* @param sortOrders the sort orders
* @return the query with the sorting clause appended to it
*/
private static CriteriaQuery addOrderBy(CriteriaBuilder builder, CriteriaQuery cq, Root root,
boolean distinct, SortOrder... sortOrders) {
return addOrderBy(builder, cq, root, null, distinct, sortOrders);
}
/**
* Adds the "order by" clause to a criteria query
*
* @param builder the criteria builder
* @param cq the criteria query
* @param root the query root
* @param multiSelect whether to select multiple properties
* @param distinct whether a 'distinct' is applied to the query. This
* influences how the sort part is built
* @param sortOrders the sort orders
* @return the criteria query with any relevant sorting instructions added to it
*/
private static CriteriaQuery addOrderBy(CriteriaBuilder builder, CriteriaQuery cq, Root root,
List> multiSelect, boolean distinct, SortOrder... sortOrders) {
List> ms = new ArrayList<>();
if (multiSelect != null && !multiSelect.isEmpty()) {
ms.addAll(multiSelect);
}
if (sortOrders != null && sortOrders.length > 0) {
List orders = new ArrayList<>();
for (SortOrder sortOrder : sortOrders) {
Expression> property = distinct ? getPropertyPath(root, sortOrder.getProperty(), true)
: getPropertyPathForSort(root, sortOrder.getProperty());
ms.add(property);
orders.add(sortOrder.isAscending() ? builder.asc(property) : builder.desc(property));
}
cq.orderBy(orders);
}
if (multiSelect != null && !ms.isEmpty()) {
cq.multiselect(ms);
}
return cq;
}
/**
* Creates a predicate based on an "And" filter
*
* @param builder the criteria builder
* @param root the root object
* @param filter the "And" filter
* @param parameters the parameters passed to the query
* @return the predicate
*/
private static Predicate createAndPredicate(CriteriaBuilder builder, Root> root, Filter filter,
Map parameters) {
And and = (And) filter;
List filters = new ArrayList<>(and.getFilters());
Predicate predicate = null;
if (!filters.isEmpty()) {
predicate = createPredicate(filters.remove(0), builder, root, parameters);
while (!filters.isEmpty()) {
Predicate next = createPredicate(filters.remove(0), builder, root, parameters);
if (next != null) {
predicate = builder.and(predicate, next);
}
}
}
return predicate;
}
/**
* Creates a predicate based on a case-insensitive Like-predicate
*
* @param builder the criteria builder
* @param root the root object
* @param like the predicate
* @return the constructed predicate
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static Predicate createCaseInsensitiveLikePredicate(CriteriaBuilder builder, Root> root, Like like) {
String unaccentName = dynamoProperties.getUnaccentFunctionName();
if (!StringUtils.isEmpty(unaccentName)) {
return builder.like(
builder.function(unaccentName, String.class,
builder.lower((Expression) getPropertyPath(root, like.getPropertyId(), true))),
removeAccents(like.getValue().toLowerCase()));
}
return builder.like(builder.lower((Expression) getPropertyPath(root, like.getPropertyId(), true)),
like.getValue().toLowerCase());
}
/**
* Creates a predicate based on a "Compare" filter
*
* @param builder the criteria builder
* @param root the query root
* @param filter the Compare filter
* @return the predicate
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static Predicate createComparePredicate(CriteriaBuilder builder, Root> root, Filter filter) {
Compare compare = (Compare) filter;
Path path = getPropertyPath(root, compare.getPropertyId(), true);
Object value = compare.getValue();
// number representation may contain locale specific separators.
// Here, we remove
// those and make sure a period is used in all cases
if (value instanceof String str) {
// strip out any "%" sign from decimal fields
value = str.replace('%', ' ').trim();
if (StringUtils.isNumeric(str.replace(".", "").replace(",", ""))) {
// first remove all periods (which may be used as
// thousands separators), then replace comma by period
str = str.replace(".", "").replace(',', '.');
value = str;
}
}
switch (compare.getOperation()) {
case EQUAL:
if (value instanceof Class>) {
// When instance of class the use type expression
return builder.equal(path.type(), builder.literal(value));
}
return builder.equal(path, value);
case GREATER:
return builder.greaterThan(path, (Comparable) value);
case GREATER_OR_EQUAL:
return builder.greaterThanOrEqualTo(path, (Comparable) value);
case LESS:
return builder.lessThan(path, (Comparable) value);
case LESS_OR_EQUAL:
return builder.lessThanOrEqualTo(path, (Comparable) value);
default:
return null;
}
}
/**
* Creates a query that performs a count
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param filter the filter to apply
* @param distinct whether to return only distinct results
* @return the constructed query
*/
public static TypedQuery createCountQuery(EntityManager entityManager, Class entityClass,
Filter filter, boolean distinct) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(Long.class);
Root root = cq.from(entityClass);
cq.select(distinct ? builder.countDistinct(root) : builder.count(root));
Map pars = createParameterMap();
Predicate predicate = createPredicate(filter, builder, root, pars);
if (predicate != null) {
cq.where(predicate);
}
TypedQuery query = entityManager.createQuery(cq);
setParameters(query, pars);
return query;
}
/**
* Creates a query for retrieving all distinct values for a certain field
*
* @param filter the search filter
* @param entityManager the entity manager
* @param entityClass the class of the entity to query
* @param distinctField the name of the field for which to retrieve the distinct
* values
* @param sortOrders the sort orders
* @return the constructed query
*/
public static TypedQuery createDistinctQuery(Filter filter, EntityManager entityManager,
Class entityClass, String distinctField, SortOrder... sortOrders) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createTupleQuery();
Root root = cq.from(entityClass);
// select only the distinctField
cq.multiselect(getPropertyPath(root, distinctField, true));
Map pars = createParameterMap();
Predicate predicate = createPredicate(filter, builder, root, pars);
if (predicate != null) {
cq.where(predicate);
}
cq.distinct(true);
cq = addOrderBy(builder, cq, root, true, sortOrders);
TypedQuery query = entityManager.createQuery(cq);
setParameters(query, pars);
return query;
}
/**
* Creates a query that fetches objects based on their IDs
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param ids the IDs of the desired entities
* @param sortOrders the sort orders
* @param fetchJoins the desired fetch joins
* @return the constructed query
*/
@SuppressWarnings("rawtypes")
public static TypedQuery createFetchQuery(EntityManager entityManager, Class entityClass,
List ids, Filter additionalFilter, SortOrders sortOrders, FetchJoinInformation... fetchJoins) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(entityClass);
Root root = cq.from(entityClass);
boolean distinct = addFetchJoins(root, fetchJoins);
if (distinct) {
log.warn("Using distinct select, sorting on complex properties is not supported!");
}
// use parameters to prevent Hibernate from creating different query plan
// every time
Expression exp = root.get(DynamoConstants.ID);
ParameterExpression idExpression = builder.parameter(List.class, DynamoConstants.IDS);
cq.distinct(distinct);
Map pars = createParameterMap();
if (additionalFilter != null) {
Predicate predicate = createPredicate(additionalFilter, builder, root, pars);
if (predicate != null) {
cq.where(predicate, exp.in(idExpression));
} else {
cq.where(exp.in(idExpression));
}
} else {
cq.where(exp.in(idExpression));
}
addOrderBy(builder, cq, root, distinct, sortOrders == null ? null : sortOrders.toArray());
TypedQuery query = entityManager.createQuery(cq);
query.setParameter(DynamoConstants.IDS, ids);
if (additionalFilter != null) {
setParameters(query, pars);
}
return query;
}
/**
* Create a query for fetching a single object
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param id ID of the object to return
* @param fetchJoins fetch joins to include
* @return the constructed query
*/
public static TypedQuery createFetchSingleObjectQuery(EntityManager entityManager, Class entityClass,
ID id, FetchJoinInformation[] fetchJoins) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(entityClass);
Root root = cq.from(entityClass);
addFetchJoins(root, fetchJoins);
Expression exp = root.get(DynamoConstants.ID);
boolean parameterSet = true;
if (id instanceof Integer) {
ParameterExpression p = builder.parameter(Integer.class, DynamoConstants.ID);
cq.where(builder.equal(exp, p));
} else if (id instanceof Long) {
ParameterExpression p = builder.parameter(Long.class, DynamoConstants.ID);
cq.where(builder.equal(exp, p));
} else if (id instanceof String) {
ParameterExpression p = builder.parameter(String.class, DynamoConstants.ID);
cq.where(builder.equal(exp, p));
} else {
// no parameter but query directly
parameterSet = false;
cq.where(builder.equal(root.get(DynamoConstants.ID), id));
}
TypedQuery query = entityManager.createQuery(cq);
if (parameterSet) {
query.setParameter(DynamoConstants.ID, id);
}
return query;
}
/**
* Creates a query for retrieving the IDs of the entities that match the
* provided filter
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param filter the filter to apply
* @param sortOrders the sorting to apply
* @return the constructed query
*/
public static TypedQuery createIdQuery(EntityManager entityManager, Class entityClass, Filter filter,
SortOrder... sortOrders) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createTupleQuery();
Root root = cq.from(entityClass);
List> selection = new ArrayList<>();
selection.add(root.get(DynamoConstants.ID));
Map pars = createParameterMap();
Predicate predicate = createPredicate(filter, builder, root, pars);
if (predicate != null) {
cq.where(predicate);
}
// When joins are added (by getPropertyPath) do distinct query
if (!root.getJoins().isEmpty()) {
cq.distinct(true);
}
// add order by clause - this is also important in case of an ID query
// since we do need to return the correct IDs!
// note: "distinct" must be false here
cq = addOrderBy(builder, cq, root, selection, false, sortOrders);
TypedQuery query = entityManager.createQuery(cq);
setParameters(query, pars);
return query;
}
/**
* Creates a predicate based on a "Like"-filter
*
* @param builder the criteria builder
* @param root the query root
* @param filter the filter
* @return the constructed predicate
*/
private static Predicate createLikePredicate(CriteriaBuilder builder, Root> root, Filter filter) {
Like like = (Like) filter;
if (like.isCaseSensitive()) {
return createLikePredicate(builder, root, like);
} else {
return createCaseInsensitiveLikePredicate(builder, root, like);
}
}
/**
* Creates a predicate based on a "Like"-filter (case-insensitive)
*
* @param builder the criteria builder
* @param root the query root
* @param like the Like filter
* @return the constructed predicate
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static Predicate createLikePredicate(CriteriaBuilder builder, Root> root, Like like) {
String unaccentName = dynamoProperties.getUnaccentFunctionName();
if (!StringUtils.isEmpty(unaccentName)) {
return builder.like(
builder.function(unaccentName, String.class, getPropertyPath(root, like.getPropertyId(), true)),
removeAccents(like.getValue()));
}
return builder.like((Expression) getPropertyPath(root, like.getPropertyId(), true), like.getValue());
}
/**
* Creates a modulo predicate
*
* @param builder the criteria builder
* @param root the query root
* @param filter the filter to apply
* @return the constructed predicate
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static Predicate createModuloPredicate(CriteriaBuilder builder, Root> root, Filter filter) {
Modulo modulo = (Modulo) filter;
if (modulo.getModExpression() != null) {
// compare to a literal expression
return builder.equal(builder.mod((Expression) getPropertyPath(root, modulo.getPropertyId(), true),
(Expression) getPropertyPath(root, modulo.getModExpression(), true)), modulo.getResult());
} else {
// compare to a property
return builder.equal(builder.mod((Expression) getPropertyPath(root, modulo.getPropertyId(), true),
modulo.getModValue().intValue()), modulo.getResult());
}
}
/**
* Creates a predicate for a logical OR
*
* @param builder the criteria builder
* @param root the query root
* @param filter the filter to apply
* @param parameters the query parameter mapping
* @return the constructed predicate
*/
private static Predicate createOrPredicate(CriteriaBuilder builder, Root> root, Filter filter,
Map parameters) {
Or or = (Or) filter;
List filters = new ArrayList<>(or.getFilters());
Predicate predicate = null;
if (!filters.isEmpty()) {
predicate = createPredicate(filters.remove(0), builder, root, parameters);
while (!filters.isEmpty()) {
Predicate next = createPredicate(filters.remove(0), builder, root, parameters);
if (next != null) {
predicate = builder.or(predicate, next);
}
}
}
return predicate;
}
private static Map createParameterMap() {
return new HashMap<>();
}
/**
* Creates a predicate based on a Filter
*
* @param filter the filter
* @param builder the criteria builder
* @param root the entity root
* @return the constructed predicate
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static Predicate createPredicate(Filter filter, CriteriaBuilder builder, Root> root,
Map parameters) {
if (filter == null) {
return null;
}
if (filter instanceof And) {
return createAndPredicate(builder, root, filter, parameters);
} else if (filter instanceof Or) {
return createOrPredicate(builder, root, filter, parameters);
} else if (filter instanceof Not not) {
return builder.not(createPredicate(not.getFilter(), builder, root, parameters));
} else if (filter instanceof Between between) {
Expression property = getPropertyPath(root, between.getPropertyId(), true);
return builder.between(property, (Comparable) between.getStartValue(), (Comparable) between.getEndValue());
} else if (filter instanceof Compare) {
return createComparePredicate(builder, root, filter);
} else if (filter instanceof IsNull isNull) {
Path path = getPropertyPath(root, isNull.getPropertyId(), true);
if (isCollection(path)) {
return builder.isEmpty(path);
}
return builder.isNull(path);
} else if (filter instanceof Like) {
return createLikePredicate(builder, root, filter);
} else if (filter instanceof Contains contains) {
return builder.isMember(contains.getValue(),
(Expression) getPropertyPath(root, contains.getPropertyId(), true));
} else if (filter instanceof In in) {
if (in.getValues() != null && !in.getValues().isEmpty()) {
Expression> exp = getPropertyPath(root, in.getPropertyId(), true);
String parName = in.getPropertyId().replace('.', '_');
// Support multiple parameters
if (parameters.containsKey(parName)) {
parName = parName + System.currentTimeMillis();
}
ParameterExpression p = builder.parameter(Collection.class, parName);
parameters.put(parName, in.getValues());
return exp.in(p);
} else {
// match with an empty list
Expression exp = getPropertyPath(root, in.getPropertyId(), true);
return exp.in(List.of(-1));
}
} else if (filter instanceof Modulo) {
return createModuloPredicate(builder, root, filter);
}
throw new UnsupportedOperationException("Filter: " + filter.getClass().getName() + " not recognized");
}
/**
* Creates a query that selects objects based on the specified filter
*
* @param filter the filter
* @param entityManager the entity manager
* @param entityClass the entity class
* @param sortOrders the sorting information
* @return the constructed query
*/
public static TypedQuery createSelectQuery(Filter filter, EntityManager entityManager, Class entityClass,
FetchJoinInformation[] fetchJoins, SortOrder... sortOrders) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(entityClass);
Root root = cq.from(entityClass);
boolean distinct = addFetchJoins(root, fetchJoins);
cq.select(root);
cq.distinct(distinct);
Map pars = createParameterMap();
Predicate p = createPredicate(filter, builder, root, pars);
if (p != null) {
cq.where(p);
}
cq = addOrderBy(builder, cq, root, distinct, sortOrders);
TypedQuery query = entityManager.createQuery(cq);
setParameters(query, pars);
return query;
}
/**
* Creates a query to fetch an object based on a value of a unique property
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param fetchJoins the fetch joins to include
* @param propertyName name of the property to search on
* @param value value of the property to search on
* @return the constructed query
*/
public static CriteriaQuery createUniquePropertyFetchQuery(EntityManager entityManager, Class entityClass,
FetchJoinInformation[] fetchJoins, String propertyName, Object value, boolean caseSensitive) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(entityClass);
Root root = cq.from(entityClass);
addFetchJoins(root, fetchJoins);
Predicate equals;
if (value instanceof String && !caseSensitive) {
equals = builder.equal(builder.upper(root.get(propertyName).as(String.class)),
((String) value).toUpperCase());
} else {
equals = builder.equal(root.get(propertyName), value);
}
cq.where(equals);
cq.distinct(true);
return cq;
}
/**
* Creates a query used to retrieve a single entity based on a unique property
* value
*
* @param entityManager the entity manager
* @param entityClass the entity class
* @param propertyName the property name
* @param value the unique value
* @return the constructed query
*/
public static CriteriaQuery createUniquePropertyQuery(EntityManager entityManager, Class entityClass,
String propertyName, Object value, boolean caseSensitive) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery cq = builder.createQuery(entityClass);
Root root = cq.from(entityClass);
Predicate equals;
if (value instanceof String && !caseSensitive) {
equals = builder.equal(builder.upper(root.get(propertyName).as(String.class)),
((String) value).toUpperCase());
} else {
equals = builder.equal(root.get(propertyName), value);
}
cq.where(equals);
return cq;
}
/**
* Gets property path.
*
* @param root the root where path starts form
* @param propertyId the property ID
* @param join set to true if you want implicit joins to be created for
* ALL collections
* @return the path to property
*/
@SuppressWarnings("unchecked")
private static Path
© 2015 - 2025 Weber Informatics LLC | Privacy Policy