/*
 * Copyright 2017-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository.query;

import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;

import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.criteria.ParameterExpression;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

The interface encapsulates the setting of query parameters which might use a significant number of variations of Query.setParameter.
Author:Jens Schauder, Mark Paluch
Since:2.0
/** * The interface encapsulates the setting of query parameters which might use a significant number of variations of * {@literal Query.setParameter}. * * @author Jens Schauder * @author Mark Paluch * @since 2.0 */
interface QueryParameterSetter { void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling);
Noop implementation
/** Noop implementation */
QueryParameterSetter NOOP = (query, values, errorHandling) -> {};
QueryParameterSetter for named or indexed parameters that might have a TemporalType specified.
/** * {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified. */
class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter { private final Function<JpaParametersParameterAccessor, Object> valueExtractor; private final Parameter<?> parameter; private final @Nullable TemporalType temporalType;
Params:
  • valueExtractor – must not be null.
  • parameter – must not be null.
  • temporalType – may be null.
/** * @param valueExtractor must not be {@literal null}. * @param parameter must not be {@literal null}. * @param temporalType may be {@literal null}. */
NamedOrIndexedQueryParameterSetter(Function<JpaParametersParameterAccessor, Object> valueExtractor, Parameter<?> parameter, @Nullable TemporalType temporalType) { Assert.notNull(valueExtractor, "ValueExtractor must not be null!"); this.valueExtractor = valueExtractor; this.parameter = parameter; this.temporalType = temporalType; } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.query.QueryParameterSetter#setParameter(javax.persistence.Query, java.lang.Object[]) */ @SuppressWarnings("unchecked") @Override public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling) { Object value = valueExtractor.apply(accessor); if (temporalType != null) { // One would think we can simply use parameter to identify the parameter we want to set. // But that does not work with list valued parameters. At least Hibernate tries to bind them by name. // TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is // fixed. if (parameter instanceof ParameterExpression) { errorHandling.execute(() -> query.setParameter((Parameter<Date>) parameter, (Date) value, temporalType)); } else if (query.hasNamedParameters() && parameter.getName() != null) { errorHandling.execute(() -> query.setParameter(parameter.getName(), (Date) value, temporalType)); } else { Integer position = parameter.getPosition(); if (position != null // && (query.getParameters().size() >= parameter.getPosition() // || query.registerExcessParameters() // || errorHandling == LENIENT)) { errorHandling.execute(() -> query.setParameter(parameter.getPosition(), (Date) value, temporalType)); } } } else { if (parameter instanceof ParameterExpression) { errorHandling.execute(() -> query.setParameter((Parameter<Object>) parameter, value)); } else if (query.hasNamedParameters() && parameter.getName() != null) { errorHandling.execute(() -> query.setParameter(parameter.getName(), value)); } else { Integer position = parameter.getPosition(); if (position != null // && (query.getParameters().size() >= position // || errorHandling == LENIENT // || query.registerExcessParameters())) { errorHandling.execute(() -> query.setParameter(position, value)); } } } } } enum ErrorHandling { STRICT { @Override public void execute(Runnable block) { block.run(); } }, LENIENT { @Override public void execute(Runnable block) { try { block.run(); } catch (RuntimeException rex) { LOG.info("Silently ignoring", rex); } } }; private static final Logger LOG = LoggerFactory.getLogger(ErrorHandling.class); abstract void execute(Runnable block); }
Cache for QueryMetadata. Optimizes for small cache sizes on a best-effort basis.
/** * Cache for {@link QueryMetadata}. Optimizes for small cache sizes on a best-effort basis. */
class QueryMetadataCache { private Map<String, QueryMetadata> cache = Collections.emptyMap();
Retrieve the QueryMetadata for a given cacheKey.
Params:
  • cacheKey –
  • query –
Returns:
/** * Retrieve the {@link QueryMetadata} for a given {@code cacheKey}. * * @param cacheKey * @param query * @return */
public QueryMetadata getMetadata(String cacheKey, Query query) { QueryMetadata queryMetadata = cache.get(cacheKey); if (queryMetadata == null) { queryMetadata = new QueryMetadata(query); Map<String, QueryMetadata> cache; if (this.cache.isEmpty()) { cache = Collections.singletonMap(cacheKey, queryMetadata); } else { cache = new HashMap<>(this.cache); cache.put(cacheKey, queryMetadata); } synchronized (this) { this.cache = cache; } } return queryMetadata; } }
Metadata for a JPA Query.
/** * Metadata for a JPA {@link Query}. */
class QueryMetadata { private final boolean namedParameters; private final Set<Parameter<?>> parameters; private final boolean registerExcessParameters; QueryMetadata(Query query) { this.namedParameters = QueryUtils.hasNamedParameter(query); this.parameters = query.getParameters(); // DATAJPA-1172 // Since EclipseLink doesn't reliably report whether a query has parameters // we simply try to set the parameters and ignore possible failures. // this is relevant for native queries with SpEL expressions, where the method parameters don't have to match the // parameters in the query. // https://bugs.eclipse.org/bugs/show_bug.cgi?id=521915 this.registerExcessParameters = query.getParameters().size() == 0 && unwrapClass(query).getName().startsWith("org.eclipse"); } QueryMetadata(QueryMetadata metadata) { this.namedParameters = metadata.namedParameters; this.parameters = metadata.parameters; this.registerExcessParameters = metadata.registerExcessParameters; }
Create a BindableQuery for a Query.
Params:
  • query –
Returns:
/** * Create a {@link BindableQuery} for a {@link Query}. * * @param query * @return */
public BindableQuery withQuery(Query query) { return new BindableQuery(this, query); }
Returns:
/** * @return */
public Set<Parameter<?>> getParameters() { return parameters; }
Returns:true if the underlying query uses named parameters.
/** * @return {@literal true} if the underlying query uses named parameters. */
public boolean hasNamedParameters() { return this.namedParameters; } public boolean registerExcessParameters() { return this.registerExcessParameters; }
Returns the actual target Query instance, even if the provided query is a Proxy based on DeferredQueryInvocationHandler.
Params:
  • query – a Query instance, possibly a Proxy.
Returns:the class of the actual underlying class if it can be determined, the class of the passed in instance otherwise.
/** * Returns the actual target {@link Query} instance, even if the provided query is a {@link Proxy} based on * {@link org.springframework.orm.jpa.SharedEntityManagerCreator.DeferredQueryInvocationHandler}. * * @param query a {@link Query} instance, possibly a Proxy. * @return the class of the actual underlying class if it can be determined, the class of the passed in instance * otherwise. */
private static Class<?> unwrapClass(Query query) { Class<? extends Query> queryType = query.getClass(); try { return Proxy.isProxyClass(queryType) // ? query.unwrap(null).getClass() // : queryType; } catch (RuntimeException e) { LoggerFactory.getLogger(QueryMetadata.class).warn("Failed to unwrap actual class for Query proxy.", e); return queryType; } } }
A bindable Query.
/** * A bindable {@link Query}. */
class BindableQuery extends QueryMetadata { private final Query query; private final Query unwrapped; BindableQuery(QueryMetadata metadata, Query query) { super(metadata); this.query = query; this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; } private BindableQuery(Query query) { super(query); this.query = query; this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; } public static BindableQuery from(Query query) { return new BindableQuery(query); } public Query getQuery() { return query; } public <T> Query setParameter(Parameter<T> param, T value) { return unwrapped.setParameter(param, value); } public Query setParameter(Parameter<Date> param, Date value, TemporalType temporalType) { return unwrapped.setParameter(param, value, temporalType); } public Query setParameter(String name, Object value) { return unwrapped.setParameter(name, value); } public Query setParameter(String name, Date value, TemporalType temporalType) { return query.setParameter(name, value, temporalType); } public Query setParameter(int position, Object value) { return unwrapped.setParameter(position, value); } public Query setParameter(int position, Date value, TemporalType temporalType) { return unwrapped.setParameter(position, value, temporalType); } } }