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 super C, ? super S> 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 super S> 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 super S, ? extends S2> 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 super X, Boolean> 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 super S, ? extends Expression> 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