
org.nuiton.topia.persistence.HqlAndParametersBuilder Maven / Gradle / Ivy
package org.nuiton.topia.persistence;
/*
* #%L
* ToPIA Extension :: API
* %%
* Copyright (C) 2018 - 2022 Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.BoundType;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.text.WordUtils;
import org.nuiton.topia.persistence.pager.FilterRuleGroupOperator;
import org.nuiton.topia.persistence.pager.PaginationOrder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A builder to create syntactically correct HQL and associated parameters given properties or after various constraint
* adds.
*
* It may be used in a Dao to ease dynamic construction of queries.
*
* @since 3.0
*/
public class HqlAndParametersBuilder {
private static final String DEFAULT_ALIAS = "topiaEntity_";
protected Joiner hqlClausesJoiner = Joiner.on(' ').skipNulls();
protected Class entityClass;
protected String selectClause;
protected Set whereClauses = new LinkedHashSet<>();
private final String alias;
protected LinkedHashSet orderByArguments;
protected Map parameters = new LinkedHashMap<>();
// List of properties that must be loaded in a single query
protected Set fetchProperties = new LinkedHashSet<>();
private boolean caseSensitive;
private final FilterRuleGroupOperator filterRuleGroupOperator;
public HqlAndParametersBuilder(Class entityClass) {
this(entityClass, DEFAULT_ALIAS, FilterRuleGroupOperator.AND);
}
public HqlAndParametersBuilder(Class entityClass, String alias) {
this(entityClass, alias, FilterRuleGroupOperator.AND);
}
public HqlAndParametersBuilder(Class entityClass, FilterRuleGroupOperator filterRuleGroupOperator) {
this(entityClass, DEFAULT_ALIAS, filterRuleGroupOperator);
}
public HqlAndParametersBuilder(Class entityClass, String alias, FilterRuleGroupOperator filterRuleGroupOperator) {
Preconditions.checkArgument(StringUtils.isNotEmpty(alias));
this.entityClass = entityClass;
this.alias = alias;
this.filterRuleGroupOperator = filterRuleGroupOperator;
}
public String getAlias() {
return alias;
}
public FilterRuleGroupOperator getFilterRuleGroupOperator() {
return filterRuleGroupOperator;
}
public String getHqlSelectClause(boolean includeFetch) {
String result = selectClause;
if (includeFetch && !CollectionUtils.isEmpty(fetchProperties)) {
Preconditions.checkState(Strings.isNullOrEmpty(result), "You cannot fetch if you specify a select clause");
result = String.format(" SELECT DISTINCT %s ", alias);
}
return result;
}
public void setSelectClause(String selectClause) {
this.selectClause = selectClause;
}
public void addNull(String property) {
Preconditions.checkArgument(StringUtils.isNotBlank(property));
whereClauses.add(alias + "." + property + " is null");
}
public void addNotNull(String property) {
Preconditions.checkArgument(StringUtils.isNotBlank(property));
whereClauses.add(alias + "." + property + " is not null");
}
public void addEquals(String property, Object value) {
Preconditions.checkArgument(StringUtils.isNotEmpty(property));
if (value == null) {
addNull(property);
} else {
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " = :" + hqlParameterName);
}
}
public void addEquals(String property, String value) {
Preconditions.checkArgument(io.ultreia.java4all.lang.Strings.isNotEmpty(property));
if (value == null) {
addNull(property);
return;
}
String variable = getVariable(property);
String hqlParameterName = putHqlParameterWithAvailableName2(property, value);
whereClauses.add(String.format("%s = :%s", variable, hqlParameterName));
}
public void addNotEquals(String property, Object value) {
Preconditions.checkArgument(StringUtils.isNotEmpty(property));
if (value == null) {
addNotNull(property);
} else {
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " != :" + hqlParameterName);
}
}
public void addIn(String property, Collection> values) {
addInOrNotIn(property, values, true);
}
public void addNotIn(String property, Collection> values) {
addInOrNotIn(property, values, false);
}
public > void addIn(String property, org.apache.commons.lang3.Range range) {
addIn(property, range, true);
}
public > void addNotIn(String property, org.apache.commons.lang3.Range range) {
addIn(property, range, false);
}
public > void addIn(String property, org.apache.commons.lang3.Range range, boolean in) {
T minimum = range.getMinimum();
T maximum = range.getMaximum();
if (in) {
doAddGreaterOrEquals(property, minimum);
doAddLowerOrEquals(property, maximum);
} else {
doAddLowerThan(property, minimum);
doAddGreaterThan(property, maximum);
}
}
public > void addIn(String property, com.google.common.collect.Range range) {
addIn(property, range, true);
}
public > void addNotIn(String property, com.google.common.collect.Range range) {
addIn(property, range, false);
}
public > void addIn(String property, com.google.common.collect.Range range, boolean in) {
if (range.hasLowerBound()) {
if (range.lowerBoundType() == BoundType.CLOSED) {
if (in) {
doAddGreaterOrEquals(property, range.lowerEndpoint());
} else {
doAddLowerThan(property, range.lowerEndpoint());
}
} else {
if (in) {
doAddGreaterThan(property, range.lowerEndpoint());
} else {
doAddLowerOrEquals(property, range.lowerEndpoint());
}
}
}
if (range.hasUpperBound()) {
if (range.upperBoundType() == BoundType.CLOSED) {
if (in) {
doAddLowerOrEquals(property, range.upperEndpoint());
} else {
doAddGreaterThan(property, range.upperEndpoint());
}
} else {
if (in) {
doAddLowerThan(property, range.upperEndpoint());
} else {
doAddGreaterOrEquals(property, range.upperEndpoint());
}
}
}
}
/**
* @param property FIXME
* @param values FIXME
* @param in true if property value must be in given collection, false if value must not be in given collection
*/
protected void addInOrNotIn(String property, Collection> values, boolean in) {
Preconditions.checkArgument(StringUtils.isNotEmpty(property));
Preconditions.checkNotNull(values);
String aliasedProperty = alias + "." + property;
int valuesSize = values.size();
if (valuesSize == 0) {
// XXX brendan 27/02/14 workaround to prevent generating "in ()" which in not supported by PostegreSQL (syntax error)
if (in) {
whereClauses.add(" 0 = 1 ");
} else {
whereClauses.add(" 1 = 1 ");
}
} else if (valuesSize == 1) {
// if there is only one possible value, replace "in" clause by a "=" clause
Object onlyElement = Iterables.getOnlyElement(values);
if (in) {
addEquals(property, onlyElement);
} else {
addNotEquals(property, onlyElement);
}
} else {
boolean propertyMayBeNull = values.contains(null);
Collection> hqlParameterValue = values;
if (propertyMayBeNull /* && ! in */) {
// duplicate given collection because we don't want 'null'
// in hqlParameterValue and we don't want side effect on parameters
hqlParameterValue = new LinkedHashSet<>(values);
hqlParameterValue.remove(null);
}
String hqlParameterName = putHqlParameterWithAvailableName(property, hqlParameterValue);
String whereClause;
if (in) {
whereClause = String.format(" %s in ( :%s ) ", aliasedProperty, hqlParameterName);
if (propertyMayBeNull) {
whereClause += " or " + aliasedProperty + " is null";
}
} else {
whereClause = String.format(" %s not in ( :%s ) ", aliasedProperty, hqlParameterName);
if (propertyMayBeNull) {
whereClause += " and " + aliasedProperty + " is not null";
}
}
whereClauses.add(whereClause);
}
}
/**
* @param property FIXME
* @param topiaId FIXME
* @see org.nuiton.topia.persistence.TopiaQueryBuilderAddCriteriaStep#addTopiaIdEquals(String, String)
*/
public void addTopiaIdEquals(String property, String topiaId) {
Preconditions.checkNotNull(topiaId);
addEquals(property + "." + TopiaEntity.PROPERTY_TOPIA_ID, topiaId);
}
/**
* @param property FIXME
* @param topiaIds FIXME
* @see org.nuiton.topia.persistence.TopiaQueryBuilderAddCriteriaStep#addTopiaIdIn(String, java.util.Collection)
*/
public void addTopiaIdIn(String property, Collection topiaIds) {
addIn(property + "." + TopiaEntity.PROPERTY_TOPIA_ID, topiaIds);
}
/**
* @param property FIXME
* @param topiaId FIXME
* @see org.nuiton.topia.persistence.TopiaQueryBuilderAddCriteriaStep#addTopiaIdNotEquals(String, String)
*/
public void addTopiaIdNotEquals(String property, String topiaId) {
Preconditions.checkNotNull(topiaId);
addNotEquals(property + "." + TopiaEntity.PROPERTY_TOPIA_ID, topiaId);
}
/**
* @param property FIXME
* @param topiaIds FIXME
* @see org.nuiton.topia.persistence.TopiaQueryBuilderAddCriteriaStep#addTopiaIdNotIn(String, java.util.Collection)
*/
public void addTopiaIdNotIn(String property, Collection topiaIds) {
addNotIn(property + "." + TopiaEntity.PROPERTY_TOPIA_ID, topiaIds);
}
public void addContains(String property, Object value) {
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(":" + hqlParameterName + " in elements(" + alias + "." + property + ")");
}
public void addNotContains(String property, Object value) {
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(":" + hqlParameterName + " not in elements(" + alias + "." + property + ")");
}
public void addLike(String property, String pattern) {
Objects.requireNonNull(pattern);
Preconditions.checkArgument(io.ultreia.java4all.lang.Strings.isNotEmpty(property));
String variable = getVariable(property);
String hqlParameterName = putHqlParameterWithAvailableName2(property, pattern);
whereClauses.add(String.format("%s like :%s", variable, hqlParameterName));
}
public void addNotLike(String property, String pattern) {
Objects.requireNonNull(pattern);
Preconditions.checkArgument(io.ultreia.java4all.lang.Strings.isNotEmpty(property));
String variable = getVariable(property);
String hqlParameterName = putHqlParameterWithAvailableName2(property, pattern);
whereClauses.add(String.format("%s not like :%s", variable, hqlParameterName));
}
public void addLowerThan(String property, Date date) {
doAddLowerThan(property, date);
}
public void addLowerOrEquals(String property, Date date) {
doAddLowerOrEquals(property, date);
}
public void addGreaterThan(String property, Date date) {
doAddGreaterThan(property, date);
}
public void addGreaterOrEquals(String property, Date date) {
doAddGreaterOrEquals(property, date);
}
public void addLowerThan(String property, Number number) {
doAddLowerThan(property, number);
}
public void addLowerOrEquals(String property, Number number) {
doAddLowerOrEquals(property, number);
}
public void addGreaterThan(String property, Number number) {
doAddGreaterThan(property, number);
}
public void addGreaterOrEquals(String property, Number number) {
doAddGreaterOrEquals(property, number);
}
public void addLowerThan(String property, String string) {
doAddLowerThan(property, string);
}
public void addLowerOrEquals(String property, String string) {
doAddLowerOrEquals(property, string);
}
public void addGreaterThan(String property, String string) {
doAddGreaterThan(property, string);
}
public void addGreaterOrEquals(String property, String string) {
doAddGreaterOrEquals(property, string);
}
public void addLowerThan(String property, LocalDate localDate) {
doAddLowerThan(property, localDate);
}
public void addLowerOrEquals(String property, LocalDate localDate) {
doAddLowerOrEquals(property, localDate);
}
public void addGreaterThan(String property, LocalDate localDate) {
doAddGreaterThan(property, localDate);
}
public void addGreaterOrEquals(String property, LocalDate localDate) {
doAddGreaterOrEquals(property, localDate);
}
public void addLowerThan(String property, LocalDateTime localDateTime) {
doAddLowerThan(property, localDateTime);
}
public void addLowerOrEquals(String property, LocalDateTime localDateTime) {
doAddLowerOrEquals(property, localDateTime);
}
public void addGreaterThan(String property, LocalDateTime localDateTime) {
doAddGreaterThan(property, localDateTime);
}
public void addGreaterOrEquals(String property, LocalDateTime localDateTime) {
doAddGreaterOrEquals(property, localDateTime);
}
protected void doAddLowerThan(String property, Object value) {
Preconditions.checkNotNull(value);
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " < :" + hqlParameterName);
}
protected void doAddLowerOrEquals(String property, Object value) {
Preconditions.checkNotNull(value);
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " <= :" + hqlParameterName);
}
protected void doAddGreaterThan(String property, Object value) {
Preconditions.checkNotNull(value);
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " > :" + hqlParameterName);
}
protected void doAddGreaterOrEquals(String property, Object value) {
Preconditions.checkNotNull(value);
String hqlParameterName = putHqlParameterWithAvailableName(property, value);
whereClauses.add(alias + "." + property + " >= :" + hqlParameterName);
}
public void addWhereClause(String whereClause) {
Preconditions.checkArgument(StringUtils.isNotBlank(whereClause));
whereClauses.add(whereClause);
}
public void addCollectionIsEmpty(String property) {
whereClauses.add(alias + "." + property + " is empty");
}
public void addCollectionIsNotEmpty(String property) {
whereClauses.add(alias + "." + property + " is not empty");
}
public void addWhereClause(String whereClause, Map hqlParameters) {
Preconditions.checkNotNull(hqlParameters);
Set collidedParameterNames = Sets.newHashSet(Sets.intersection(parameters.keySet(), hqlParameters.keySet()));
boolean noCollision = collidedParameterNames.isEmpty();
if (noCollision) {
parameters.putAll(hqlParameters);
} else {
// add all parameters with no collision
Set noCollisionParameterNames = Sets.difference(hqlParameters.keySet(), collidedParameterNames);
for (String parameterName : noCollisionParameterNames) {
Object parameterValue = hqlParameters.get(parameterName);
parameters.put(parameterName, parameterValue);
}
// resolve all collision parameters
for (String parameterName : collidedParameterNames) {
Object parameterValue = hqlParameters.get(parameterName);
// resolved parameter name
String newParameterName = putHqlParameterWithAvailableName(parameterName, parameterValue);
// replace the :parameterName (with no next alphanumeric caracter)
whereClause = whereClause.replaceAll(":" + parameterName + "(?!\\w)", ":" + newParameterName);
}
}
addWhereClause(whereClause);
}
public void setWhereClauses(Set whereClauses) {
Preconditions.checkNotNull(whereClauses);
this.whereClauses = whereClauses;
}
public Set getWhereClauses() {
return whereClauses;
}
public void setParameters(Map parameters) {
Preconditions.checkNotNull(parameters);
this.parameters = parameters;
}
public void setOrderByArguments(LinkedHashSet orderByArguments) {
Preconditions.checkNotNull(orderByArguments);
this.orderByArguments = new LinkedHashSet<>();
this.orderByArguments.addAll(orderByArguments);
}
public void setOrderByArguments(String... orderByArguments) {
Preconditions.checkNotNull(orderByArguments);
LinkedHashSet orderByArgumentsAsSet = new LinkedHashSet<>();
List list = Lists.newArrayList(orderByArguments);
orderByArgumentsAsSet.addAll(list);
if (orderByArgumentsAsSet.size() < list.size()) {
throw new IllegalStateException("Duplicate ORDER BY arguments found: " + list);
}
this.orderByArguments = orderByArgumentsAsSet;
}
public void setOrderByArguments(Collection paginationOrders) {
Preconditions.checkNotNull(paginationOrders);
LinkedHashSet orderByArgumentsAsSet = new LinkedHashSet<>();
for (PaginationOrder paginationOrder : paginationOrders) {
String orderClause = String.format("%s %s", paginationOrder.getClause(), paginationOrder.isDesc() ? "desc" : "asc");
boolean added = orderByArgumentsAsSet.add(orderClause);
if (!added) {
throw new IllegalStateException("Duplicate ORDER BY arguments found: " + orderClause);
}
}
this.orderByArguments = orderByArgumentsAsSet;
}
public void addAllFetches(Collection properties) {
Preconditions.checkNotNull(properties);
for (String property : properties) {
addFetch(property);
}
}
public void addAllFetches(String property, String... otherProperties) {
addFetch(property);
addAllFetches(Arrays.asList(otherProperties));
}
public void addFetch(String property) {
Preconditions.checkArgument(StringUtils.isNotBlank(property));
fetchProperties.add(property);
}
public boolean hasFetchProperties() {
return !fetchProperties.isEmpty();
}
public void setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
protected String putHqlParameterWithAvailableName2(String propertyName, Object value) {
if (!caseSensitive && value instanceof String) {
value = ((String) value).toLowerCase();
}
return putHqlParameterWithAvailableName(propertyName, value);
}
private String getVariable(String property) {
String variable = String.format("TRIM(BOTH ' ' FROM %s.%s)", getAlias(), property);
if (!caseSensitive) {
variable = String.format("LOWER(%s)", variable);
}
return variable;
}
protected String getHqlFromClause(boolean includeFetch) {
StringBuilder hqlFromClauseBuilder = new StringBuilder(String.format("FROM %s %s ", entityClass.getCanonicalName(), alias));
if (includeFetch) {
int fetchedPropertiesAliasIndex = 0;
Map aliases = Maps.newHashMap();
for (String propertyName : fetchProperties) {
// addAllFetches("a.b").addAllFetches("i.j.k") will produce:
// left join topiaEntity_.a fetchedProp0_ left join fetch fetchedProp0_.b
// left join topiaEntity_.i fetchedProp1_ left join fetch fetchedProp1_.j fetchedProp2_ left join fetchedProp2_.k
// addAllFetches("a.b").addAllFetches("a.c") will produce:
// left join topiaEntity_.a fetchedProp0_ left join fetch fetchedProp1_.b
// left join fetch fetchedProp1_.c
StringBuilder path = new StringBuilder(); // The path to reach the property
// Loop over each part of the "a.b.c"
for (String part : Splitter.on('.').split(propertyName)) {
// Look for the parent alias if already computed
String previousPath = path.toString();
String previousAlias = MoreObjects.firstNonNull(aliases.get(previousPath), alias);
// Compute the current path and look for its alias
if (path.length() > 0) {
path.append('.');
}
path.append(part);
String currentPath = path.toString();
String currentAlias = aliases.get(currentPath);
// No current alias found, create and add it
if (Strings.isNullOrEmpty(currentAlias)) {
currentAlias = String.format("fetchedProp%d_", fetchedPropertiesAliasIndex++);
aliases.put(currentPath, currentAlias);
String fetch = String.format(" LEFT JOIN FETCH %s.%s %s ", previousAlias, part, currentAlias);
hqlFromClauseBuilder.append(fetch);
}
}
}
}
return hqlFromClauseBuilder.toString();
}
public String getHqlWhereClause() {
String hqlWhereClause;
if (whereClauses.isEmpty()) {
hqlWhereClause = null;
} else if (whereClauses.size() == 1) {
String whereClause = Iterables.getOnlyElement(whereClauses);
hqlWhereClause = "where " + whereClause;
} else {
String hqlOperator = filterRuleGroupOperator == FilterRuleGroupOperator.AND ? "and" : "or";
hqlWhereClause = "where (" + StringUtils.join(whereClauses, ") " + hqlOperator + " (") + ")";
}
return hqlWhereClause;
}
public String getHqlOrderByClause() {
String hqlOrderByClause = null;
if (CollectionUtils.isNotEmpty(orderByArguments)) {
hqlOrderByClause = "order by " + alias + "." + StringUtils.join(orderByArguments, ", " + alias + ".");
}
return hqlOrderByClause;
}
public String getHql() {
return hqlClausesJoiner.join(
getHqlSelectClause(true),
getHqlFromClause(true),
getHqlWhereClause(),
getHqlOrderByClause());
}
public String getHqlForFetchStep1() {
String selectClause =
"select " + alias + "." + TopiaEntity.PROPERTY_TOPIA_ID + " " +
Strings.nullToEmpty(getHqlSelectClause(false));
return hqlClausesJoiner.join(
selectClause,
getHqlFromClause(false),
getHqlWhereClause(),
getHqlOrderByClause());
}
public String getHqlForFetchStep2() {
String whereClause = "where " + alias + "." + TopiaEntity.PROPERTY_TOPIA_ID + " in ( :topiaIdsForFetch_ ) ";
return hqlClausesJoiner.join(
getHqlSelectClause(true),
getHqlFromClause(true),
whereClause);
}
/**
* Converts a (nested) property name to an HQL argument name.
*
* For example getParameterName("yearlyDeclaration.survey.topiaId") → "yearlyDeclarationSurveyTopiaId"
*
* @param propertyName the name of a property, can be a path to a nested property
* @return a string that can syntactically be used as an HQL parameter name, not prefixed by ':'
*/
protected String getParameterName(String propertyName) {
Preconditions.checkArgument(StringUtils.isNotBlank(propertyName));
String capitalize = WordUtils.capitalize(propertyName, '.');
String withoutDots = capitalize.replaceAll("\\.", "");
return StringUtils.uncapitalize(withoutDots);
}
/**
* Add a parameter in the parameters map searching with the suitable parameter name in order to prevent conflicts.
*
* @param propertyName FIXME
* @param value FIXME
* @return the found key where the parameter has been added, suitable to use in the where clause
*/
public String putHqlParameterWithAvailableName(String propertyName, Object value) {
String parameterNamePrefix = getParameterName(propertyName);
int suffix = 0;
String parameterName = parameterNamePrefix + suffix;
while (parameters.containsKey(parameterName)) {
suffix++;
parameterName = parameterNamePrefix + suffix;
}
parameters.put(parameterName, value);
return parameterName;
}
public Map getHqlParameters() {
return parameters;
}
public boolean isOrderByClausePresent() {
return CollectionUtils.isNotEmpty(orderByArguments);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("hql", getHql())
.append("hqlParameters", getHqlParameters())
.toString();
}
}