package org.springframework.data.jpa.repository.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.QueryHint;
import javax.persistence.Tuple;
import javax.persistence.TupleElement;
import javax.persistence.TypedQuery;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.PagedExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ProcedureExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.SingleEntityExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.SlicedExecution;
import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public abstract class AbstractJpaQuery implements RepositoryQuery {
private final JpaQueryMethod method;
private final EntityManager em;
private final JpaMetamodel metamodel;
private final PersistenceProvider provider;
private final Lazy<JpaQueryExecution> execution;
final Lazy<ParameterBinder> parameterBinder = new Lazy<>(this::createBinder);
public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
Assert.notNull(method, "JpaQueryMethod must not be null!");
Assert.notNull(em, "EntityManager must not be null!");
this.method = method;
this.em = em;
this.metamodel = JpaMetamodel.of(em.getMetamodel());
this.provider = PersistenceProvider.fromEntityManager(em);
this.execution = Lazy.of(() -> {
if (method.isStreamQuery()) {
return new StreamExecution();
} else if (method.isProcedureQuery()) {
return new ProcedureExecution();
} else if (method.isCollectionQuery()) {
return new CollectionExecution();
} else if (method.isSliceQuery()) {
return new SlicedExecution();
} else if (method.isPageQuery()) {
return new PagedExecution();
} else if (method.isModifyingQuery()) {
return null;
} else {
return new SingleEntityExecution();
}
});
}
@Override
public JpaQueryMethod getQueryMethod() {
return method;
}
protected EntityManager getEntityManager() {
return em;
}
protected JpaMetamodel getMetamodel() {
return metamodel;
}
@Nullable
@Override
public Object execute(Object[] parameters) {
return doExecute(getExecution(), parameters);
}
@Nullable
private Object doExecute(JpaQueryExecution execution, Object[] values) {
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(method.getParameters(), values);
Object result = execution.execute(this, accessor);
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
}
protected JpaQueryExecution getExecution() {
JpaQueryExecution execution = this.execution.getNullable();
if (execution != null) {
return execution;
}
if (method.isModifyingQuery()) {
return new ModifyingExecution(method, em);
} else {
return new SingleEntityExecution();
}
}
protected <T extends Query> T applyHints(T query, JpaQueryMethod method) {
List<QueryHint> hints = method.getHints();
if (!hints.isEmpty()) {
for (QueryHint hint : hints) {
applyQueryHint(query, hint);
}
}
return query;
}
protected <T extends Query> void applyQueryHint(T query, QueryHint hint) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(hint, "QueryHint must not be null!");
query.setHint(hint.name(), hint.value());
}
private Query applyLockMode(Query query, JpaQueryMethod method) {
LockModeType lockModeType = method.getLockModeType();
return lockModeType == null ? query : query.setLockMode(lockModeType);
}
protected ParameterBinder createBinder() {
return ParameterBinderFactory.createBinder(getQueryMethod().getParameters());
}
protected Query createQuery(JpaParametersParameterAccessor parameters) {
return applyLockMode(applyEntityGraphConfiguration(applyHints(doCreateQuery(parameters), method), method), method);
}
private Query applyEntityGraphConfiguration(Query query, JpaQueryMethod method) {
JpaEntityGraph entityGraph = method.getEntityGraph();
if (entityGraph != null) {
Map<String, Object> hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(),
getQueryMethod().getEntityInformation().getJavaType());
for (Map.Entry<String, Object> hint : hints.entrySet()) {
query.setHint(hint.getKey(), hint.getValue());
}
}
return query;
}
protected Query createCountQuery(JpaParametersParameterAccessor values) {
Query countQuery = doCreateCountQuery(values);
return method.applyHintsToCountQuery() ? applyHints(countQuery, method) : countQuery;
}
@Nullable
protected Class<?> getTypeToRead(ReturnedType returnedType) {
if (PersistenceProvider.ECLIPSELINK.equals(provider)) {
return null;
}
return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType())
? Tuple.class
: null;
}
protected abstract Query doCreateQuery(JpaParametersParameterAccessor accessor);
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);
static class TupleConverter implements Converter<Object, Object> {
private final ReturnedType type;
public TupleConverter(ReturnedType type) {
Assert.notNull(type, "Returned type must not be null!");
this.type = type;
}
@Override
public Object convert(Object source) {
if (!(source instanceof Tuple)) {
return source;
}
Tuple tuple = (Tuple) source;
List<TupleElement<?>> elements = tuple.getElements();
if (elements.size() == 1) {
Object value = tuple.get(elements.get(0));
if (type.isInstance(value) || value == null) {
return value;
}
}
return new TupleBackedMap(tuple);
}
private static class TupleBackedMap implements Map<String, Object> {
private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified.";
private final Tuple tuple;
TupleBackedMap(Tuple tuple) {
this.tuple = tuple;
}
@Override
public int size() {
return tuple.getElements().size();
}
@Override
public boolean isEmpty() {
return tuple.getElements().isEmpty();
}
@Override
public boolean containsKey(Object key) {
try {
tuple.get((String) key);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
@Override
public boolean containsValue(Object value) {
return Arrays.asList(tuple.toArray()).contains(value);
}
@Override
@Nullable
public Object get(Object key) {
if (!(key instanceof String)) {
return null;
}
try {
return tuple.get((String) key);
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public Object put(String key, Object value) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Object remove(Object key) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void putAll(Map<? extends String, ?> m) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void clear() {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Set<String> keySet() {
return tuple.getElements().stream()
.map(TupleElement::getAlias)
.collect(Collectors.toSet());
}
@Override
public Collection<Object> values() {
return Arrays.asList(tuple.toArray());
}
@Override
public Set<Entry<String, Object>> entrySet() {
return tuple.getElements().stream()
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e)))
.collect(Collectors.toSet());
}
}
}
}