All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.topia.persistence.HqlAndParametersBuilder Maven / Gradle / Ivy

The newest version!
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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy