All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.querystream.jpa.QueryStreamImpl Maven / Gradle / Ivy


/*
 * Copyright (C) 2018 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.querystream.jpa;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.criteria.CommonAbstractCriteria;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Selection;
import javax.persistence.metamodel.SingularAttribute;

import org.dellroad.querystream.jpa.querytype.QueryType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Builder for JPA criteria queries, based on configuration through a {@link java.util.stream.Stream}-like API.
 *
 * @param  stream item type
 * @param  criteria type for stream item
 * @param  configured criteria API query type
 * @param  final criteria API query type
 * @param  JPA query type
 * @param  corresponding {@link QueryType} subtype
 * @param  subclass type
 */
abstract class QueryStreamImpl,
  C extends CommonAbstractCriteria,
  C2 extends C,
  Q extends Query,
  QT extends QueryType> implements QueryStream {

    private static final ThreadLocal THREAD_QUERY_INFO = new ThreadLocal<>();
    private static final ThreadLocal THREAD_CURRENT_QUERY = new ThreadLocal<>();

    private static final String LOAD_GRAPH_HINT = "javax.persistence.loadgraph";
    private static final String FETCH_GRAPH_HINT = "javax.persistence.fetchgraph";

    protected final Logger log = LoggerFactory.getLogger(this.getClass());

    final EntityManager entityManager;
    final QT queryType;
    final QueryConfigurer configurer;
    final QueryInfo queryInfo;

// Constructors

    QueryStreamImpl(EntityManager entityManager, QT queryType, QueryConfigurer configurer, QueryInfo queryInfo) {
        if (entityManager == null)
            throw new IllegalArgumentException("null entityManager");
        if (queryType == null)
            throw new IllegalArgumentException("null queryType");
        if (configurer == null)
            throw new IllegalArgumentException("null configurer");
        if (queryInfo == null)
            throw new IllegalArgumentException("null queryInfo");
        this.entityManager = entityManager;
        this.queryType = queryType;
        this.configurer = configurer;
        this.queryInfo = queryInfo;
    }

// QueryType

    @Override
    public QT getQueryType() {
        return this.queryType;
    }

// Extenders

    /**
     * Create an instance equivalent to this one where the given operation is applied to the query.
     *
     * @param modifier additional query modification
     */
    QueryStream modQuery(BiConsumer modifier) {
        if (modifier == null)
            throw new IllegalArgumentException("null modifier");
        return this.create(this.entityManager, this.queryType, (builder, query) -> {
            final S selection = this.configure(builder, query);
            modifier.accept(query, selection);
            return selection;
        }, this.queryInfo);
    }

    /**
     * Create an instance equivalent to this one but with a new {@link QueryConfigurer}.
     *
     * @param configurer new query configuration
     */
    QueryStream withConfig(QueryConfigurer configurer) {
        if (configurer == null)
            throw new IllegalArgumentException("null configurer");
        return this.create(this.entityManager, this.queryType, configurer, this.queryInfo);
    }

    /**
     * Create an instance equivalent to this one but with a new {@link QueryInfo}.
     *
     * @param queryInfo new query info
     */
    QueryStream withQueryInfo(QueryInfo queryInfo) {
        return this.create(this.entityManager, this.queryType, this.configurer, queryInfo);
    }

// Subclass required methods

    /**
     * Create a new instance with the same type as this instance.
     */
    abstract QueryStream create(EntityManager entityManager,
      QT queryType, QueryConfigurer configurer, QueryInfo queryInfo);

    /**
     * Apply selection criteria, if appropriate.
     *
     * @param query query to select from
     * @param selection what to select
     * @return modified query
     */
    abstract C2 select(C2 query, S selection);

// QueryConfigurer

    @Override
    public S configure(CriteriaBuilder builder, C query) {
        return this.configurer.configure(builder, query);
    }

// Queryification

    CriteriaBuilder builder() {
        return this.entityManager.getCriteriaBuilder();
    }

    @Override
    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    @Override
    public C2 toCriteriaQuery() {
        final CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
        final C2 query = this.queryType.createCriteriaQuery(builder);
        return QueryStreamImpl.withCurrentQuery(builder, query, () -> this.select(query, this.configure(builder, query)));
    }

    @Override
    public Q toQuery() {

        // Create a merged QueryInfo object into which we can merge this and all subquery QueryInfo's
        final QueryInfo previous = THREAD_QUERY_INFO.get();
        THREAD_QUERY_INFO.set(this.queryInfo);
        final Q query;
        try {
            query = this.queryType.createQuery(this.entityManager, this.toCriteriaQuery());
            THREAD_QUERY_INFO.get().applyTo(query);                 // apply merged QueryInfo configuration to the query
            return query;
        } finally {
            THREAD_QUERY_INFO.set(previous);
        }
    }

    @Override
    public int getFirstResult() {
        return this.queryInfo.getFirstResult();
    }

    @Override
    public int getMaxResults() {
        return this.queryInfo.getMaxResults();
    }

    @Override
    public FlushModeType getFlushMode() {
        return this.queryInfo.getFlushMode();
    }

    @Override
    public QueryStream withFlushMode(FlushModeType flushMode) {
        return this.withQueryInfo(this.queryInfo.withFlushMode(flushMode));
    }

    @Override
    public LockModeType getLockMode() {
        return this.queryInfo.getLockMode();
    }

    @Override
    public QueryStream withLockMode(LockModeType lockMode) {
        return this.withQueryInfo(this.queryInfo.withLockMode(lockMode));
    }

    @Override
    public Map getHints() {
        return this.queryInfo.getHints();
    }

    @Override
    public QueryStream withHint(String name, Object value) {
        return this.withQueryInfo(this.queryInfo.withHint(name, value));
    }

    @Override
    public QueryStream withHints(Map hints) {
        return this.withQueryInfo(this.queryInfo.withHints(hints));
    }

    @Override
    public Set> getParams() {
        return this.queryInfo.getParams();
    }

    @Override
    public  QueryStream withParam(Parameter parameter, T value) {
        return this.withQueryInfo(this.queryInfo.withParam(parameter, value));
    }

    @Override
    public QueryStream withParam(Parameter parameter, Date value, TemporalType temporalType) {
        return this.withQueryInfo(this.queryInfo.withParam(parameter, value, temporalType));
    }

    @Override
    public QueryStream withParam(Parameter parameter, Calendar value, TemporalType temporalType) {
        return this.withQueryInfo(this.queryInfo.withParam(parameter, value, temporalType));
    }

    @Override
    public QueryStream withParams(Set> params) {
        return this.withQueryInfo(this.queryInfo.withParams(params));
    }

    @Override
    public QueryStream withLoadGraph(String name) {
        return this.withEntityGraph(LOAD_GRAPH_HINT, name);
    }

    @Override
    public QueryStream withFetchGraph(String name) {
        return this.withEntityGraph(FETCH_GRAPH_HINT, name);
    }

    private QueryStream withEntityGraph(String hintName, String graphName) {
        if (graphName == null)
            throw new IllegalArgumentException("null entity graph name");
        return this.withHint(hintName, this.entityManager.getEntityGraph(graphName));
    }

// Refs

    @Override
    public QueryStream bind(Ref ref) {
        return this.bind(ref, s -> s);
    }

    @Override
    public QueryStream peek(Consumer peeker) {
        if (peeker == null)
            throw new IllegalArgumentException("null peeker");
        return this.withConfig((builder, query) -> {
            final S selection = this.configure(builder, query);
            peeker.accept(selection);
            return selection;
        });
    }

    @Override
    public > QueryStream bind(
      Ref ref, Function refFunction) {
        if (ref == null)
            throw new IllegalArgumentException("null ref");
        if (refFunction == null)
            throw new IllegalArgumentException("null refFunction");
        if (ref.isBound())
            throw new IllegalArgumentException("reference is already bound");
        return this.withConfig((builder, query) -> {
            final S selection = this.configure(builder, query);
            ref.bind(refFunction.apply(selection));
            return selection;
        });
    }

// Filtering

    @Override
    public QueryStream filter(SingularAttribute attribute) {
        if (attribute == null)
            throw new IllegalArgumentException("null attribute");
        return this.withConfig((builder, query) -> {
            final S result = this.configure(builder, query);
            this.and(builder, query, builder.isTrue(((Path)result).get(attribute))); // cast must be valid if attribute exists
            return result;
        });
    }

    @Override
    public QueryStream filter(Function> predicateBuilder) {
        if (predicateBuilder == null)
            throw new IllegalArgumentException("null predicateBuilder");
        QueryStreamImpl.checkOffsetLimit(this, "filter()");
        return this.withConfig((builder, query) -> {
            final S result = this.configure(builder, query);
            this.and(builder, query, predicateBuilder.apply(result));
            return result;
        });
    }

    void and(CriteriaBuilder builder, C query, Expression expression) {
        final Predicate oldRestriction = query.getRestriction();
        this.queryType.where(query, oldRestriction != null ? builder.and(oldRestriction, expression) : expression);
    }

// Streamy stuff

    @Override
    public QueryStream limit(int limit) {
        if (limit < 0)
            throw new IllegalArgumentException("limit < 0");
        final int newMaxResults = this.getMaxResults() != -1 ? Math.min(limit, this.getMaxResults()) : limit;
        return this.withQueryInfo(this.queryInfo.withMaxResults(newMaxResults));
    }

    @Override
    public QueryStream skip(int skip) {
        if (skip < 0)
            throw new IllegalArgumentException("skip < 0");
        final int newFirstResult = this.getFirstResult() != -1 ? this.getFirstResult() + skip : skip;
        return this.withQueryInfo(this.queryInfo.withFirstResult(newFirstResult));
    }

    // This is used to prevent other stuff that would affect the returned results being applied after skip()/limit()
    static void checkOffsetLimit(QueryStream stream, String operation) {
        if (stream.getFirstResult() != -1 || stream.getMaxResults() != -1)
            QueryStreamImpl.failJpaRestriction(operation + " must be performed prior to skip() or limit()");
    }

    static void failJpaRestriction(String restriction) {
        throw new UnsupportedOperationException("sorry, " + restriction + " because the JPA Criteria API allows certain"
          + " information to be configured only on a Query object, not on a CriteriaQuery or Subquery");
    }

// QueryInfo Merging

    // Merge the QueryInfo information from a subquery into the "global" QueryInfo we will use for the outermost query
    static void mergeQueryInfo(QueryInfo innerQueryInfo) {
        if (innerQueryInfo == null)
            throw new IllegalArgumentException("null innerQueryInfo");
        final QueryInfo outerQueryInfo = THREAD_QUERY_INFO.get();
        if (outerQueryInfo != null)
            THREAD_QUERY_INFO.set(outerQueryInfo.withMergedInfo(innerQueryInfo));
    }

// CurrentQuery

    static CurrentQuery getCurrentQuery() {
        final CurrentQuery info = THREAD_CURRENT_QUERY.get();
        if (info == null)
            throw new IllegalStateException("subquery must be created in the context of a containing query");
        return info;
    }

    static  T withCurrentQuery(CriteriaBuilder builder, CommonAbstractCriteria query, Supplier action) {
        if (action == null)
            throw new IllegalArgumentException("null action");
        final CurrentQuery prev = THREAD_CURRENT_QUERY.get();
        final CurrentQuery info = new CurrentQuery(builder, query);
        THREAD_CURRENT_QUERY.set(info);
        try {
            return action.get();
        } finally {
            THREAD_CURRENT_QUERY.set(prev);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy