/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos 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 2 of the License, or
(at your option) any later version.
Cyclos 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 Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.utils.hibernate;
import java.beans.PropertyDescriptor;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import nl.strohalm.cyclos.entities.Entity;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.PropertyHelper;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
/**
* Class with helper method to work with Hibernate
* @author rafael
*/
public class HibernateHelper {
/**
* Contains a value and an operator for use on queries
*
* @author luis
*/
public static class QueryParameter {
private final Object value;
private final String operator;
public QueryParameter(final Object value, final String operator) {
this.value = value;
this.operator = operator;
}
public String getOperator() {
return operator;
}
public Object getValue() {
return value;
}
}
/**
* Cached direct relationship properties (child -> parent) by entity class
*/
private static Map<Class<? extends Entity>, Set<String>> directPropertiesCache = new HashMap<Class<? extends Entity>, Set<String>>();
/**
* Adds an "in elements()" operator parameter to the HQL query, if the given value is not empty, appending the values to the named parameters map
* Used to search on associated relations
*/
public static void addInElementsParameter(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final Entity value) {
if (value != null && value.isPersistent()) {
final String parameterName = getParameterName(namedParameters, path);
hql.append(" and :").append(parameterName).append(" in elements(").append(path).append(") ");
namedParameters.put(parameterName, value);
}
}
/**
* Adds an "in" operator parameter to the HQL query, if the given value is not empty, appending the values to the named parameters map
*/
public static void addInParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final Collection<?> values) {
if (values != null && !values.isEmpty()) {
final String parameterName = getParameterName(namedParameters, path);
hql.append(" and ").append(path).append(" in (:").append(parameterName).append(") ");
namedParameters.put(parameterName, values);
}
}
/**
* Adds an "in" operator parameter to the HQL query, if the given value is not empty, appending the values to the named parameters map
*/
public static void addInParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final Object... values) {
if (values != null && values.length > 0) {
addInParameterToQuery(hql, namedParameters, path, Arrays.asList(values));
}
}
/**
* Adds a 'path like %value%' parameter to the HQL query if the given value is not empty, appending the value to the named parameters map
*/
public static void addLikeParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final String value) {
doAddLike(hql, namedParameters, path, value, false);
}
/**
* Adds a equals parameter to the HQL query, if the given value is not empty, appending the value to the named parameters map
*/
public static void addParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final Object value) {
addParameterToQueryOperator(hql, namedParameters, path, "=", value);
}
/**
* Adds a custom parameter to the HQL query, if the given parameter is not empty, appending the value to the named parameters map
*/
public static void addParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final QueryParameter parameter) {
if (parameter != null) {
addParameterToQueryOperator(hql, namedParameters, path, parameter.getOperator(), parameter.getValue());
}
}
/**
* Adds a custom operator parameter to the HQL query, if the given value is not empty, appending the value to the named parameters map
*/
public static void addParameterToQueryOperator(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final String operator, final Object value) {
if (value != null && !"".equals(value)) {
final String parameterName = getParameterName(namedParameters, path);
hql.append(" and ").append(path).append(" ").append(operator).append(" :").append(parameterName).append(" ");
namedParameters.put(parameterName, value);
}
}
/**
* Adds a period test to the HQL query, if the given period is not empty, appending the value to the named parameters map. See {@link Period}, as
* it controls whether the begin and end dates are inclusive / exclusive.
*
*/
public static void addPeriodParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final Period period) {
addParameterToQuery(hql, namedParameters, path, getBeginParameter(period));
addParameterToQuery(hql, namedParameters, path, getEndParameter(period));
}
/**
* Adds a 'path like value%' parameter to the HQL query if the given value is not empty, appending the value to the named parameters map
*/
public static void addRightLikeParameterToQuery(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, final String value) {
doAddLike(hql, namedParameters, path, value, true);
}
/**
* Appends the join portion on the query to fetch the specified relationships, when appliable
*/
public static void appendJoinFetch(final StringBuilder hql, final Class<? extends Entity> entityType, final String entityAlias, final Collection<Relationship> fetch) {
if (fetch != null) {
final Set<String> directRelationships = getDirectRelationshipProperties(entityType, fetch);
for (final String directRelationship : directRelationships) {
hql.append(" left join fetch ").append(entityAlias).append(".").append(directRelationship).append(" ");
}
}
}
/**
* Appends the order by portion, with the given path lists (with an optional direction, ie: "e.date desc", "e.name", "x.name")
*/
public static void appendOrder(final StringBuilder hql, final Collection<String> paths) {
if (CollectionUtils.isNotEmpty(paths)) {
hql.append(" order by " + StringUtils.join(paths.iterator(), ","));
}
}
/**
* Appends the order by portion, with the given path lists (with an optional direction, ie: "e.date desc", "e.name", "x.name")
*/
public static void appendOrder(final StringBuilder hql, final String... paths) {
if (paths != null && paths.length > 0) {
appendOrder(hql, Arrays.asList(paths));
}
}
/**
* Returns the begin date of the given period, handling null
*/
public static QueryParameter getBeginParameter(final Period period) {
if (period == null) {
return null;
}
Calendar begin = period.getBegin();
if (begin == null) {
return null;
}
// We must consider the time when explicitly set
if (!period.isUseTime()) {
// Truncate the begin date
begin = DateHelper.truncate(begin);
}
String operator = period.isInclusiveBegin() ? ">=" : ">";
return new QueryParameter(begin, operator);
}
/**
* Returns the end date of the given period, handling null
*/
public static QueryParameter getEndParameter(final Period period) {
if (period == null) {
return null;
}
Calendar end = period.getEnd();
if (end == null) {
return null;
}
// We must consider the time when explicitly set
if (!period.isUseTime()) {
// Truncate the end date and set the next day
end = DateHelper.getDayEnd(end);
}
String operator = period.isInclusiveEnd() ? "<=" : "<";
return new QueryParameter(end, operator);
}
/**
* Returns a StringBuilder containing the begin of a single entity select HQL
* @param entityType The entity type to search
* @param entityAlias The entity alias on the query
* @return The StringBuiler
*/
public static StringBuilder getInitialQuery(final Class<? extends Entity> entityType, final String entityAlias) {
return getInitialQuery(entityType, entityAlias, null);
}
/**
* Returns a StringBuilder containing the begin of a single entity select HQL, with the especified fetch relationships, when appliable
* @param entityType The entity type to search
* @param entityAlias The entity alias on the query
* @param fetch The relationships to fetch
* @return The StringBuiler
*/
public static StringBuilder getInitialQuery(final Class<? extends Entity> entityType, final String entityAlias, final Collection<Relationship> fetch) {
final StringBuilder hql = new StringBuilder(" from ").append(entityType.getName()).append(" ").append(entityAlias).append(" ");
appendJoinFetch(hql, entityType, entityAlias, fetch);
hql.append(" where 1=1 ");
return hql;
}
private static void doAddLike(final StringBuilder hql, final Map<String, Object> namedParameters, final String path, String value, final boolean rightOnly) {
value = StringUtils.trimToNull(value);
if (value == null) {
return;
}
// Remove any manually entered '%'
value = StringUtils.trimToNull(StringUtils.replace(value, "%", ""));
if (value == null) {
return;
}
// Assuming the default database collation is case insensitive, we don't need to perform case transformations
if (rightOnly) {
value += "%";
} else {
value = "%" + value + "%";
}
addParameterToQueryOperator(hql, namedParameters, path, "like", value);
}
/**
* Returns a set of properties that will be fetched directly on the HQL
*/
private static Set<String> getDirectRelationshipProperties(final Class<? extends Entity> entityType, final Collection<Relationship> fetch) {
// Populate the direct properties cache for this entity if not yet exists
Set<String> cachedDirectProperties = directPropertiesCache.get(entityType);
if (cachedDirectProperties == null) {
cachedDirectProperties = new HashSet<String>();
final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(entityType);
// Scan for child -> parent relationships
for (final PropertyDescriptor descriptor : propertyDescriptors) {
if (descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null && Entity.class.isAssignableFrom(descriptor.getPropertyType())) {
// This is a child -> parent relationship. Add it to the cache
cachedDirectProperties.add(descriptor.getName());
}
}
directPropertiesCache.put(entityType, cachedDirectProperties);
}
// Build the properties to add to HQL fetch from a given relationship set
final Set<String> propertiesToAddToFetch = new HashSet<String>();
for (final Relationship relationship : fetch) {
final String name = PropertyHelper.firstProperty(relationship.getName());
if (cachedDirectProperties.contains(name)) {
propertiesToAddToFetch.add(name);
}
}
return propertiesToAddToFetch;
}
/**
* Generates a parameter name
*/
private static String getParameterName(final Map<String, Object> namedParameters, final String propertyName) {
int counter = 1;
// Transform the property in a valid identifier
final StringBuilder sb = new StringBuilder(propertyName.length());
for (int i = 0, len = propertyName.length(); i < len; i++) {
final char c = propertyName.charAt(i);
if (Character.isJavaIdentifierPart(c)) {
sb.append(c);
} else {
sb.append('_');
}
}
final String field = sb.toString();
String parameterName = field.concat("_1");
while (namedParameters.containsKey(parameterName)) {
parameterName = field.concat("_").concat(String.valueOf(++counter));
}
return parameterName;
}
}