/** * Copyright (c) 2009 - 2016 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package org.candlepin.model; import org.candlepin.util.ElementTransformer; import com.google.inject.persist.Transactional; import org.hibernate.CacheMode; import org.hibernate.Criteria; import org.hibernate.LockMode; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.Session; import org.hibernate.criterion.DetachedCriteria; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projection; import org.hibernate.criterion.Projections; import org.hibernate.internal.CriteriaImpl; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.loader.criteria.CriteriaQueryTranslator; import org.hibernate.type.Type; import java.lang.reflect.Field; import java.util.Collection; import java.util.Collections; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.HashMap; import javax.persistence.LockModeType; /** * The DetachedCandlepinQuery class represents a detached criteria and provides fluent-style methodsfor * configuring how the criteria is to be executed and how the result should be processed. * * @param <T> * The entity type to be returned by this criteria's result output methods */ public class DetachedCandlepinQuery<T> implements CandlepinQuery<T> { protected Session session; protected DetachedCriteria criteria; protected CriteriaImpl initialState; protected int offset; protected int limit; protected LockMode lockMode; /** * Creates a new DetachedCandlepinQuery instance using the specified criteria and session. * * @param criteria * The detached criteria to execute * * @param session * The session to use to execute the given criteria * * @throws IllegalArgumentException * if either criteria or session are null */ public DetachedCandlepinQuery(Session session, DetachedCriteria criteria) { if (session == null) { throw new IllegalArgumentException("session is null"); } if (criteria == null) { throw new IllegalArgumentException("criteria is null"); } this.session = session; this.criteria = criteria; // Make a copy of the initial state. We'll restore this every time we generate an // executable criteria to simulate a copy and, hopefully, not carry state between calls this.initialState = new CriteriaImpl(null, null, null); this.copyFields((CriteriaImpl) criteria.getExecutableCriteria(session), this.initialState, false); this.offset = -1; this.limit = -1; this.lockMode = null; } /** * Worker method which copies the state from one criteria to another. * * @param source * The source criteria from which the state should be copied * * @param dest * The destination criteria to receive the copied state * * @param copyCollections * True if the collections (maps, lists, etc.) should be copied instead of referenced */ private void copyFields(CriteriaImpl source, CriteriaImpl dest, boolean copyCollections) { try { // Impl note: // We currently only perform shallow copies since most things are either immutable, or // are only modifiable by accessing the criteria object directly. As more functionality // is added to the CandlepinQuery interface, we'll need to evaluate whether or not more // explicit state copying is needed here. for (Field field : CriteriaImpl.class.getDeclaredFields()) { boolean accessible = field.isAccessible(); field.setAccessible(true); if (copyCollections && Collection.class.isAssignableFrom(field.getType())) { field.set(dest, new ArrayList((Collection) field.get(source))); } else if (copyCollections && Map.class.isAssignableFrom(field.getType())) { field.set(dest, new HashMap((Map) field.get(source))); } else { field.set(dest, field.get(source)); } field.setAccessible(accessible); } } catch (IllegalAccessException e) { // This shouldn't happen throw new RuntimeException("Illegal access exception", e); } } /** * Retreives an executable criteria and configures it to be ready to run the criteria with the * configuration set by this criteria instance. * * @return * a fully configured, executable criteria */ protected Criteria getExecutableCriteria() { // Impl/sadness note: // As of Hibernate 5.0, this does not actually result in a new criteria instance -- it just // returns its internal CriteriaImpl instance. Changes we make will be reflected between // calls. CriteriaImpl executable = (CriteriaImpl) this.criteria.getExecutableCriteria(this.session); // Restore our initial state this.copyFields(this.initialState, executable, true); // Set the session again since we just clobbered it. executable.setSession((SessionImplementor) this.session); if (this.offset > -1) { executable.setFirstResult(this.offset); } if (this.limit > -1) { executable.setMaxResults(this.limit); } if (this.lockMode != null) { executable.setLockMode(this.lockMode); } // TODO: Add read-only when we have a requirement to do so. return executable; } /** * {@inheritDoc} */ @Override public CandlepinQuery<T> useSession(Session session) { if (session == null) { throw new IllegalArgumentException("session is null"); } this.session = session; return this; } /** * {@inheritDoc} */ @Override public CandlepinQuery<T> setFirstResult(int offset) { this.offset = offset; return this; } /** * {@inheritDoc} */ @Override public CandlepinQuery<T> setMaxResults(int limit) { this.limit = limit; return this; } /** * {@inheritDoc} */ @Override public CandlepinQuery<T> addOrder(Order order) { if (order == null) { throw new IllegalArgumentException("order is null"); } this.criteria.addOrder(order); return this; } /** * {@inheritDoc} */ @Override public CandlepinQuery<T> setLockMode(LockModeType lockMode) { // Translate the given lock mode to a Hibernate lock mode if (lockMode != null) { this.lockMode = LockMode.valueOf(lockMode.name()); } else { this.lockMode = null; } return this; } /** * {@inheritDoc} */ @Override public <O> CandlepinQuery<O> transform(ElementTransformer<T, O> transformer) { return new TransformedCandlepinQuery(this, transformer); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public List<T> list() { Criteria executable = this.getExecutableCriteria(); List<T> list = (List<T>) executable.list(); return list != null ? list : Collections.<T>emptyList(); } /** * {@inheritDoc} */ @Override public int forEach(ResultProcessor<T> processor) { return this.forEach(0, false, processor); } /** * {@inheritDoc} */ @Override public int forEach(int column, ResultProcessor<T> processor) { return this.forEach(column, false, processor); } /** * {@inheritDoc} */ @Override @Transactional @SuppressWarnings("unchecked") public int forEach(int column, boolean evict, ResultProcessor<T> processor) { if (processor == null) { throw new IllegalArgumentException("processor is null"); } Criteria executable = this.getExecutableCriteria(); // We always override the cache mode here to ensure we don't evict things that may be in // cache from another request. if (evict) { executable.setCacheMode(CacheMode.GET); } ScrollableResults cursor = executable.scroll(ScrollMode.FORWARD_ONLY); int count = 0; try { boolean cont = true; if (evict) { while (cont && cursor.next()) { T result = (T) cursor.get(column); cont = processor.process(result); this.session.evict(result); ++count; } } else { while (cont && cursor.next()) { cont = processor.process((T) cursor.get(column)); ++count; } } } finally { cursor.close(); } return count; } /** * {@inheritDoc} */ @Override @Transactional public int forEachRow(ResultProcessor<Object[]> processor) { if (processor == null) { throw new IllegalArgumentException("processor is null"); } Criteria executable = this.getExecutableCriteria(); ScrollableResults cursor = executable.scroll(ScrollMode.FORWARD_ONLY); int count = 0; try { boolean cont = true; while (cont && cursor.next()) { cont = processor.process(cursor.get()); ++count; } } finally { cursor.close(); } return count; } /** * {@inheritDoc} */ @Override public ResultIterator<T> iterate() { return this.iterate(0, false); } /** * {@inheritDoc} */ @Override public ResultIterator<T> iterator() { return this.iterate(0, false); } /** * {@inheritDoc} */ @Override public ResultIterator<T> iterate(int column) { return this.iterate(column, false); } /** * {@inheritDoc} */ @Override public ResultIterator<T> iterate(int column, boolean evict) { Criteria executable = this.getExecutableCriteria(); // We always override the cache mode here to ensure we don't evict things that may be in // cache from another request. if (evict) { executable.setCacheMode(CacheMode.GET); } ScrollableResults cursor = executable.scroll(ScrollMode.FORWARD_ONLY); return new ColumnarResultIterator<T>(this.session, cursor, column, evict); } /** * {@inheritDoc} */ @Override public ResultIterator<Object[]> iterateByRow() { Criteria executable = this.getExecutableCriteria(); ScrollableResults cursor = executable.scroll(ScrollMode.FORWARD_ONLY); return new RowResultIterator(cursor); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public T uniqueResult() { Criteria executable = this.getExecutableCriteria(); return (T) executable.uniqueResult(); } /** * {@inheritDoc} */ @Override @SuppressWarnings({"unchecked", "checkstyle:indentation"}) public int getRowCount() { CriteriaImpl executable = (CriteriaImpl) this.getExecutableCriteria(); // Impl note: // We're using the projection method here over using a cursor to scroll the results due to // limitations on various connectors' cursor implementations. Some don't properly support // fast-forwarding/jumping (Oracle) and others fake the whole thing by running the query // and pretending to scroll (MySQL). Until these are addressed, the hack below is going to // be far more performant and significantly safer (which makes me sad). // Remove any ordering that may be applied (since we almost certainly won't have the field // available anymore) for (Iterator iterator = executable.iterateOrderings(); iterator.hasNext();) { iterator.next(); iterator.remove(); } Projection projection = executable.getProjection(); if (projection != null && projection.isGrouped()) { // We have a projection that alters the grouping of the query. We need to rebuild the // projection such that it gets our row count and properly applies the group by // statement. // The logic for this block is largely derived from this Stack Overflow posting: // http://stackoverflow.com/ // questions/32498229/hibernate-row-count-on-criteria-with-already-set-projection // // A safer alternative may be to generate a query that uses the given criteria as a // subquery (SELECT count(*) FROM (<criteria SQL>)), but is probably less performant // than this hack. CriteriaQueryTranslator translator = new CriteriaQueryTranslator( (SessionFactoryImplementor) this.session.getSessionFactory(), executable, executable.getEntityOrClassName(), CriteriaQueryTranslator.ROOT_SQL_ALIAS ); executable.setProjection(Projections.projectionList() .add(Projections.rowCount()) .add(Projections.sqlGroupProjection( "count(count(1))", translator.getGroupBy(), new String[] {}, new Type[] {} ))); } else { executable.setProjection(Projections.rowCount()); } Long count = (Long) executable.uniqueResult(); return count != null ? count.intValue() : 0; } }