/*
* Copyright 2002-2016 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
*
* http://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.integration.jpa.core;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Query;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.jpa.support.JpaParameter;
import org.springframework.integration.jpa.support.PersistMode;
import org.springframework.integration.jpa.support.parametersource.BeanPropertyParameterSourceFactory;
import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory;
import org.springframework.integration.jpa.support.parametersource.ParameterSource;
import org.springframework.integration.jpa.support.parametersource.ParameterSourceFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Executes Jpa Operations that produce payload objects from the result of the provided:
*
* <ul>
* <li>entityClass</li>
* <li>JpQl Select Query</li>
* <li>Sql Native Query</li>
* <li>JpQl Named Query</li>
* <li>Sql Native Named Query</li>
* </ul>
*
* When objects are being retrieved, it also possibly to:
*
* <ul>
* <li>delete the retrieved object</li>
* </ul>
*
* If neither entityClass nor any other query is specified then the entity-class
* is "guessed" from the {@link Message} payload.
*
* @author Gunnar Hillert
* @author Amol Nayak
* @author Artem Bilan
* @since 2.2
*
*/
public class JpaExecutor implements InitializingBean, BeanFactoryAware {
private final JpaOperations jpaOperations;
private volatile List<JpaParameter> jpaParameters;
private volatile Class<?> entityClass;
private volatile String jpaQuery;
private volatile String nativeQuery;
private volatile String namedQuery;
private volatile Expression maxResultsExpression;
private volatile Expression firstResultExpression;
private volatile Expression idExpression;
private volatile PersistMode persistMode = PersistMode.MERGE;
private volatile ParameterSourceFactory parameterSourceFactory = null;
private volatile ParameterSource parameterSource;
private volatile boolean flush = false;
private volatile int flushSize = 0;
private volatile boolean clearOnFlush = false;
private volatile boolean deleteAfterPoll = false;
private volatile boolean deleteInBatch = false;
private volatile boolean expectSingleResult = false;
/**
* Indicates that whether only the payload of the passed in {@link Message}
* will be used as a source of parameters. The is 'true' by default because as a
* default a {@link BeanPropertyParameterSourceFactory} implementation is
* used for the sqlParameterSourceFactory property.
*/
private volatile Boolean usePayloadAsParameterSource = null;
private volatile BeanFactory beanFactory;
private volatile EvaluationContext evaluationContext;
/**
* Constructor taking an {@link EntityManagerFactory} from which the
* {@link EntityManager} can be obtained.
* @param entityManagerFactory Must not be null.
*/
public JpaExecutor(EntityManagerFactory entityManagerFactory) {
Assert.notNull(entityManagerFactory, "entityManagerFactory must not be null.");
DefaultJpaOperations defaultJpaOperations = new DefaultJpaOperations();
defaultJpaOperations.setEntityManagerFactory(entityManagerFactory);
defaultJpaOperations.afterPropertiesSet();
this.jpaOperations = defaultJpaOperations;
}
/**
* Constructor taking an {@link EntityManager} directly.
* @param entityManager Must not be null.
*/
public JpaExecutor(EntityManager entityManager) {
Assert.notNull(entityManager, "entityManager must not be null.");
DefaultJpaOperations defaultJpaOperations = new DefaultJpaOperations();
defaultJpaOperations.setEntityManager(entityManager);
defaultJpaOperations.afterPropertiesSet();
this.jpaOperations = defaultJpaOperations;
}
/**
* If custom behavior is required a custom implementation of {@link JpaOperations}
* can be passed in. The implementations themselves typically provide access
* to the {@link EntityManager}.
* See also {@link DefaultJpaOperations} and {@link AbstractJpaOperations}.
* @param jpaOperations Must not be null.
*/
public JpaExecutor(JpaOperations jpaOperations) {
Assert.notNull(jpaOperations, "jpaOperations must not be null.");
this.jpaOperations = jpaOperations;
}
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Verify and sets the parameters. E.g. initializes the to be used
* {@link ParameterSourceFactory}.
*/
@Override
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(this.jpaParameters)) {
if (this.parameterSourceFactory == null) {
ExpressionEvaluatingParameterSourceFactory expressionSourceFactory =
new ExpressionEvaluatingParameterSourceFactory(this.beanFactory);
expressionSourceFactory.setParameters(this.jpaParameters);
this.parameterSourceFactory = expressionSourceFactory;
}
else {
if (!(this.parameterSourceFactory instanceof ExpressionEvaluatingParameterSourceFactory)) {
throw new IllegalStateException("You are providing 'JpaParameters'. "
+ "Was expecting the the provided jpaParameterSourceFactory "
+ "to be an instance of 'ExpressionEvaluatingJpaParameterSourceFactory', "
+ "however the provided one is of type '" + this.parameterSourceFactory.getClass().getName() + "'");
}
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = false;
}
}
else {
if (this.parameterSourceFactory == null) {
this.parameterSourceFactory = new BeanPropertyParameterSourceFactory();
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = true;
}
}
if (this.flushSize > 0) {
this.flush = true;
}
else if (this.flush) {
this.flushSize = 1;
}
if (this.evaluationContext == null) {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
}
/**
* Execute the actual Jpa Operation. Call this method, if you need access to
* process return values. This methods return a Map that contains either
* the number of affected entities or the affected entity itself.
*<p>Keep in mind that the number of entities effected by the operation may
* not necessarily correlate with the number of rows effected in the database.
* @param message The message.
* @return Either the number of affected entities when using a JPQL query.
* When using a merge/persist the updated/inserted itself is returned.
*/
public Object executeOutboundJpaOperation(final Message<?> message) {
final Object result;
ParameterSource parameterSource = null;
if (this.jpaQuery != null || this.nativeQuery != null || this.namedQuery != null) {
parameterSource = this.determineParameterSource(message);
}
if (this.jpaQuery != null) {
result = this.jpaOperations.executeUpdate(this.jpaQuery, parameterSource);
}
else if (this.nativeQuery != null) {
result = this.jpaOperations.executeUpdateWithNativeQuery(this.nativeQuery, parameterSource);
}
else if (this.namedQuery != null) {
result = this.jpaOperations.executeUpdateWithNamedQuery(this.namedQuery, parameterSource);
}
else {
if (PersistMode.PERSIST.equals(this.persistMode)) {
this.jpaOperations.persist(message.getPayload(), this.flushSize, this.clearOnFlush);
result = message.getPayload();
}
else if (PersistMode.MERGE.equals(this.persistMode)) {
result = this.jpaOperations.merge(message.getPayload(), this.flushSize, this.clearOnFlush);
}
else if (PersistMode.DELETE.equals(this.persistMode)) {
this.jpaOperations.delete(message.getPayload());
if (this.flush) {
this.jpaOperations.flush();
}
result = message.getPayload();
}
else {
throw new IllegalStateException(String.format("Unsupported PersistMode: '%s'", this.persistMode.name()));
}
}
return result;
}
/**
* Execute a (typically retrieving) JPA operation. The <i>requestMessage</i>
* can be used to provide additional query parameters using
* {@link JpaExecutor#parameterSourceFactory}. If the
* <i>requestMessage</i> parameter is null then
* {@link JpaExecutor#parameterSource} is being used for providing query parameters.
* @param requestMessage May be null.
* @return The payload object, which may be null.
*/
@SuppressWarnings("unchecked")
public Object poll(final Message<?> requestMessage) {
final Object payload;
if (this.idExpression != null) {
Object id = this.idExpression.getValue(this.evaluationContext, requestMessage);
Class<?> entityClass = this.entityClass;
if (entityClass == null) {
entityClass = requestMessage.getPayload().getClass();
}
payload = this.jpaOperations.find(entityClass, id);
}
else {
final List<?> result;
int maxNumberOfResults = this.evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression);
if (requestMessage == null) {
result = this.doPoll(this.parameterSource, 0, maxNumberOfResults);
}
else {
int firstResult = 0;
if (this.firstResultExpression != null) {
firstResult = this.getFirstResult(requestMessage);
}
ParameterSource parameterSource = this.determineParameterSource(requestMessage);
result = this.doPoll(parameterSource, firstResult, maxNumberOfResults);
}
if (result.isEmpty()) {
payload = null;
}
else {
if (this.expectSingleResult) {
if (result.size() == 1) {
payload = result.iterator().next();
}
else {
throw new MessagingException(requestMessage,
"The Jpa operation returned more than 1 result object but expectSingleResult was 'true'.");
}
}
else {
payload = result;
}
}
}
if (payload != null && this.deleteAfterPoll) {
if (payload instanceof Iterable) {
if (this.deleteInBatch) {
this.jpaOperations.deleteInBatch((Iterable<Object>) payload);
}
else {
for (Object entity : (Iterable<?>) payload) {
this.jpaOperations.delete(entity);
}
}
}
else {
this.jpaOperations.delete(payload);
}
if (this.flush) {
this.jpaOperations.flush();
}
}
return payload;
}
private int getFirstResult(final Message<?> requestMessage) {
return this.evaluateExpressionForNumericResult(requestMessage, this.firstResultExpression);
}
private int evaluateExpressionForNumericResult(final Message<?> requestMessage, Expression expression) {
int evaluatedResult = 0;
if (expression != null) {
Object evaluationResult = expression.getValue(this.evaluationContext, requestMessage);
if (evaluationResult != null) {
if (evaluationResult instanceof Number) {
evaluatedResult = ((Number) evaluationResult).intValue();
}
else if (evaluationResult instanceof String) {
try {
evaluatedResult = Integer.parseInt((String) evaluationResult);
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Value " + evaluationResult + " passed as cannot be " +
"parsed to a number, expected to be numeric");
}
}
else {
throw new IllegalArgumentException("Expected the value to be a Number" +
" got " + evaluationResult.getClass().getName());
}
}
}
return evaluatedResult;
}
private ParameterSource determineParameterSource(final Message<?> requestMessage) {
ParameterSource parameterSource;
if (this.usePayloadAsParameterSource) {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage.getPayload());
}
else {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage);
}
return parameterSource;
}
/**
* Execute the JPA operation. Delegates to {@link JpaExecutor#poll(Message)}.
* @return The object or null.
*/
public Object poll() {
return this.poll(null);
}
protected List<?> doPoll(ParameterSource jpaQLParameterSource, int firstResult, int maxNumberOfResults) {
List<?> payload = null;
if (this.jpaQuery != null) {
payload = this.jpaOperations.getResultListForQuery(this.jpaQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.nativeQuery != null) {
payload = this.jpaOperations.getResultListForNativeQuery(this.nativeQuery, this.entityClass, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.namedQuery != null) {
payload = this.jpaOperations.getResultListForNamedQuery(this.namedQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.entityClass != null) {
payload = this.jpaOperations.getResultListForClass(this.entityClass, firstResult, maxNumberOfResults);
}
else {
throw new IllegalStateException("For the polling operation, one of "
+ "the following properties must be specified: "
+ "query, namedQuery or entityClass.");
}
return payload;
}
/**
* Sets the class type which is being used for retrieving entities from the
* database.
* @param entityClass Must not be null.
*/
public void setEntityClass(Class<?> entityClass) {
Assert.notNull(entityClass, "entityClass must not be null.");
this.entityClass = entityClass;
}
/**
* @param jpaQuery The provided JPA query must neither be null nor empty.
*/
public void setJpaQuery(String jpaQuery) {
Assert.isTrue(this.nativeQuery == null && this.namedQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.hasText(jpaQuery, "jpaQuery must neither be null nor empty.");
this.jpaQuery = jpaQuery;
}
/**
* You can also use native Sql queries to poll data from the database. If set
* this property will allow you to use native SQL. Optionally you can also set
* the entityClass property at the same time. If specified the entityClass will
* be used as the result class for the native query.
* @param nativeQuery The provided SQL query must neither be null nor empty.
*/
public void setNativeQuery(String nativeQuery) {
Assert.isTrue(this.namedQuery == null && this.jpaQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.hasText(nativeQuery, "nativeQuery must neither be null nor empty.");
this.nativeQuery = nativeQuery;
}
/**
* A named query can either refer to a named JPQL based query or a native SQL
* query.
* @param namedQuery Must neither be null nor empty
*/
public void setNamedQuery(String namedQuery) {
Assert.isTrue(this.jpaQuery == null && this.nativeQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.hasText(namedQuery, "namedQuery must neither be null nor empty.");
this.namedQuery = namedQuery;
}
public void setPersistMode(PersistMode persistMode) {
this.persistMode = persistMode;
}
public void setJpaParameters(List<JpaParameter> jpaParameters) {
this.jpaParameters = jpaParameters;
}
public void setUsePayloadAsParameterSource(Boolean usePayloadAsParameterSource) {
this.usePayloadAsParameterSource = usePayloadAsParameterSource;
}
/**
* If set to {@code true} the {@link javax.persistence.EntityManager#flush()} will be called
* after persistence operation.
* Has the same effect, if the {@link #flushSize} is specified to {@code 1}.
* For convenience in cases when the provided entity to persist is not an instance of {@link Iterable}.
* @param flush defaults to 'false'.
*/
public void setFlush(boolean flush) {
this.flush = flush;
}
/**
* If the provided value is greater than {@code 0}, then {@link javax.persistence.EntityManager#flush()}
* will be called after persistence operations as well as within batch operations.
* This property has precedence over the {@link #flush}, if it is specified to a value greater than {@code 0}.
* If the entity to persist is not an instance of {@link Iterable} and this property is greater than {@code 0},
* then the entity will be flushed as if the {@link #flush} attribute was set to {@code true}.
* @param flushSize defaults to '0'.
*/
public void setFlushSize(int flushSize) {
Assert.state(flushSize >= 0, "'flushSize' cannot be less than '0'.");
this.flushSize = flushSize;
}
/**
* If set to {@code true} the {@link javax.persistence.EntityManager#clear()} will be called,
* and only if the {@link javax.persistence.EntityManager#flush()} was called after performing persistence operations.
* @param clearOnFlush defaults to 'false'.
* @see #setFlush(boolean)
* @see #setFlushSize(int)
*/
public void setClearOnFlush(boolean clearOnFlush) {
this.clearOnFlush = clearOnFlush;
}
/**
* If not set, this property defaults to <code>false</code>, which means that
* deletion occurs on a per object basis if a collection of entities is being
* deleted.
*<p>If set to 'true' the elements of the payload are deleted as a batch
* operation. Be aware that this exhibits issues in regards to cascaded deletes.
*<p>The specification 'JSR 317: Java Persistence API, Version 2.0' does not
* support cascaded deletes in batch operations. The specification states in
* chapter 4.10:
*<p>"A delete operation only applies to entities of the specified class and
* its subclasses. It does not cascade to related entities."
* @param deleteInBatch Defaults to 'false' if not set.
*/
public void setDeleteInBatch(boolean deleteInBatch) {
this.deleteInBatch = deleteInBatch;
}
/**
* If set to 'true', the retrieved objects are deleted from the database upon
* being polled. May not work in all situations, e.g. for Native SQL Queries.
* @param deleteAfterPoll Defaults to 'false'.
*/
public void setDeleteAfterPoll(boolean deleteAfterPoll) {
this.deleteAfterPoll = deleteAfterPoll;
}
/**
* @param parameterSourceFactory Must not be null
*/
public void setParameterSourceFactory(ParameterSourceFactory parameterSourceFactory) {
Assert.notNull(parameterSourceFactory, "parameterSourceFactory must not be null.");
this.parameterSourceFactory = parameterSourceFactory;
}
/**
* Specify the {@link ParameterSource} that would be used to provide
* additional parameters.
* @param parameterSource Must not be null.
*/
public void setParameterSource(ParameterSource parameterSource) {
Assert.notNull(parameterSource, "parameterSource must not be null.");
this.parameterSource = parameterSource;
}
/**
*
* This parameter indicates that only one result object shall be returned as
* a result from the executed JPA operation. If set to <code>true</code> and
* the result list from the JPA operations contains only 1 element, then that
* 1 element is extracted and returned as payload.
* <p>If the result map contains more than 1 element and
* {@link JpaExecutor#expectSingleResult} is <code>true</code>, then a
* {@link MessagingException} is thrown.
* <p>If set to <code>false</code>, the complete result list is returned as the
* payload.
* @param expectSingleResult true if a single object is expected.
*
*/
public void setExpectSingleResult(boolean expectSingleResult) {
this.expectSingleResult = expectSingleResult;
}
/**
* Set the expression that will be evaluated to get the first result in the query executed.
* If a null expression is set, all the results in the result set will be retrieved
* @param firstResultExpression The first result expression.
* @see Query#setFirstResult(int)
*/
public void setFirstResultExpression(Expression firstResultExpression) {
this.firstResultExpression = firstResultExpression;
}
/**
* Set the expression that will be evaluated to get the {@code primaryKey} for
* {@link javax.persistence.EntityManager#find(Class, Object)}
* @param idExpression the SpEL expression for entity {@code primaryKey}.
* @since 4.0
*/
public void setIdExpression(Expression idExpression) {
this.idExpression = idExpression;
}
/**
* Set the expression for maximum number of results expression. It has be a non null value
* Not setting one will default to the behavior of fetching all the records
* @param maxResultsExpression The maximum results expression.
*/
public void setMaxResultsExpression(Expression maxResultsExpression) {
Assert.notNull(maxResultsExpression, "maxResultsExpression cannot be null");
this.maxResultsExpression = maxResultsExpression;
}
/**
* Set the max number of results to retrieve from the database. Defaults to
* 0, which means that all possible objects shall be retrieved.
* @param maxNumberOfResults Must not be negative.
* @see Query#setMaxResults(int)
*/
public void setMaxNumberOfResults(int maxNumberOfResults) {
this.setMaxResultsExpression(new LiteralExpression("" + maxNumberOfResults));
}
}