From d981171972d26cd1bc5aa22d3b3b64fe09818e21 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 27 Nov 2018 11:00:40 -0500 Subject: [PATCH] Fix ambiguity in the `JpaExecutor` (#2640) * Fix ambiguity in the `JpaExecutor` Since we can't mutate a provided `ParameterSourceFactory` with the provided `jpaParameters`, we have to reject such a combined configuration in favor of the advice to configure params on the provided `ParameterSourceFactory` * Throw `IllegalStateException` when `ParameterSourceFactory` and `jpaParameters` are configured together on the `JpaExecutor` * Some refactoring for `JpaExecutor` to have a consistent code style * Fix tests according new logic * Document such an ambiguity configuration to avoid confusion **Cherry-pick to 5.0.x** * * Use `assertThatThrownBy()` in the `JpaExecutorTests` * Fix "No BeanFactory" warning in the `JpaExecutorTests` * * Resolve potential NPE and add `@NonNullApi` with `@Nullable` on the `poll()` --- .../integration/jpa/core/JpaExecutor.java | 583 +++++++++--------- .../integration/jpa/core/package-info.java | 1 + .../jpa/config/spring-integration-jpa-5.1.xsd | 15 +- .../jpa/core/JpaExecutorTests.java | 23 +- .../integration/jpa/dsl/JpaTests.java | 4 +- .../jpa/outbound/JpaOutboundGatewayTests.java | 62 +- src/reference/asciidoc/jpa.adoc | 15 +- 7 files changed, 363 insertions(+), 340 deletions(-) diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java index 78fc20afbc..6a2e032d90 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -36,6 +36,7 @@ import org.springframework.integration.jpa.support.parametersource.BeanPropertyP 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.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.util.Assert; @@ -59,11 +60,12 @@ import org.springframework.util.CollectionUtils; * * * If neither entityClass nor any other query is specified then the entity-class - * is "guessed" from the {@link Message} payload. + * is "guessed" from the {@link Message} payload.idExpression * * @author Gunnar Hillert * @author Amol Nayak * @author Artem Bilan + * * @since 2.2 * */ @@ -71,39 +73,39 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { private final JpaOperations jpaOperations; - private volatile List jpaParameters; + private List jpaParameters; - private volatile Class entityClass; + private Class entityClass; - private volatile String jpaQuery; + private String jpaQuery; - private volatile String nativeQuery; + private String nativeQuery; - private volatile String namedQuery; + private String namedQuery; - private volatile Expression maxResultsExpression; + private Expression maxResultsExpression; - private volatile Expression firstResultExpression; + private Expression firstResultExpression; - private volatile Expression idExpression; + private Expression idExpression; - private volatile PersistMode persistMode = PersistMode.MERGE; + private PersistMode persistMode = PersistMode.MERGE; - private volatile ParameterSourceFactory parameterSourceFactory = null; + private ParameterSourceFactory parameterSourceFactory = null; - private volatile ParameterSource parameterSource; + private ParameterSource parameterSource; - private volatile boolean flush = false; + private boolean flush = false; - private volatile int flushSize = 0; + private int flushSize = 0; - private volatile boolean clearOnFlush = false; + private boolean clearOnFlush = false; - private volatile boolean deleteAfterPoll = false; + private boolean deleteAfterPoll = false; - private volatile boolean deleteInBatch = false; + private boolean deleteInBatch = false; - private volatile boolean expectSingleResult = false; + private boolean expectSingleResult = false; /** * Indicates that whether only the payload of the passed in {@link Message} @@ -111,11 +113,11 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { * default a {@link BeanPropertyParameterSourceFactory} implementation is * used for the sqlParameterSourceFactory property. */ - private volatile Boolean usePayloadAsParameterSource = null; + private Boolean usePayloadAsParameterSource = null; - private volatile BeanFactory beanFactory; + private BeanFactory beanFactory; - private volatile EvaluationContext evaluationContext; + private EvaluationContext evaluationContext; /** * Constructor taking an {@link EntityManagerFactory} from which the @@ -161,274 +163,6 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { 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. - *

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 requestMessage - * can be used to provide additional query parameters using - * {@link JpaExecutor#parameterSourceFactory}. If the - * requestMessage 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) 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. @@ -517,7 +251,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { /** * 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. + * 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) @@ -628,4 +363,270 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { this.setMaxResultsExpression(new LiteralExpression("" + maxNumberOfResults)); } + @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 { + throw new IllegalStateException("The 'jpaParameters' and 'parameterSourceFactory' " + + "are mutually exclusive. Consider to configure parameters on the provided " + + "'parameterSourceFactory': " + this.parameterSourceFactory); + } + + 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. + *

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 { + switch (this.persistMode) { + case PERSIST: + this.jpaOperations.persist(message.getPayload(), this.flushSize, this.clearOnFlush); + result = message.getPayload(); + break; + case MERGE: + result = this.jpaOperations.merge(message.getPayload(), this.flushSize, this.clearOnFlush); + break; + case DELETE: + this.jpaOperations.delete(message.getPayload()); + if (this.flush) { + this.jpaOperations.flush(); + } + result = message.getPayload(); + break; + default: + throw new IllegalStateException("Unsupported PersistMode: " + this.persistMode.name()); + } + } + + return result; + + } + + /** + * Execute the JPA operation. Delegates to {@link JpaExecutor#poll(Message)}. + * @return The object or null. + */ + public Object poll() { + return poll(null); + } + + /** + * Execute a (typically retrieving) JPA operation. The requestMessage + * can be used to provide additional query parameters using + * {@link JpaExecutor#parameterSourceFactory}. If the + * requestMessage 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. + */ + public Object poll(@Nullable final Message requestMessage) { + final Object payload; + + if (this.idExpression != null) { + Object id = this.idExpression.getValue(this.evaluationContext, requestMessage); // NOSONAR It can be null + Assert.state(id != null, "The 'idExpression' cannot evaluate to null."); + Class entityClass = this.entityClass; + if (entityClass == null && requestMessage != null) { + entityClass = requestMessage.getPayload().getClass(); + } + payload = this.jpaOperations.find(entityClass, id); + } + else { + final List result; + int maxNumberOfResults = evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression); + if (requestMessage == null) { + result = doPoll(this.parameterSource, 0, maxNumberOfResults); + } + else { + int firstResult = 0; + if (this.firstResultExpression != null) { + firstResult = getFirstResult(requestMessage); + } + ParameterSource parameterSource = determineParameterSource(requestMessage); + result = doPoll(parameterSource, firstResult, maxNumberOfResults); + } + + if (result.isEmpty()) { + payload = null; + } + else { + if (this.expectSingleResult) { + if (result.size() == 1) { + payload = result.iterator().next(); + } + else if (requestMessage != null) { + throw new MessagingException(requestMessage, + "The Jpa operation returned more than 1 result for expectSingleResult mode."); + } + else { + throw new MessagingException( + "The Jpa operation returned more than 1 result for expectSingleResult mode."); + } + } + else { + payload = result; + } + } + } + + if (payload != null && this.deleteAfterPoll) { + if (payload instanceof Iterable) { + if (this.deleteInBatch) { + this.jpaOperations.deleteInBatch((Iterable) payload); + } + else { + for (Object entity : (Iterable) payload) { + this.jpaOperations.delete(entity); + } + } + } + else { + this.jpaOperations.delete(payload); + } + + if (this.flush) { + this.jpaOperations.flush(); + } + } + return payload; + } + + protected List doPoll(ParameterSource jpaQLParameterSource, int firstResult, int maxNumberOfResults) { + List payload; + 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; + } + + private int getFirstResult(final Message requestMessage) { + return evaluateExpressionForNumericResult(requestMessage, this.firstResultExpression); + } + + private int evaluateExpressionForNumericResult(@Nullable final Message requestMessage, + @Nullable Expression expression) { + + int evaluatedResult = 0; + if (expression != null) { + Object evaluationResult = expression.getValue(this.evaluationContext, requestMessage); // NOSONAR can be null + 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; + } + } diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/package-info.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/package-info.java index f5d170d3b6..67b7215137 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/package-info.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/package-info.java @@ -1,4 +1,5 @@ /** * Provides core classes of the JPA module. */ +@org.springframework.lang.NonNullApi package org.springframework.integration.jpa.core; diff --git a/spring-integration-jpa/src/main/resources/org/springframework/integration/jpa/config/spring-integration-jpa-5.1.xsd b/spring-integration-jpa/src/main/resources/org/springframework/integration/jpa/config/spring-integration-jpa-5.1.xsd index 40472b4090..4ab0677341 100644 --- a/spring-integration-jpa/src/main/resources/org/springframework/integration/jpa/config/spring-integration-jpa-5.1.xsd +++ b/spring-integration-jpa/src/main/resources/org/springframework/integration/jpa/config/spring-integration-jpa-5.1.xsd @@ -90,12 +90,13 @@ @@ -129,12 +130,12 @@ - + Reference to a ParameterSourceFactory. + Mutually exclusive with the 'parameter' sub-element(s). @@ -188,8 +189,8 @@ parameters for the queries that are either based on the Java Persistence Query Language (JPQL) or native SQL queries. - Parameters can also be provided for Named Queries. + Mutually exclusive with the 'parameter-source-factory' attribute. ]]> @@ -222,8 +223,8 @@ parameters for the queries that are either based on the Java Persistence Query Language (JPQL) or native SQL queries. - Parameters can also be provided for Named Queries. + Mutually exclusive with the 'parameter-source-factory' attribute. ]]> @@ -395,8 +396,8 @@ The parameter source factory that would be used for evaluating the - parameters of the response JPA QL that would be evaluated - JPA outbound gateway + parameters of the response JPA QL that would be evaluated JPA outbound gateway. + Mutually exclusive with the 'parameter' sub-element(s). diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java index 81b70a2fd2..7e66e9a1ba 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java @@ -16,6 +16,7 @@ package org.springframework.integration.jpa.core; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -36,12 +37,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.jpa.support.JpaParameter; import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory; +import org.springframework.integration.jpa.support.parametersource.ParameterSourceFactory; import org.springframework.integration.jpa.test.entity.StudentDomain; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; /** @@ -52,8 +53,7 @@ import org.springframework.transaction.annotation.Transactional; * * @since 2.2 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) public class JpaExecutorTests { @Autowired @@ -70,6 +70,7 @@ public class JpaExecutorTests { @Test public void testExecutePollWithNoEntityClassSpecified() { JpaExecutor jpaExecutor = new JpaExecutor(mock(EntityManager.class)); + jpaExecutor.setBeanFactory(mock(BeanFactory.class)); jpaExecutor.afterPropertiesSet(); try { jpaExecutor.poll(); @@ -191,6 +192,7 @@ public class JpaExecutorTests { executor.setJpaQuery(query); executor.setExpectSingleResult(true); executor.setUsePayloadAsParameterSource(false); + executor.setBeanFactory(mock(BeanFactory.class)); executor.afterPropertiesSet(); return executor; } @@ -205,6 +207,7 @@ public class JpaExecutorTests { executor.setJpaQuery(query); executor.setExpectSingleResult(true); executor.setUsePayloadAsParameterSource(true); + executor.setBeanFactory(mock(BeanFactory.class)); executor.afterPropertiesSet(); return executor; } @@ -274,4 +277,16 @@ public class JpaExecutorTests { fail("Expected the test case to throw an exception"); } + @Test + public void testParameterSourceFactoryAndJpaParameters() { + JpaExecutor executor = new JpaExecutor(this.entityManager); + ParameterSourceFactory parameterSourceFactory = new ExpressionEvaluatingParameterSourceFactory(); + executor.setParameterSourceFactory(parameterSourceFactory); + executor.setJpaParameters(Collections.singletonList(new JpaParameter("firstName", null, "#this"))); + + assertThatThrownBy(executor::afterPropertiesSet) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("The 'jpaParameters' and 'parameterSourceFactory' are mutually exclusive."); + } + } diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java index 3aa65d6fef..c0fa5c7562 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java @@ -177,7 +177,7 @@ public class JpaTests { @Test public void testRetrievingGatewayFlow() { - this.retrievingGatewayFlowInput.send(MessageBuilder.withPayload(1002L).build()); + this.retrievingGatewayFlowInput.send(MessageBuilder.withPayload("foo").setHeader("payloadId", 1002L).build()); Message receive = this.retrieveResults.receive(10_000); assertNotNull(receive); assertThat(receive.getPayload(), instanceOf(StudentDomain.class)); @@ -252,7 +252,7 @@ public class JpaTests { .handle(Jpa.retrievingGateway(entityManagerFactory) .jpaQuery("from Student s where s.id = :id") .expectSingleResult(true) - .parameterExpression("id", "payload")) + .parameterExpression("id", "headers[payloadId]")) .channel(c -> c.queue("retrieveResults")); } diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests.java index cceb3c474e..d801fcbaae 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -16,10 +16,16 @@ package org.springframework.integration.jpa.outbound; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + import java.util.List; import org.junit.After; -import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,6 +44,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Gunnar Hillert * @author Artem Bilan * @author Amol Nayak + * * @since 2.2 */ @RunWith(SpringJUnit4ClassRunner.class) @@ -60,21 +67,21 @@ public class JpaOutboundGatewayTests { @Test public void getStudent() { final StudentDomain student = studentService.getStudent(1001L); - Assert.assertNotNull(student); + assertNotNull(student); } @Test public void getAllStudentsStartingFromGivenRecord() { List students = studentService.getAllStudentsFromGivenRecord(1); - Assert.assertNotNull(students); - Assert.assertEquals(2, students.size()); + assertNotNull(students); + assertEquals(2, students.size()); } @Test public void getAllStudentsWithMaxNumberOfRecords() { List students = studentService.getStudents(1); - Assert.assertNotNull(students); - Assert.assertEquals(1, students.size()); + assertNotNull(students); + assertEquals(1, students.size()); } @@ -86,27 +93,24 @@ public class JpaOutboundGatewayTests { try { studentService.deleteStudent(student); + fail("IllegalArgumentException is expected"); } catch (IllegalArgumentException e) { - return; + assertThat(e.getMessage(), startsWith("Removing a detached instance")); } - Assert.fail("Was expecting a MessageHandlingException to be thrown."); } @Test public void getStudentWithException() { try { studentService.getStudentWithException(1001L); + fail("MessageHandlingException is expected"); } catch (MessagingException e) { - Assert.assertEquals("The Jpa operation returned more than 1 result object but expectSingleResult was 'true'.", + assertEquals("The Jpa operation returned more than 1 result for expectSingleResult mode.", e.getMessage()); - - return; } - - Assert.fail("Was expecting a MessageHandlingException to be thrown."); } @Test @@ -114,15 +118,15 @@ public class JpaOutboundGatewayTests { StudentDomain student = studentService.getStudentWithParameters("First Two"); - Assert.assertEquals("First Two", student.getFirstName()); - Assert.assertEquals("Last Two", student.getLastName()); + assertEquals("First Two", student.getFirstName()); + assertEquals("Last Two", student.getLastName()); } @Test public void getAllStudents() { List students = studentService.getAllStudents(); - Assert.assertNotNull(students); - Assert.assertTrue(students.size() == 3); + assertNotNull(students); + assertEquals(3, students.size()); } @Test @@ -130,11 +134,11 @@ public class JpaOutboundGatewayTests { public void persistStudent() { final StudentDomain studentToPersist = JpaTestUtils.getTestStudent(); - Assert.assertNull(studentToPersist.getRollNumber()); + assertNull(studentToPersist.getRollNumber()); final StudentDomain persistedStudent = studentService.persistStudent(studentToPersist); - Assert.assertNotNull(persistedStudent); - Assert.assertNotNull(persistedStudent.getRollNumber()); + assertNotNull(persistedStudent); + assertNotNull(persistedStudent.getRollNumber()); } @@ -143,18 +147,18 @@ public class JpaOutboundGatewayTests { public void persistStudentUsingMerge() { final StudentDomain studentToPersist = JpaTestUtils.getTestStudent(); - Assert.assertNull(studentToPersist.getRollNumber()); + assertNull(studentToPersist.getRollNumber()); final StudentDomain persistedStudent = studentService.persistStudentUsingMerge(studentToPersist); - Assert.assertNotNull(persistedStudent); - Assert.assertNotNull(persistedStudent.getRollNumber()); + assertNotNull(persistedStudent); + assertNotNull(persistedStudent.getRollNumber()); } @Test public void testRetrievingGatewayInsideChain() { final StudentDomain student = studentService.getStudent2(1001L); - Assert.assertNotNull(student); + assertNotNull(student); } @Test @@ -162,18 +166,18 @@ public class JpaOutboundGatewayTests { public void testUpdatingGatewayInsideChain() { final StudentDomain studentToPersist = JpaTestUtils.getTestStudent(); - Assert.assertNull(studentToPersist.getRollNumber()); + assertNull(studentToPersist.getRollNumber()); final StudentDomain persistedStudent = studentService.persistStudent2(studentToPersist); - Assert.assertNotNull(persistedStudent); - Assert.assertNotNull(persistedStudent.getRollNumber()); + assertNotNull(persistedStudent); + assertNotNull(persistedStudent.getRollNumber()); } @Test public void testJpaRepositoryAsService() { List students = this.studentService.getStudentsUsingJpaRepository("F"); - Assert.assertEquals(2, students.size()); + assertEquals(2, students.size()); } } diff --git a/src/reference/asciidoc/jpa.adoc b/src/reference/asciidoc/jpa.adoc index 7875c97073..d7008f794f 100644 --- a/src/reference/asciidoc/jpa.adoc +++ b/src/reference/asciidoc/jpa.adoc @@ -368,7 +368,7 @@ If `false`, the `List` of entities is sent as the payload of the message. The value defaults to `false`. Optional. <9> An implementation of `org.springframework.integration.jpa.core.JpaOperations` used to perform the JPA operations. -We recommend not providig an implementation of your own but using the default `org.springframework.integration.jpa.core.DefaultJpaOperations` implementation. +We recommend not providing an implementation of your own but using the default `org.springframework.integration.jpa.core.DefaultJpaOperations` implementation. You can use any of the `entity-manager`, `entity-manager-factory`, or `jpa-operations` attributes. Optional. <10> The JPA QL to be executed by this adapter. @@ -705,7 +705,7 @@ It defaults to `Ordered.LOWEST_PRECEDENCE`. Optional. <11> An instance of `o.s.i.jpa.support.parametersource.ParameterSourceFactory` used to get an instance of `o.s.i.jpa.support.parametersource.ParameterSource`, which is used to resolve the values of the parameters in the query. Ignored if you perform operations by using a JPA entity. -If you use a parameter element, the factory must be of type `ExpressionEvaluatingParameterSourceFactory`, located in the `o.s.i.jpa.support.parametersource` package. +The `parameter` sub-elements are mutually exclusive with the `parameter-source-factory` attribute and must be configured on the provided `ParameterSourceFactory`. Optional. <12> Accepts one of the following: `PERSIST`, `MERGE`, or `DELETE`. Indicates the operation that the adapter needs to perform. @@ -872,8 +872,8 @@ Consequently, the following parameters is available: `parameter-source-factory`:: An instance of `o.s.i.jpa.support.parametersource.ParameterSourceFactory` used to get an instance of `o.s.i.jpa.support.parametersource.ParameterSource`. The `ParameterSource` is used to resolve the values of the parameters provided in the query. -If you perform operations by using a JPA entity, the `parameter-source-factory attribute` is ignored. -If you use a `parameter` element, the factory must be of type `ExpressionEvaluatingParameterSourceFactory` (located in package `o.s.i.jpa.support.parametersource`). +If you perform operations by using a JPA entity, the `parameter-source-factory` attribute is ignored. +The `parameter` sub-elements are mutually exclusive with the `parameter-source-factory` and they have to be configured on the provided `ParameterSourceFactory`. Optional. `use-payload-as-parameter-source`:: @@ -1047,7 +1047,7 @@ The following example shows all the attributes that you can set on a retrieving ---- -<1> (Since Spring Integration 4.0.) The SpEL expression that determines the `primaryKey` value for `EntityManager.find(Class entityClass, Object primaryKey)` method against the `requestMessage` as the root object of evaluation context. +<1> (Since Spring Integration 4.0) The SpEL expression that determines the `primaryKey` value for `EntityManager.find(Class entityClass, Object primaryKey)` method against the `requestMessage` as the root object of evaluation context. The `entityClass` argument is determined from the `entity-class` attribute, if present. Otherwise, it is determined from the `payload` class. All other attributes are disallowed if you use `id-expression`. @@ -1075,7 +1075,8 @@ Version 3.0 introduced this attribute. Optional. ==== -The remaining attributes are described earlier in this chapter. See <> and <>. +The remaining attributes are described earlier in this chapter. +See <> and <>. ==== Configuring with Java Configuration @@ -1237,7 +1238,7 @@ The following example uses a retrieving outbound gateway and JPQL to retrieve (s ---- ==== -Retrieving an Entity by Using `id-expression` +===== Retrieving an Entity by Using `id-expression` The following example uses a retrieving outbound gateway with `id-expression` to retrieve (find) one and only one entity from the database: The `primaryKey` is the result of `id-expression` evaluation.