/*
* Copyright (c) 2016 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.core.service.impl.hibernate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType;
import org.obiba.core.service.PagingClause;
import org.obiba.core.service.SortingClause;
import org.obiba.core.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wraps a Hibernate {@link Criteria} to allow adding restrictions on association paths.
*/
public class AssociationCriteria {
static private final Logger log = LoggerFactory.getLogger(AssociationCriteria.class);
/**
* Used to add {@link Restrictions} to an {@link AssociationCriteria}
*/
public enum Operation {
/**
* Used to add an {@link AssociationExample} criterion to the Criteria.
* <p>
* Required values:
* <ol>
* <li>the template object</li>
* </ol>
* </p>
*/
match {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
// Use the whole path (eg: a.b.c) and not the qualifier (eg: a.b)
Criteria c = criteria.getAssociationCriteria(path, false);
c.add(AssociationExample.create(values[0]));
return criteria;
}
},
/**
* Tests that a property is equal to the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
eq {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.eq(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is not equal to the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
ne {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.ne(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is less or equal to the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
le {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.le(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is less than the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
lt {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.lt(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is greater or equal to the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
ge {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.ge(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is greater than the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
gt {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.gt(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is like (SQL semantic) the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
like {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.like(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is case insensitive like (SQL semantic) the specified value.
* <p>
* Required values:
* <ol>
* <li>the value to compare to</li>
* </ol>
* </p>
*/
ilike {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.ilike(propertyName, values[0]));
return criteria;
}
},
/**
* Tests that a property is empty
* <p>
* Required values: none.
* </p>
*/
isEmpty {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.isEmpty(propertyName));
return criteria;
}
},
/**
* Tests that a property is not empty
* <p>
* Required values: none.
* </p>
*/
isNotEmpty {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.isNotEmpty(propertyName));
return criteria;
}
},
/**
* Tests that a property is NULL
* <p>
* Required values: none.
* </p>
*/
isNull {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.isNull(propertyName));
return criteria;
}
},
/**
* Tests that a property is not NULL
* <p>
* Required values: none.
* </p>
*/
isNotNull {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.isNotNull(propertyName));
return criteria;
}
},
/**
* Union of two {@link Criterion} rooted at the specified path
* <p>
* Required values:
* <ol>
* <li>first {@link Criterion}</li>
* <li>second {@link Criterion}</li>
* </ol>
* </p>
*/
or {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(path, false);
if(values.length != 2 || values[0] instanceof Criterion == false || values[1] instanceof Criterion == false) {
throw new IllegalArgumentException(
"Operation.or requires exactly two parameters or type org.hibernate.criterion.Criterion");
}
c.add(Restrictions.or((Criterion) values[0], (Criterion) values[1]));
return criteria;
}
},
/**
* Test whether a property is equal to one of ('in') the specified values
* <p>
* Required values:
* <ol>
* <li>Object[]</li>
* </ol>
* </p>
*/
in {
@Override
protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values) {
Criteria c = criteria.getAssociationCriteria(qualifier, false);
c.add(Restrictions.in(propertyName, values));
return criteria;
}
};
/**
* Add this operation to the specified AssociationCriteria.
*
* @param criteria the criteria
* @param path the association path (eg: a.b.c.name)
* @param values the required operation values
* @return the criteria for method call chaining
*/
protected AssociationCriteria add(AssociationCriteria criteria, String path, Object... values) {
String propertyName = getProperty(path);
String qualifier = getQualifier(path);
log.debug("Adding operation {}({}) at path {}",
new Object[] { toString(), StringUtil.deferToString(values), path });
return add(criteria, path, qualifier, propertyName, values);
}
/**
* Implemented by each operation where the actual {@link Criterion} is added to the {@link Criteria}
*
* @param criteria the criteria
* @param path the association path (eg: a.b.c.name)
* @param qualifier the entity qualifier (eg: a.b.c)
* @param propertyName the entity property name (eg: name)
* @param values the required operation values
* @return the criteria for method call chaining
*/
abstract protected AssociationCriteria add(AssociationCriteria criteria, String path, String qualifier,
String propertyName, Object... values);
}
/**
* The resulting Criteria "rooted" on the initial entity
*/
private final Criteria baseCriteria;
/**
* A Map of association path to the Criteria instance
*/
private final Map<String, Criteria> associationCriteria = new HashMap<String, Criteria>();
/**
* Builds a new instance of an AssociationCriteria for the specified entity type and the specified session.
*
* @param entityType the entity type returned by this criteria.
* @param session the session used to create the Criteria.
*/
public AssociationCriteria(Class<?> entityType, Session session) {
baseCriteria = session.createCriteria(entityType);
associationCriteria.put("", baseCriteria);
// This is not always required.
// When required and not present, the result of the criteria is invalid.
// When not required and present, the result is valid, but performance may suffer.
// The sensible decision is to always use it...
baseCriteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
}
/**
* Constructs a new AssociationCriteria using the builder pattern to allow method chaining.
*
* @param entityType the type of entity this Criteria will return
* @param session the hibernate session used to create the Criteria
* @return the new instance
*/
static public AssociationCriteria create(Class<?> entityType, Session session) {
return new AssociationCriteria(entityType, session);
}
/**
* Adds an {@link Operation} to the {@link Criteria}.
* <p>
* This allows to easily create restrictions on association paths (ie: a.b.c), something
* that the native Hibernate {@link Criteria} does not allow.
* </p>
* <p>
* For example, one can create a "Not Null" restriction on a.b.c.name like so:
* </p>
* <pre>
* AssociationCriteria.create(A.class, session).add(Operation.isNotNull, "a.b.c.name");
* </pre>
* Chaining other {@link AssociationCriteria#add} calls, complex queries can be created easily.
*
* @param op the operation
* @param path the association path (eg: a.b.c.name)
* @param values the parameters required by the {@link Operation} (see the operation javadoc)
* @return this for method call chaining
*/
public AssociationCriteria add(String path, Operation op, Object... values) {
return op.add(this, path, values);
}
/**
* Adds {@link SortingClause}s to the resulting {@link Criteria}.
*
* @param clauses an array of {@link SortingClause}
* @return this for method call chaining
*/
public AssociationCriteria addSortingClauses(SortingClause... clauses) {
if(clauses != null && clauses.length > 0) {
for(SortingClause clause : clauses) {
if(clause != null) {
String path = clause.getField();
String qualifier = getQualifier(path);
String property = getProperty(path);
Criteria c = getAssociationCriteria(qualifier, true);
c.addOrder(clause.isAscending() ? Order.asc(property) : Order.desc(property));
}
}
}
return this;
}
/**
* Adds a {@link PagingClause} to the resulting {@link Criteria}
*
* @param clause the paging clause
* @return this for method call chaining
*/
public AssociationCriteria addPagingClause(PagingClause clause) {
if(clause != null) {
if(clause.getOffset() > 0) {
baseCriteria.setFirstResult(clause.getOffset());
}
if(clause.getLimit() > 0) {
baseCriteria.setMaxResults(clause.getLimit());
}
}
return this;
}
/**
* Returns the resulting {@link Criteria}
*
* @return the resulting {@link Criteria}
*/
public Criteria getCriteria() {
return baseCriteria;
}
public int count() {
Object res = baseCriteria.setProjection(Projections.rowCount()).uniqueResult();
return res != null ? Long.valueOf(res.toString()).intValue() : 0;
}
@SuppressWarnings("unchecked")
public <T> List<T> list() {
return baseCriteria.list();
}
/**
* Returns the {@link Criteria} "rooted" at the specified association path. This method will create
* all the required {@link Criteria} instances along the path.
*
* @param path the association path from the root (eg: a.b.c)
* @param leftJoin whether to use a left join when creating new {@link Criteria} instances (useful when creating paths for sorting)
* @return the {@link Criteria} instance "rooted" at the specified path.
*/
protected Criteria getAssociationCriteria(String path, boolean leftJoin) {
Criteria c = associationCriteria.get(path);
if(c == null) {
c = createAssociationCriteria(path, leftJoin);
}
return c;
}
/**
* Creates a {@link Criteria} instance for the specified path. This method walks the
* association path and creates any required {@link Criteria} along the way. After returning,
* each node in the path will have its own {@link Criteria} instance.
*
* @param path the path (eg: a.b.c)
* @param leftJoin true to create left joins for new {@link Criteria} instances
* @return an instance of {@link Criteria} "rooted" at the specified path.
*/
protected Criteria createAssociationCriteria(String path, boolean leftJoin) {
String[] elements = path.split("\\.");
Criteria parentCriteria = baseCriteria;
StringBuilder sb = new StringBuilder();
for(int i = 0; i < elements.length; i++) {
if(i > 0) sb.append(".");
String subPath = sb.append(elements[i]).toString();
Criteria criteria = associationCriteria.get(subPath);
if(criteria == null) {
criteria = createSubCriteria(parentCriteria, elements[i], leftJoin);
associationCriteria.put(subPath, criteria);
}
parentCriteria = criteria;
}
return parentCriteria;
}
/**
* Creates a sub criteria with the specified Criteria as its parent.
*
* @param parentCriteria the parent {@link Criteria}
* @param property the property name
* @param leftJoin true to create a left join association
* @return the new {@link Criteria} instance for the specified property of the parent {@link Criteria}
*/
protected Criteria createSubCriteria(Criteria parentCriteria, String property, boolean leftJoin) {
return leftJoin
? parentCriteria.createCriteria(property, JoinType.LEFT_OUTER_JOIN)
: parentCriteria.createCriteria(property);
}
/**
* Given a.b.name returns name
*
* @param path the association path to an entity's property
* @return the property expression
*/
static private String getProperty(String path) {
int dotIndex = path.lastIndexOf('.');
if(dotIndex != -1) {
return path.substring(dotIndex + 1);
}
return path;
}
/**
* Given a.b.name returns a.b
*
* @param path the association path to an entity's property
* @return the qualifier (path from root) to the entity
*/
static private String getQualifier(String path) {
int dotIndex = path.lastIndexOf('.');
if(dotIndex != -1) {
return path.substring(0, dotIndex);
}
// It's the root entity
return "";
}
}