/* * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ /*------------------------------------------------------------------------------ * * Written by: Josh Moore <josh.moore@gmx.de> * *------------------------------------------------------------------------------ */ package ome.services.query; import java.sql.SQLException; import java.util.HashSet; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.Criteria; import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.Session; import org.springframework.orm.hibernate3.HibernateCallback; import ome.parameters.Parameters; import static ome.parameters.Parameters.*; import ome.parameters.QueryParameter; /** * base Query type to facilitate the creation of ORM queries. This class * attempts to enforce a strict usage pattern. First, subclasses must define a * {@link ome.services.query.Definitions} instance, which can optionally (and * perhaps preferably) be static, which must be passed into the super * constructor along with the {@link ome.parameters.Parameters} provided during * lookup. * <p> * Queries can optionally define a {@link #enableFilters(Session)} method * (perhaps using pre-defined filters like * {@link #ownerOrGroupFilters(Session, String[], String[])} to limit the entities * returned. * </p> * * @author Josh Moore, <a href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a> * @since OMERO 3.0 */ public abstract class Query<T> implements HibernateCallback { private static Logger log = LoggerFactory.getLogger(Query.class); // For Criteria /** * imported constant for ease of use * * @see FetchMode#JOIN */ protected final static FetchMode FETCH = FetchMode.JOIN; /** * imported constant for ease of use * * @see Criteria#LEFT_JOIN */ protected final static int LEFT_JOIN = Criteria.LEFT_JOIN; /** * imported constant for ease of use * * @see Criteria#INNER_JOIN */ protected final static int INNER_JOIN = Criteria.INNER_JOIN; /** * container of {@link QueryParameterDef} instances. Typically created * statically in Query subclasses and passed to the * {@link Query#Query(Definitions, Parameters)} constructor */ protected final Definitions defs; /** * container of {@link QueryParameter} instances. These must at least cover * all the {@link QueryParameterDef}s defined for this {@link Query} but * can define more. Other special fields of {@link Parameters}, such as a * {@link ome.parameters.Filter} instance can also be used by {@link Query} * instances. */ protected final Parameters params; /** * one of two possible outcomes of the {@link #buildQuery(Session)} method. * If not null on {@link #doInHibernate(Session) execution}, this instance * will be used. * * This field is private so that we can guarantee that subclasses build one * and only one representation. * * @see #_criteria */ private org.hibernate.Query _query; /** * one of two possible outcomes of the {@link #buildQuery(Session)} method. * If not null on {@link #doInHibernate(Session) execution}, this instance * will be used. * * This field is private so that we can guarantee that subclasses build one * and only one representation. * * @see #_query */ private org.hibernate.Criteria _criteria; /* have to have the Parameters */ private Query() { this.defs = null; this.params = null; checkParameters(); } /** * main constructor used by subclasses. Both arguments must be provided. * * @param definitions * Not null. * @param parameters * Not null. */ public Query(Definitions definitions, Parameters parameters) { this.defs = definitions; this.params = parameters; checkParameters(); } /** * check the {@link Parameters} instance against the {@link Definitions} * instance for this Query. Can be extended by subclasses, but * <code>super.checkParameters()</code> should most likely be called. */ protected void checkParameters() { if (defs == null) { throw new IllegalStateException( "Query parameter definitions not set."); } if (params == null) { throw new IllegalArgumentException("Null arrays " + "are not valid for definitions."); } Set<String> missing = new HashSet<String>(); for (String name : defs.keySet()) { if (!defs.get(name).optional && !params.keySet().contains(name)) { missing.add(name); } } if (missing.size() > 0) { throw new IllegalArgumentException( "Required parameters missing from query: " + missing); } for (String name : defs.keySet()) { QueryParameter parameter = params.get(name); QueryParameterDef def = defs.get(name); def.errorIfInvalid(parameter); } } // ~ Lookups // ========================================================================= /** * check that there is a {@link Definitions definition} for this * {@link Query} with the provided argument as its * {@link QueryParameterDef#name name}. */ public boolean check(String name) { return defs.containsKey(name); } /** * get the {@link QueryParameter} for this name argument. */ public QueryParameter get(String name) { return params.get(name); } /** * get the Object value for this name argument. Returns null even if no * QueryParameter is associated with the name (no exception). */ public Object value(String name) { QueryParameter p = params.get(name); return p == null ? null : p.value; } // ~ Execution // ========================================================================= /** * template method defined by {@link org.springframework.orm.hibernate3.HibernateTemplate}. * This does not need to be overridden by subclasses, but rather * {@link #buildQuery(Session)}. * This ensures that the filters are set properly, that * {@link #buildQuery(Session)} does its job, and that everything is cleaned * up properly afterwards. * * It also enforces contracts established by {@link Parameters} and * {@link ome.parameters.Filter} */ public Object doInHibernate(Session session) throws HibernateException, SQLException { try { enableFilters(session); buildQuery(session); if (_query == null && _criteria == null) { throw new IllegalStateException( "buildQuery did not properly define a Query or " + "Criteria\n by calling setQuery() or setCriteria()."); } boolean unique = params.isUnique(); // ticket:1232 int offset = 0; int limit = Integer.MAX_VALUE; if (params.getOffset() != null) { offset = params.getOffset(); } if (params.getLimit() != null) { limit = params.getLimit(); } if (_query != null) { _query.setFirstResult(offset); _query.setMaxResults(limit); return unique ? _query.uniqueResult() : _query.list(); } else { _criteria.setFirstResult(offset); _criteria.setMaxResults(limit); return unique ? _criteria.uniqueResult() : _criteria.list(); } } finally { disableFilters(session); } } /** * main point of entry for subclasses. This method must build either a * {@link org.hibernate.Criteria} or a {@link org.hibernate.Query} instance * and make it available via {@link #setCriteria(org.hibernate.Criteria)} or * {@link #setQuery(org.hibernate.Query)} */ protected abstract void buildQuery(Session session) throws HibernateException, SQLException; /** * provide this Query instance with a {@link org.hibernate.Query} to be used * for retrieving data. {@link #setCriteria(org.hibernate.Criteria)} should * not also be called with a non-null value. */ protected void setQuery(org.hibernate.Query query) { if (_criteria != null) { throw new IllegalStateException( "This Query already has a Criteria set:" + _criteria); } _query = query; } /** * provide this Query instance with a {@link org.hibernate.Criteria} to be * used for retrieving data. {@link #setQuery(org.hibernate.Query)} should * not also be called with a non-null value. */ protected void setCriteria(org.hibernate.Criteria criteria) { if (_query != null) { throw new IllegalStateException( "This Query already has a Query set:" + _query); } _criteria = criteria; } /** * the set of filters that is being or has been enabled for this Query. */ protected Set<String> newlyEnabledFilters = new HashSet<String>(); /** * does nothing by default, but can be overridden by subclasses to enable * particular filters. */ protected void enableFilters(Session session) { } /** * standard filter used by many subclasses which uses the * {@link Parameters#isExperimenter()} and {@link Parameters#isGroup()} * booleans to see if a filter should be turned on. If both booleans are * active, group wins. The constant {@link Parameters#OWNER_ID} or * {@link Parameters#GROUP_ID} is then used to define a filter. */ protected void ownerOrGroupFilters(Session session, String[] ownerFilters, String[] groupFilters) { if (params.isGroup()) { for (String filter : groupFilters) { if (session.getEnabledFilter(filter) != null) { newlyEnabledFilters.add(filter); } session.enableFilter(filter).setParameter(GROUP_ID, params.getGroup()); } } else if (params.isExperimenter()) { for (String filter : ownerFilters) { if (session.getEnabledFilter(filter) != null) { newlyEnabledFilters.add(filter); } session.enableFilter(filter).setParameter(OWNER_ID, params.getExperimenter()); } } } /** * turns the filters off that are listed in {@link #newlyEnabledFilters} */ protected void disableFilters(Session session) { for (String enabledFilter : newlyEnabledFilters) { session.disableFilter(enabledFilter); } } }