// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.db.hqlbuilder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.transform.DistinctRootEntityResultTransformer;
import edu.harvard.med.screensaver.db.Criterion.Operator;
import edu.harvard.med.screensaver.db.SortDirection;
import edu.harvard.med.screensaver.model.Entity;
import edu.harvard.med.screensaver.model.meta.PropertyPath;
import edu.harvard.med.screensaver.model.meta.RelationshipPath;
import edu.harvard.med.screensaver.util.StringUtils;
// TODO: would be beneficial to refactor this class such that query args are not permanently
// set in the query that is built; instead the built query should allow different arg values
// to be applied each time toQuery() is invoked
public class HqlBuilder
{
// static members
private static Logger log = Logger.getLogger(HqlBuilder.class);
static final String SET_ARG_SUFFIX = "Set";
// inner classes
private boolean _isDistinct = false;
private boolean _isDistinctRootEntities = false;
private StringBuilder _select = new StringBuilder();
private StringBuilder _from = new StringBuilder();
private CompositePredicate _where = new Conjunction();
private CompositePredicate _having = new Conjunction();
private List<String> _orderBy = Lists.newArrayList();
private List<String> _groupBy = Lists.newArrayList();
private Map<String,Object> _args = new HashMap<String,Object>();
private Set<String> _aliases = new HashSet<String>();
private Set<String> _selectAliases = new HashSet<String>();
// public constructors & methods
public HqlBuilder()
{
}
public HqlBuilder distinctProjectionValues()
{
_isDistinct = true;
return this;
}
public HqlBuilder distinctRootEntities()
{
_isDistinctRootEntities = true;
return this;
}
/**
* Select a full entity to retrieved.
* @param alias
*/
public HqlBuilder select(String alias)
{
return select(alias, null);
}
public HqlBuilder selectExpression(String expression)
{
if (_select.length() > 0) {
_select.append(", ");
}
_select.append(expression);
return this;
}
public HqlBuilder select(String alias, String property)
{
_selectAliases.add(alias);
if (_select.length() > 0) {
_select.append(", ");
}
_select.append(alias);
if (property != null) {
_select.append('.').append(property);
} // else, select the full entity
return this;
}
public HqlBuilder from(Class<?> entityClass, String alias)
{
checkAliasIsUnique(alias);
if (_from.length() > 0) {
_from.append(", ");
}
_from.append(entityName(entityClass)).append(' ').append(alias);
return this;
}
public HqlBuilder fromFetch(String joinAlias, RelationshipPath joinRelationship, String alias)
{
return from(joinAlias, joinRelationship, alias, JoinType.LEFT_FETCH);
}
public HqlBuilder from(String joinAlias, RelationshipPath joinRelationship, String alias)
{
return from(joinAlias, joinRelationship, alias, JoinType.LEFT);
}
public HqlBuilder from(String joinAlias, RelationshipPath joinRelationship, String alias, JoinType joinType)
{
// if (joinRelationship.getPathLength() > 1) {
// throw new IllegalArgumentException("joinRelationship length must be 1");
// }
if (joinRelationship instanceof PropertyPath) {
if (!((PropertyPath) joinRelationship).isCollectionOfValues()) {
throw new IllegalArgumentException("joinRelationships that are PropertyPaths must be a 'collection of values'");
}
return from(joinAlias, ((PropertyPath) joinRelationship).getPropertyName(), alias, joinType);
}
return from(joinAlias, joinRelationship.getLeaf(), alias, joinType);
}
private HqlBuilder from(String joinAlias, String joinRelationship, String alias, JoinType joinType)
{
checkAliasExists(joinAlias);
checkAliasIsUnique(alias);
if (_from.length() == 0) {
throw new IllegalStateException("must call from(Class, String) first");
}
if (joinType == JoinType.LEFT) {
_from.append(" left join ");
}
else if (joinType == JoinType.LEFT_FETCH) {
_from.append(" left join fetch ");
}
else {
_from.append(" join ");
}
_from.append(joinAlias).append('.').append(joinRelationship).append(' ').append(alias);
return this;
}
public HqlBuilder restrictFrom(String alias, String property, Operator operator, Object value)
{
checkAliasExists(alias);
_from.append(" with ").append(new SimplePredicate(this, makeRef(alias, property), operator, value).toHql());
return this;
}
public HqlBuilder where(String alias, String property, Operator operator, Object value)
{
checkAliasExists(alias);
_where.add(simplePredicate(makeRef(alias, property), operator, value));
return this;
}
/**
* Use when lhs of where expression is a collection of values (which
* themselves have no internal property other than the value itself).
*/
public HqlBuilder where(String alias, Operator operator, Object value)
{
return where(alias, null, operator, value);
}
public HqlBuilder where(String alias1, String property1, Operator operator, String alias2, String property2)
{
checkAliasExists(alias1);
checkAliasExists(alias2);
_where.add(simplePredicate(makeRef(alias1, property1), makeRef(alias2, property2), operator));
return this;
}
/**
* Use when lhs and rhs of where expression is a collection of values (which
* themselves have no internal property other than the value itself).
*/
public HqlBuilder where(String alias1, Operator operator, String alias2)
{
return where(alias1, null, operator, alias2, null);
}
/**
* Use when the rhs of where expression is an entity object, in which case
* Hibernate will use the entity's ID.
*/
public HqlBuilder where(String alias, Entity entity)
{
checkAliasExists(alias);
_where.add(simplePredicate(alias, Operator.EQUAL, entity));
return this;
}
public HqlBuilder whereIn(String alias, String property, Set<?> values)
{
checkAliasExists(alias);
if (values.size() == 0) {
_where.add(SimplePredicate.FALSE);
}
else {
_where.add(new SimplePredicate(this, makeRef(alias, property), values));
}
return this;
}
/**
* Use when the lhs of where..in expression is a collection of values (which
* themselves have no internal property other than the value itself).
*/
public HqlBuilder whereIn(String alias, Set<?> values)
{
return whereIn(alias, null, values);
}
public HqlBuilder where(Predicate predicate)
{
_where.add(predicate);
return this;
}
public Disjunction disjunction()
{
return new Disjunction();
}
public Conjunction conjunction()
{
return new Conjunction();
}
public SimplePredicate simplePredicate(String lhs, Operator operator, Object value)
{
return new SimplePredicate(this, lhs, operator, value);
}
public SimplePredicate simplePredicate(String lhs, String property, Operator operator, Object value)
{
return new SimplePredicate(this, makeRef(lhs, property), operator, value);
}
public SimplePredicate simplePredicate(String lhs, String rhs, Operator operator)
{
return new SimplePredicate(this, lhs, rhs, operator);
}
public HqlBuilder orderBy(String alias, String property)
{
return orderBy(alias, property, SortDirection.ASCENDING);
}
/**
* Use when you need to order by a collection of values (which
* themselves have no internal property other than the value itself).
*/
public HqlBuilder orderBy(String alias, SortDirection sortDirection)
{
return orderBy(alias, null, sortDirection);
}
public HqlBuilder orderBy(String alias, String property, SortDirection sortDirection)
{
checkAliasExists(alias);
StringBuilder orderBy = new StringBuilder(alias);
if (!!!StringUtils.isEmpty(property)) {
orderBy.append('.').append(property);
}
if (sortDirection == SortDirection.DESCENDING) {
orderBy.append(" desc");
}
_orderBy.add(orderBy.toString());
return this;
}
public String toHql()
{
StringBuilder _hql = new StringBuilder();
if (!_aliases.containsAll(_selectAliases)) {
Set<String> _undefinedSelectAliases = new HashSet<String>(_selectAliases);
_undefinedSelectAliases.removeAll(_aliases);
throw new RuntimeException("select aliases " + _undefinedSelectAliases + " undefined");
}
if (_select.length() > 0) {
_hql.append("select ");
if (_isDistinct) {
_hql.append("distinct ");
}
_hql.append(_select).append(' ');
}
if (_from.length() == 0) {
throw new RuntimeException("empty from clause");
}
_hql.append("from " ).append(_from);
if (_where.size() > 0) {
_hql.append(" where ").append(_where.toHql());
}
if (!!!_groupBy.isEmpty()) {
// note: if we're using "group by", we also need to explicitly group by
// any fields that we're ordering on, and these need to be first if
// we're going to respect the requested ordering
// TODO: as convenience, we could also group on any select fields that are non-aggregate expressions
List<String> nonRedundantGroupBy = Lists.newArrayList(_groupBy);
nonRedundantGroupBy.removeAll(_orderBy);
_hql.append(" group by ").append(Joiner.on(", ").join(Iterables.concat(_orderBy, nonRedundantGroupBy)));
}
if (_having.size() > 0) {
_hql.append(" having ").append(_having.toHql());
}
if (!!!_orderBy.isEmpty()) {
_hql.append(" order by ").append(Joiner.on(", ").join(_orderBy));
}
return _hql.toString();
}
public Map<String,Object> args()
{
return _args;
}
public Object arg(String name)
{
return _args.get(name);
}
public Query toQuery(Session session, boolean isReadOnly)
{
org.hibernate.Query query = session.createQuery(toHql());
for (Map.Entry<String,Object> arg : args().entrySet()) {
if (arg.getKey().endsWith(SET_ARG_SUFFIX)) {
// HACK: handle 'list' type parameters, used with the 'IN (?)' operator
query.setParameterList(arg.getKey(), (Set<?>) arg.getValue());
}
else {
query.setParameter(arg.getKey(), arg.getValue());
}
}
if (_isDistinctRootEntities) {
query.setResultTransformer(DistinctRootEntityResultTransformer.INSTANCE);
}
query.setReadOnly(isReadOnly);
return query;
}
@Override
public String toString()
{
return toHql() + " " + args();
}
// private methods
private String entityName(Class<?> entityClass)
{
return entityClass.getSimpleName();
}
private void checkAliasExists(String alias)
{
if (!_aliases.contains(alias)) {
throw new IllegalArgumentException("alias " + alias + " not defined");
}
}
private void checkAliasIsUnique(String alias)
{
if (!_aliases.add(alias)) {
throw new IllegalArgumentException("alias " + alias + " is already used");
}
}
private String makeRef(String alias, String property)
{
if (!!!StringUtils.isEmpty(property)) {
return alias + "." + property;
}
return alias;
}
public HqlBuilder groupBy(String alias)
{
return groupBy(alias, null);
}
public HqlBuilder groupBy(String alias, String property)
{
checkAliasExists(alias);
StringBuilder groupBy = new StringBuilder(alias);
if (!!!StringUtils.isEmpty(property)) {
groupBy.append('.').append(property);
}
_groupBy.add(groupBy.toString());
return this;
}
public HqlBuilder having(Predicate havingPredicate)
{
_having.add(havingPredicate);
return this;
}
}