/* * Copyright 2008 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.tools.hibernate; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import ome.conditions.ApiUsageException; import ome.parameters.Filter; import ome.parameters.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.Query; import org.hibernate.Session; import org.springframework.aop.framework.Advised; /** * Very thin wrapper around a {@link StringBuilder} to generate HQL queries. * This comes from the very real deficiencies of the Criteria API when trying to * implement ome.services.SearchBean. * * Note: It is the responsibility of each method here to end with a blank space, * meaning that each method may also begin without one. * * This class is NOT thread-safe. * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 */ public class QueryBuilder { private final static Logger log = LoggerFactory.getLogger(QueryBuilder.class); private int count = 0; private final StringBuilder select = new StringBuilder(); private final StringBuilder from = new StringBuilder(); private final StringBuilder join = new StringBuilder(); private final StringBuilder where = new StringBuilder(); private final StringBuilder order = new StringBuilder(); private final StringBuilder group = new StringBuilder(); private final Set<String> random = new HashSet<String>(); private final Map<Integer, Object> seqParams = new HashMap<Integer, Object>(); private final Map<String, Object> params = new HashMap<String, Object>(); private final Map<String, Collection> listParams = new HashMap<String, Collection>(); /** * final {@link StringBuilder} instance which is currently being modified. * For example, when "select" is called, then the {@link #select} instance * is stored in {@link #current} so that calls to methods like * {@link #append(String)} do the right thing. */ private StringBuilder current; /** * Number of clauses in the where. */ private int whereCount = 0; private String self; private Filter filter; /** * The path which the userId and the groupId in the {@link Filter} (if * present} will be applied to. */ private String filterTarget; /** * @see {@link QueryBuilder#QueryBuilder(boolean)} */ private boolean sqlQuery = false; public QueryBuilder() { // no-op } /** * Whether {@link Session#createSQLQuery(String)} should be used or not during * {@link #query(Session)} and similar. */ public QueryBuilder(boolean sqlQuery) { this.sqlQuery = sqlQuery; } public QueryBuilder(int size) { // ignore size for the moment this(); } /** * Obtain a unique alias to be used in the SQL. * @param prefix the prefix for the alias * @return a unique alias */ public String unique_alias(String prefix) { StringBuilder sb = new StringBuilder(prefix.length() + 8); sb.append(prefix.trim()); while (random.contains(sb.toString())) { sb.append(count++); } String alias = sb.toString(); random.add(alias); return alias; } /** * Simple delegate method to allow appending arbitrary strings. */ public QueryBuilder append(String string) { current.append(string); return this; } public QueryBuilder select(String... selects) { _type("select"); if (selects == null || selects.length == 0) { throw new ApiUsageException("Empty select"); } for (int i = 0; i < selects.length; i++) { if (i != 0) { select.append(", "); } select.append(selects[i]); appendSpace(); } return this; } void _type(String type) { current = select; if (select.length() == 0) { select.append(type); appendSpace(); } } public QueryBuilder from(String type, String alias) { current = from; if (from.length() == 0) { from.append("from "); } else { from.append(", "); } this.self = alias; from.append(type); appendSpace(); from.append("as "); from.append(alias); appendSpace(); return this; } public QueryBuilder join(String path, String alias, boolean outer, boolean fetch) { current = join; if (outer) { join.append("left outer "); } join.append("join "); if (fetch) { join.append("fetch "); } join.append(path); appendSpace(); join.append("as "); join.append(alias); appendSpace(); return this; } /** * Marks the end of all fetches by adding a "where" clause to the string. */ public QueryBuilder where() { current = where; if (where.length() == 0) { where.append("where "); } return this; } /** * Appends "and" plus your string unless this is the first where-spec in * which case it is simply appended. */ public QueryBuilder and(String str) { return _where("and ", str); } /** * Appends "or" plus your string unless this is the first where-spec in * which case it is simply appended. */ public QueryBuilder or(String str) { return _where("or ", str); } private QueryBuilder _where(String bool, String str) { where(); // check size and set current whereCount++; if (whereCount != 1) { where.append(bool); } where.append(str); appendSpace(); return this; } /** * Appends the string representation of the {@link QueryBuilder} argument * inside of parentheses. */ public QueryBuilder subselect(QueryBuilder subselect) { // Leave current as is. if ("".equals(subselect.queryString().trim())) { // Nothing to do. return this; } current.append("("); current.append(subselect.queryString()); current.append(")"); for (String key : subselect.listParams.keySet()) { this.listParams.put(key, subselect.listParams.get(key)); } appendSpace(); return this; } public QueryBuilder order(String path, boolean ascending) { current = order; if (order.length() == 0) { order.append("order by "); } else { order.append(", "); } order.append(path); appendSpace(); if (ascending) { order.append("asc "); } else { order.append("desc "); } return this; } public QueryBuilder param(int id, Object o) { seqParams.put(id, o); return this; } public QueryBuilder param(String key, Object o) { params.put(key, o); return this; } public QueryBuilder paramList(String key, Collection c) { listParams.put(key, c); return this; } /** * In order to support the order() method in addition * to a filter, we allow applying the filter and nulling * the instance eagerly before the user calls order. */ public QueryBuilder filterNow() { if (filter != null && filterTarget != null) { if (filter.owner() >= 0) { this.and(filterTarget+".details.owner.id = "); String alias = this.unique_alias("owner"); this.append(":"); this.append(alias); this.param(alias, filter.owner()); appendSpace(); } if (filter.group() >= 0) { this.and(filterTarget+".details.group.id = "); String alias = this.unique_alias("group"); this.append(":"); this.append(alias); this.param(alias, filter.group()); appendSpace(); } } return this; } public Query queryWithoutFilter(Session session) { return __query(session, false); } public Query query(Session session) { return __query(session, true); } private Query __query(Session session, boolean usefilter) { if (usefilter) { filterNow(); } Query q = null; try { final String s = queryString(); if (sqlQuery) { // ticket:9435 - in order to allow updates with raw // SQL we will unwrap the session. This is the only // location that is doing such unwrapping. // Also see ticket:9496 about deleting rdefs. if (s.startsWith("update") || s.startsWith("delete")) { if (session instanceof Advised) { Advised proxy = (Advised) session; try { session = (Session) proxy.getTargetSource().getTarget(); } catch (Exception e) { RuntimeException rt = new RuntimeException(e); rt.initCause(e); throw rt; } } } q = session.createSQLQuery(queryString()); } else { q = session.createQuery(queryString()); } } catch (RuntimeException rt) { // We're logging failed queries because the almost always point // to an internal exception that shouldn't be happening. log.warn("Failed query: " + queryString(), rt); throw rt; } for (String key : params.keySet()) { q.setParameter(key, params.get(key)); } for (Integer key : seqParams.keySet()) { q.setParameter(key, seqParams.get(key)); } for (String key : listParams.keySet()) { q.setParameterList(key, listParams.get(key)); } if (filter != null) { if (filter.limit !=null) { q.setMaxResults(filter.limit); } if (filter.offset != null) { q.setFirstResult(filter.offset); } } return q; } public QueryBuilder appendSpace() { if (current.length() == 0) { current.append(' '); } else if (current.charAt(current.length() - 1) != ' ') { current.append(' '); } return this; } /** * Returns the current query as a String. As opposed to {@link #toString()}, * this method should return parseable HQL. * @return the current HQL query */ public String queryString() { StringBuilder sb = new StringBuilder(); sb.append(this.select.toString()); sb.append(this.from.toString()); sb.append(this.join.toString()); sb.append(this.where.toString()); sb.append(this.group.toString()); sb.append(this.order.toString()); return sb.toString(); } @Override public String toString() { StringBuilder toString = new StringBuilder(); toString.append(queryString()); toString.append(params); toString.append(seqParams); toString.append(listParams); return toString.toString(); } // State methods // Used in case the standard workflow is not optimal public void update(String table) { _type("update"); select.append(table); appendSpace(); skipFrom(); } public void delete(String table) { if (sqlQuery) { _type("delete from "); } else { _type("delete"); } select.append(table); appendSpace(); skipFrom(); } public QueryBuilder skipFrom() { current = from; appendSpace(); return this; } public QueryBuilder skipWhere() { current = where; appendSpace(); return this; } /** * Similar to how skipFrom and skipWhere were previously used, this sets * the current builder to {@link #where()} but without prefacing the * "where " string. Instead, it adds a space so that further calls to * {@link #where()} also won't add it. This can be used to create a clause * that can later be combined via {@link #subselect(QueryBuilder)}. */ public QueryBuilder whereClause() { current = where; appendSpace(); // Add something empty. return this; } public void filter(String string, Filter filter) { filterTarget = string; this.filter = filter; } public void params(Parameters params2) { if (params2 != null) { for (String key : params2.keySet()) { Object o = params2.get(key).value; if (o instanceof Collection) { paramList(key, (Collection) o); } else { param(key, o); } } } } }