org.candlepin.model.DetachedCandlepinQuery.java Source code

Java tutorial

Introduction

Here is the source code for org.candlepin.model.DetachedCandlepinQuery.java

Source

/**
 * 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<>(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;
    }
}