From 61f24c8d9e27024993feb893861afcc5d7d44f12 Mon Sep 17 00:00:00 2001 From: Amol Nayak Date: Fri, 27 Sep 2013 19:43:55 +0300 Subject: [PATCH] INT-2881: Add support for the firstRecord to retrieving JPA gateway JIRA: https://jira.springsource.org/browse/INT-2881 --- .../config/xml/IntegrationNamespaceUtils.java | 1 - .../RetrievingJpaOutboundGatewayParser.java | 17 +++- .../jpa/core/DefaultJpaOperations.java | 39 ++++++-- .../integration/jpa/core/JpaExecutor.java | 96 +++++++++++++++---- .../integration/jpa/core/JpaOperations.java | 12 ++- .../jpa/outbound/JpaOutboundGateway.java | 14 +-- .../JpaOutboundGatewayFactoryBean.java | 2 - .../config/xml/spring-integration-jpa-3.0.xsd | 23 +++++ .../JpaInvalidOutboundGatewayParserTests.xml | 29 ++++++ .../xml/JpaOutboundGatewayParserTests.java | 58 +++++++---- .../xml/JpaOutboundGatewayParserTests.xml | 41 +++++++- .../jpa/core/AbstractJpaOperationsTests.java | 56 ++++++++--- .../jpa/core/JpaExecutorTests.java | 46 +++++++++ ...utboundGatewayIntegrationTests-context.xml | 25 +++++ .../JpaOutboundGatewayIntegrationTests.java | 72 ++++++++++++++ .../JpaOutboundGatewayTests-context.xml | 22 +++++ .../jpa/outbound/JpaOutboundGatewayTests.java | 13 ++- .../jpa/outbound/StudentService.java | 4 +- .../jpa/test/entity/StudentDomain.java | 5 +- src/reference/docbook/jpa.xml | 16 ++++ src/reference/docbook/whats-new.xml | 9 ++ 21 files changed, 516 insertions(+), 84 deletions(-) create mode 100644 spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaInvalidOutboundGatewayParserTests.xml create mode 100644 spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests-context.xml create mode 100644 spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java index 4d98b70048..9fbf517132 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/IntegrationNamespaceUtils.java @@ -470,5 +470,4 @@ public abstract class IntegrationNamespaceUtils { .addConstructorArgValue(methodSignature); registry.registerBeanDefinition(functionId, builder.getBeanDefinition()); } - } diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/config/xml/RetrievingJpaOutboundGatewayParser.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/config/xml/RetrievingJpaOutboundGatewayParser.java index fa758d5979..4e23346f45 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/config/xml/RetrievingJpaOutboundGatewayParser.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/config/xml/RetrievingJpaOutboundGatewayParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -15,13 +15,17 @@ */ package org.springframework.integration.jpa.config.xml; +import static org.springframework.integration.config.xml.IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression; + +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.jpa.support.OutboundGatewayType; -import org.w3c.dom.Element; /** * The Parser for the Retrieving Jpa Outbound Gateway. @@ -41,6 +45,13 @@ public class RetrievingJpaOutboundGatewayParser extends AbstractJpaOutboundGatew final BeanDefinitionBuilder jpaExecutorBuilder = JpaParserUtils.getOutboundGatewayJpaExecutorBuilder(gatewayElement, parserContext); + + RootBeanDefinition firstResultExpression = createExpressionDefinitionFromValueOrExpression("first-result", + "first-result-expression", parserContext, gatewayElement, false); + + if(firstResultExpression != null) { + jpaExecutorBuilder.addPropertyValue("firstResultExpression", firstResultExpression); + } IntegrationNamespaceUtils.setValueIfAttributeDefined(jpaExecutorBuilder, gatewayElement, "max-number-of-results"); IntegrationNamespaceUtils.setValueIfAttributeDefined(jpaExecutorBuilder, gatewayElement, "delete-after-poll"); IntegrationNamespaceUtils.setValueIfAttributeDefined(jpaExecutorBuilder, gatewayElement, "delete-in-batch"); @@ -55,7 +66,5 @@ public class RetrievingJpaOutboundGatewayParser extends AbstractJpaOutboundGatew jpaOutboundGatewayBuilder.addConstructorArgReference(jpaExecutorBeanName); jpaOutboundGatewayBuilder.addPropertyValue("gatewayType", OutboundGatewayType.RETRIEVING); return jpaOutboundGatewayBuilder; - } - } diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/DefaultJpaOperations.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/DefaultJpaOperations.java index 4d6745e396..ec1acc7bbe 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/DefaultJpaOperations.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/DefaultJpaOperations.java @@ -25,6 +25,7 @@ import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.integration.jpa.support.JpaUtils; import org.springframework.integration.jpa.support.parametersource.ParameterSource; import org.springframework.integration.jpa.support.parametersource.PositionSupportingParameterSource; @@ -45,11 +46,13 @@ public class DefaultJpaOperations extends AbstractJpaOperations { private static final Log logger = LogFactory.getLog(DefaultJpaOperations.class); + @Override public void delete(Object entity) { Assert.notNull(entity, "The entity must not be null!"); entityManager.remove(entity); } + @Override public void deleteInBatch(Iterable entities) { Assert.notNull(entities, "entities must not be null."); @@ -80,24 +83,28 @@ public class DefaultJpaOperations extends AbstractJpaOperations { } + @Override public int executeUpdate(String updateQuery, ParameterSource source) { Query query = entityManager.createQuery(updateQuery); setParametersIfRequired(updateQuery, source, query); return query.executeUpdate(); } + @Override public int executeUpdateWithNamedQuery(String updateQuery, ParameterSource source) { Query query = entityManager.createNamedQuery(updateQuery); setParametersIfRequired(updateQuery, source, query); return query.executeUpdate(); } + @Override public int executeUpdateWithNativeQuery(String updateQuery, ParameterSource source) { Query query = entityManager.createNativeQuery(updateQuery); setParametersIfRequired(updateQuery, source, query); return query.executeUpdate(); } + @Override public T find(Class entityType, Object id) { return entityManager.find(entityType, id); } @@ -108,11 +115,15 @@ public class DefaultJpaOperations extends AbstractJpaOperations { return query; } - public List getResultListForClass(Class entityClass, int maxNumberOfResults) { + + @Override + public List getResultListForClass(Class entityClass, int firstResult, int maxNumberOfResults) { final String entityName = JpaUtils.getEntityName(entityManager, entityClass); final Query query = entityManager.createQuery("select x from " + entityName + " x", entityClass); - + if(firstResult > 0) { + query.setFirstResult(firstResult); + } if(maxNumberOfResults > 0) { query.setMaxResults(maxNumberOfResults); } @@ -121,12 +132,16 @@ public class DefaultJpaOperations extends AbstractJpaOperations { } + @Override public List getResultListForNamedQuery(String selectNamedQuery, - ParameterSource parameterSource, int maxNumberOfResults) { + ParameterSource parameterSource, int firstResult, int maxNumberOfResults) { final Query query = entityManager.createNamedQuery(selectNamedQuery); setParametersIfRequired(selectNamedQuery, parameterSource, query); + if(firstResult > 0) { + query.setFirstResult(firstResult); + } if(maxNumberOfResults > 0) { query.setMaxResults(maxNumberOfResults); } @@ -135,8 +150,9 @@ public class DefaultJpaOperations extends AbstractJpaOperations { } + @Override public List getResultListForNativeQuery(String selectQuery, Class entityClass, - ParameterSource parameterSource, int maxNumberOfResults) { + ParameterSource parameterSource, int firstResult, int maxNumberOfResults) { final Query query; @@ -148,6 +164,9 @@ public class DefaultJpaOperations extends AbstractJpaOperations { setParametersIfRequired(selectQuery, parameterSource, query); + if(firstResult > 0) { + query.setFirstResult(firstResult); + } if(maxNumberOfResults > 0) { query.setMaxResults(maxNumberOfResults); } @@ -155,15 +174,20 @@ public class DefaultJpaOperations extends AbstractJpaOperations { return query.getResultList(); } + @Override public List getResultListForQuery(String query, ParameterSource source) { - return getResultListForQuery(query,source, 0); + return getResultListForQuery(query,source, 0, 0); } + @Override public List getResultListForQuery(String queryString, ParameterSource source, - int maxNumberOfResults) { + int firstResult, int maxNumberOfResults) { Query query = getQuery(queryString,source); + if(firstResult > 0) { + query.setFirstResult(firstResult); + } if(maxNumberOfResults > 0) { query.setMaxResults(maxNumberOfResults); } @@ -171,16 +195,19 @@ public class DefaultJpaOperations extends AbstractJpaOperations { return query.getResultList(); } + @Override public Object getSingleResultForQuery(String queryString, ParameterSource source) { Query query = getQuery(queryString,source); return query.getSingleResult(); } + @Override public Object merge(Object entity) { Assert.notNull(entity, "The object to merge must not be null."); return persistOrMerge(entity, true); } + @Override public void persist(Object entity) { Assert.notNull(entity, "The object to persist must not be null."); persistOrMerge(entity, false); 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 f2b7ab5755..29279c2a63 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-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -25,8 +25,11 @@ 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.integration.Message; import org.springframework.integration.MessagingException; +import org.springframework.integration.expression.IntegrationEvaluationContextAware; import org.springframework.integration.jpa.support.JpaParameter; import org.springframework.integration.jpa.support.PersistMode; import org.springframework.integration.jpa.support.parametersource.BeanPropertyParameterSourceFactory; @@ -60,7 +63,7 @@ import org.springframework.util.Assert; * @since 2.2 * */ -public class JpaExecutor implements InitializingBean, BeanFactoryAware { +public class JpaExecutor implements InitializingBean, BeanFactoryAware, IntegrationEvaluationContextAware { private volatile JpaOperations jpaOperations; private volatile List jpaParameters; @@ -73,6 +76,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { /** 0 means all possible objects shall be retrieved. */ private volatile int maxNumberOfResults = 0; + private volatile Expression firstResultExpression; + private volatile PersistMode persistMode = PersistMode.MERGE; private volatile ParameterSourceFactory parameterSourceFactory = null; @@ -93,6 +98,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { private volatile BeanFactory beanFactory; + private volatile EvaluationContext evaluationContext; + /** * Constructor taking an {@link EntityManagerFactory} from which the * {@link EntityManager} can be obtained. @@ -148,6 +155,7 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { * {@link ParameterSourceFactory}. * */ + @Override public void afterPropertiesSet() { if (this.jpaParameters != null) { @@ -247,6 +255,7 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { } + /** * Execute a (typically retrieving) JPA operation. The requestMessage * can be used to provide additional query parameters using @@ -259,18 +268,18 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { */ @SuppressWarnings("unchecked") public Object poll(final Message requestMessage) { - final Object payload; - final List result; - if (requestMessage == null) { - result = doPoll(this.parameterSource); + result = doPoll(this.parameterSource, 0); } else { + int firstResult = 0; + if(firstResultExpression != null) { + firstResult = getFirstResult(requestMessage); + } ParameterSource parameterSource = determineParameterSource(requestMessage); - - result = doPoll(parameterSource); + result = doPoll(parameterSource, firstResult); } if (result.isEmpty()) { @@ -283,21 +292,17 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { 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); @@ -313,10 +318,34 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { } } - return payload; } + private int getFirstResult(final Message requestMessage) { + int firstResult = 0; + Object firstRecordEvaluationResult = firstResultExpression.getValue(evaluationContext, requestMessage); + if(firstRecordEvaluationResult != null) { + if(firstRecordEvaluationResult instanceof Number) { + firstResult = ((Number)firstRecordEvaluationResult).intValue(); + } + else if(firstRecordEvaluationResult instanceof String){ + try { + firstResult = Integer.parseInt((String)firstRecordEvaluationResult); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Value " + firstRecordEvaluationResult + " passed as firstRecord cannot be " + + "parsed to a number"); + } + } + else { + throw new IllegalArgumentException("Expected the value of the firstRecord to be a Number" + + " got " + firstRecordEvaluationResult.getClass().getName()); + } + } + return firstResult; + } + private ParameterSource determineParameterSource(final Message requestMessage) { ParameterSource parameterSource; if (usePayloadAsParameterSource) { @@ -335,28 +364,29 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { return poll(null); } - protected List doPoll(ParameterSource jpaQLParameterSource) { - + protected List doPoll(ParameterSource jpaQLParameterSource, int firstResult) { List payload = null; - if (this.jpaQuery != null) { - payload = jpaOperations.getResultListForQuery(this.jpaQuery, jpaQLParameterSource, maxNumberOfResults); + payload = jpaOperations.getResultListForQuery(this.jpaQuery, jpaQLParameterSource, + firstResult, maxNumberOfResults); } else if (this.nativeQuery != null) { - payload = jpaOperations.getResultListForNativeQuery(this.nativeQuery, this.entityClass, jpaQLParameterSource, maxNumberOfResults); + payload = jpaOperations.getResultListForNativeQuery(this.nativeQuery, this.entityClass, jpaQLParameterSource, + firstResult, maxNumberOfResults); } else if (this.namedQuery != null) { - payload = jpaOperations.getResultListForNamedQuery(this.namedQuery, jpaQLParameterSource, maxNumberOfResults); + payload = jpaOperations.getResultListForNamedQuery(this.namedQuery, jpaQLParameterSource, + firstResult, maxNumberOfResults); } else if (this.entityClass != null) { - payload = jpaOperations.getResultListForClass(this.entityClass, maxNumberOfResults); + payload = 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; } @@ -509,4 +539,28 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware { this.maxNumberOfResults = maxNumberOfResults; } + /** + * Sets 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 + * + * @see Query#setFirstResult(int) + */ + public void setFirstResultExpression(Expression firstResultExpression) { + this.firstResultExpression = firstResultExpression; + } + + + /** + * Sets the evaluation context for evaluating the expression to get the from record of the + * result set retrieved by the retrieving gateway. + * + * @param evaluationContext + */ + @Override + public void setIntegrationEvaluationContext( + EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + } } diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaOperations.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaOperations.java index 7c6e3145cc..e7d9628e63 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaOperations.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaOperations.java @@ -85,20 +85,24 @@ public interface JpaOperations { /** * * @param entityClass + * @param firstResult * @param maxNumberOfReturnedObjects * @return List of found entities */ List getResultListForClass(Class entityClass, + int firstResult, int maxNumberOfReturnedObjects); /** * * @param selectNamedQuery * @param jpaQLParameterSource + * @param firstResult * @param maxNumberOfResults * @return List of found entities */ List getResultListForNamedQuery(String selectNamedQuery, ParameterSource jpaQLParameterSource, + int firstResult, int maxNumberOfResults); /** @@ -106,11 +110,14 @@ public interface JpaOperations { * @param selectQuery * @param entityClass * @param jpaQLParameterSource + * @param firstResult * @param maxNumberOfResults * @return List of found entities */ List getResultListForNativeQuery(String selectQuery, - Class entityClass, ParameterSource jpaQLParameterSource, int maxNumberOfResults); + Class entityClass, ParameterSource jpaQLParameterSource, + int firstResult, + int maxNumberOfResults); /** * Executes the provided query to return a list of results @@ -124,11 +131,12 @@ public interface JpaOperations { * Executes the provided query to return a list of results. * * @param query Must not be null or empty + * @param firstResult The first result * @param maxNumberOfResults Must be a non-negative value * @param source the Parameter source for this query to be executed, if none then set null * @return List of found entities */ - List getResultListForQuery(String query, ParameterSource source, int maxNumberOfResults); + List getResultListForQuery(String query, ParameterSource source, int firstResult, int maxNumberOfResults); /** * Executes the provided query to return a single element diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGateway.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGateway.java index 97aeba4571..d6b01970ad 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGateway.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGateway.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -39,6 +39,8 @@ import org.springframework.util.Assert; * constructor. * * @author Gunnar Hillert + * @author Amol Nayak + * * @since 2.2 * */ @@ -57,6 +59,7 @@ public class JpaOutboundGateway extends AbstractReplyProducingMessageHandler { public JpaOutboundGateway(JpaExecutor jpaExecutor) { Assert.notNull(jpaExecutor, "jpaExecutor must not be null."); this.jpaExecutor = jpaExecutor; + } /** @@ -70,21 +73,13 @@ public class JpaOutboundGateway extends AbstractReplyProducingMessageHandler { @Override protected Object handleRequestMessage(Message requestMessage) { - final Object result; - if (OutboundGatewayType.RETRIEVING.equals(this.gatewayType)) { - result = this.jpaExecutor.poll(requestMessage); - } else if (OutboundGatewayType.UPDATING.equals(this.gatewayType)) { - result = this.jpaExecutor.executeOutboundJpaOperation(requestMessage); - } else { - throw new IllegalArgumentException(String.format("GatewayType '%s' is not supported.", this.gatewayType)); - } if (result == null || !producesReply) { @@ -114,5 +109,4 @@ public class JpaOutboundGateway extends AbstractReplyProducingMessageHandler { public void setProducesReply(boolean producesReply) { this.producesReply = producesReply; } - } diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayFactoryBean.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayFactoryBean.java index 92df61c498..193008a355 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayFactoryBean.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayFactoryBean.java @@ -155,7 +155,6 @@ public class JpaOutboundGatewayFactoryBean extends AbstractFactoryBean + + + + The attribute that is used to set the first result marker while executing the + results. A negative value will retrieve from first record in the result set. + It is a way for the application to use the gateway to paginate the results in + combination with the max-number-of-results attribute. This attribute is mutually + exclusive to first-result-expression attribute + + + + + + + The attribute that is used to set the first result expression that would + be evaluated to get the first record while executing the JPA query for result + A negative value will retrieve from first record in the result set. + It is a way for the application to use the gateway to paginate the results in + combination with the max-number-of-results attribute. This attribute is mutually + exclusive to first-result attribute + + + diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaInvalidOutboundGatewayParserTests.xml b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaInvalidOutboundGatewayParserTests.xml new file mode 100644 index 0000000000..8f0e8fec1e --- /dev/null +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaInvalidOutboundGatewayParserTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.java index 06b9ff3141..0173050670 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.java @@ -18,13 +18,17 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.aopalliance.intercept.MethodInvocation; import org.junit.After; import org.junit.Test; import org.mockito.Mockito; + import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.integration.Message; import org.springframework.integration.MessagingException; import org.springframework.integration.channel.AbstractMessageChannel; @@ -57,43 +61,47 @@ public class JpaOutboundGatewayParserTests extends AbstractRequestHandlerAdvice @Test public void testRetrievingJpaOutboundGatewayParser() throws Exception { setUp("JpaOutboundGatewayParserTests.xml", getClass(), "retrievingJpaOutboundGateway"); - - final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(this.consumer, "inputChannel", AbstractMessageChannel.class); - assertEquals("in", inputChannel.getComponentName()); - final JpaOutboundGateway jpaOutboundGateway = TestUtils.getPropertyValue(this.consumer, "handler", JpaOutboundGateway.class); - final OutboundGatewayType gatewayType = TestUtils.getPropertyValue(jpaOutboundGateway, "gatewayType", OutboundGatewayType.class); - assertEquals(OutboundGatewayType.RETRIEVING, gatewayType); - long sendTimeout = TestUtils.getPropertyValue(jpaOutboundGateway, "messagingTemplate.sendTimeout", Long.class); - assertEquals(100, sendTimeout); - assertFalse(TestUtils.getPropertyValue(jpaOutboundGateway, "requiresReply", Boolean.class)); - final JpaExecutor jpaExecutor = TestUtils.getPropertyValue(this.consumer, "handler.jpaExecutor", JpaExecutor.class); - assertNotNull(jpaExecutor); - final Class entityClass = TestUtils.getPropertyValue(jpaExecutor, "entityClass", Class.class); - assertEquals("org.springframework.integration.jpa.test.entity.StudentDomain", entityClass.getName()); - final JpaOperations jpaOperations = TestUtils.getPropertyValue(jpaExecutor, "jpaOperations", JpaOperations.class); - assertNotNull(jpaOperations); - assertTrue(TestUtils.getPropertyValue(jpaExecutor, "expectSingleResult", Boolean.class)); - final Integer maxNumberOfResults = TestUtils.getPropertyValue(jpaExecutor, "maxNumberOfResults", Integer.class); - assertEquals(Integer.valueOf(55), maxNumberOfResults); } + @Test + public void testRetrievingJpaOutboundGatewayParserWithFirstResult() throws Exception { + setUp("JpaOutboundGatewayParserTests.xml", getClass(), "retrievingJpaOutboundGatewayWithFirstResult"); + final JpaOutboundGateway jpaOutboundGateway = TestUtils.getPropertyValue(this.consumer, "handler", JpaOutboundGateway.class); + Expression firstResultExpression = + TestUtils.getPropertyValue(jpaOutboundGateway, "jpaExecutor.firstResultExpression", Expression.class); + assertNotNull(firstResultExpression); + assertEquals(LiteralExpression.class, firstResultExpression.getClass()); + assertEquals("1", TestUtils.getPropertyValue(firstResultExpression, "literalValue", String.class)); + } + + @Test + public void testRetrievingJpaOutboundGatewayParserWithFirstResultExpression() throws Exception { + setUp("JpaOutboundGatewayParserTests.xml", getClass(), "retrievingJpaOutboundGatewayWithFirstResultExpression"); + final JpaOutboundGateway jpaOutboundGateway = TestUtils.getPropertyValue(this.consumer, "handler", JpaOutboundGateway.class); + Expression firstResultExpression = + TestUtils.getPropertyValue(jpaOutboundGateway, "jpaExecutor.firstResultExpression", Expression.class); + assertNotNull(firstResultExpression); + assertEquals(SpelExpression.class, firstResultExpression.getClass()); + assertEquals("header['firstResult']", TestUtils.getPropertyValue(firstResultExpression, "expression", String.class)); + } + @Test public void testUpdatingJpaOutboundGatewayParser() throws Exception { setUp("JpaOutboundGatewayParserTests.xml", getClass(), "updatingJpaOutboundGateway"); @@ -170,6 +178,18 @@ public class JpaOutboundGatewayParserTests extends AbstractRequestHandlerAdvice } + @Test + public void withBothFirstResultAndFirstResultExpressionPresent() { + try { + this.context = new ClassPathXmlApplicationContext("JpaInvalidOutboundGatewayParserTests.xml", getClass()); + } catch (BeanDefinitionStoreException e) { + assertTrue(e.getMessage().startsWith("Configuration problem: Only one of 'first-result' or 'first-result-expression' is allowed")); + return; + } + fail("BeanDefinitionStoreException expected."); + + } + @After public void tearDown() { if (context != null) { diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.xml b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.xml index 3471e9e2f3..1df8275b01 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.xml +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/config/xml/JpaOutboundGatewayParserTests.xml @@ -26,6 +26,46 @@ reply-timeout="100" requires-reply="false"/> + + + + + + + - diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/AbstractJpaOperationsTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/AbstractJpaOperationsTests.java index 968a5b5971..b992972961 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/AbstractJpaOperationsTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/AbstractJpaOperationsTests.java @@ -24,6 +24,7 @@ import java.util.List; import javax.persistence.EntityManager; import org.junit.Assert; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory; @@ -60,7 +61,7 @@ public class AbstractJpaOperationsTests { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertTrue(students.size() == 3); } @@ -72,7 +73,7 @@ public class AbstractJpaOperationsTests { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List students = jpaOperations.getResultListForClass(StudentDomain.class, 2); + final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0, 2); Assert.assertTrue(String.format("Was expecting 2 Students to be returned but got '%s'.", students.size()), students.size() == 2); @@ -87,7 +88,7 @@ public class AbstractJpaOperationsTests { final StudentDomain student = JpaTestUtils.getTestStudent(); - List students = jpaOperations.getResultListForClass(StudentDomain.class, 0); + List students = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertTrue(students.size() == 3); ParameterSourceFactory requestParameterSourceFactory = @@ -156,7 +157,7 @@ public class AbstractJpaOperationsTests { Class entityClass = StudentDomain.class; - List students = jpaOperations.getResultListForNativeQuery(selectSqlQuery, entityClass, null, 0); + List students = jpaOperations.getResultListForNativeQuery(selectSqlQuery, entityClass, null, 0, 0); Assert.assertTrue(students.size() == 1); @@ -181,7 +182,7 @@ public class AbstractJpaOperationsTests { String selectSqlQuery = "select rollNumber, firstName, lastName, gender, dateOfBirth, lastUpdated from Student where lastName = 'Last One'"; - List students = jpaOperations.getResultListForNativeQuery(selectSqlQuery, null, null, 0); + List students = jpaOperations.getResultListForNativeQuery(selectSqlQuery, null, null, 0, 0); Assert.assertTrue(students.size() == 1); @@ -285,7 +286,7 @@ public class AbstractJpaOperationsTests { public void testMergeCollectionWithNullElement() { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List studentsFromDbBeforeTest = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDbBeforeTest = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertEquals(3, studentsFromDbBeforeTest.size()); @@ -379,7 +380,7 @@ public class AbstractJpaOperationsTests { public void testPersistCollectionWithNullElement() { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List studentsFromDbBeforeTest = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDbBeforeTest = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertEquals(3, studentsFromDbBeforeTest.size()); @@ -405,7 +406,7 @@ public class AbstractJpaOperationsTests { Assert.assertNotNull(student1.getRollNumber()); Assert.assertNotNull(student3.getRollNumber()); - final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(studentsFromDb); Assert.assertEquals(5, studentsFromDb.size()); @@ -414,7 +415,7 @@ public class AbstractJpaOperationsTests { public void testDeleteInBatch() { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(students); @@ -432,7 +433,7 @@ public class AbstractJpaOperationsTests { transactionManager.commit(status); - final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(studentsFromDb); Assert.assertTrue(studentsFromDb.size() == 0); @@ -451,7 +452,7 @@ public class AbstractJpaOperationsTests { public void testDelete() { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(studentsFromDb); Assert.assertTrue(studentsFromDb.size() == 3); @@ -475,7 +476,7 @@ public class AbstractJpaOperationsTests { transactionManager.commit(status); - final List studentsFromDbAfterDelete = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDbAfterDelete = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(studentsFromDbAfterDelete); Assert.assertTrue(studentsFromDbAfterDelete.size() == 2); @@ -485,7 +486,7 @@ public class AbstractJpaOperationsTests { public void testDeleteInBatchWithEmptyCollection() { final JpaOperations jpaOperations = getJpaOperations(entityManager); - final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List students = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(students); Assert.assertTrue(students.size() == 3); @@ -503,11 +504,38 @@ public class AbstractJpaOperationsTests { transactionManager.commit(status); - final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0); + final List studentsFromDb = jpaOperations.getResultListForClass(StudentDomain.class, 0, 0); Assert.assertNotNull(studentsFromDb); Assert.assertTrue(studentsFromDb.size() == 3); //Nothing should have happened } + public void testGetAllStudentsFromThirdRecord() { + JpaOperations jpaOperations = getJpaOperations(entityManager); + List results = jpaOperations.getResultListForClass(StudentDomain.class, 2, 0); + assertEquals(1, results.size()); + } + + + public void testGetAllStudentsUsingNativeQueryFromThirdRecord() { + JpaOperations jpaOperations = getJpaOperations(entityManager); + String query = "select * from Student"; + List results = jpaOperations.getResultListForNativeQuery(query, StudentDomain.class, null, 2, 0); + assertEquals(1, results.size()); + } + + + public void testGetAllStudentsUsingNamedQueryFromThirdRecord() { + JpaOperations jpaOperations = getJpaOperations(entityManager); + List results = jpaOperations.getResultListForNamedQuery("selectAllStudents", null, 2, 0); + assertEquals(1, results.size()); + } + + public void testGetAllStudentsUsingJPAQueryFromThirdRecord() { + JpaOperations jpaOperations = getJpaOperations(entityManager); + String query = "select s from Student s"; + List results = jpaOperations.getResultListForQuery(query, null, 2, 0); + assertEquals(1, results.size()); + } } 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 efa3839baf..6c4dad7034 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 @@ -15,6 +15,7 @@ package org.springframework.integration.jpa.core; import static org.mockito.Mockito.mock; import java.util.Collections; +import java.util.List; import java.util.Map; import javax.persistence.EntityManager; @@ -25,6 +26,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.Message; import org.springframework.integration.jpa.support.JpaParameter; import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory; @@ -224,4 +226,48 @@ public class JpaExecutorTests { } + @Test + public void testResultStartingFromThirdRecordForJPAQuery() throws Exception { + final JpaExecutor jpaExecutor = new JpaExecutor(entityManager); + jpaExecutor.setJpaQuery("select s from Student s"); + jpaExecutor.setFirstResultExpression(new LiteralExpression("2")); + jpaExecutor.afterPropertiesSet(); + + List results = (List)jpaExecutor.poll(MessageBuilder.withPayload("").build()); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + } + + @Test + public void testResultStartingFromThirdRecordForNativeQuery() throws Exception { + final JpaExecutor jpaExecutor = new JpaExecutor(entityManager); + jpaExecutor.setNativeQuery("select * from Student s"); + jpaExecutor.setFirstResultExpression(new LiteralExpression("2")); + jpaExecutor.afterPropertiesSet(); + List results = (List)jpaExecutor.poll(MessageBuilder.withPayload("").build()); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + } + + @Test + public void testResultStartingFromThirdRecordForNamedQuery() throws Exception { + final JpaExecutor jpaExecutor = new JpaExecutor(entityManager); + jpaExecutor.setNamedQuery("selectAllStudents"); + jpaExecutor.setFirstResultExpression(new LiteralExpression("2")); + jpaExecutor.afterPropertiesSet(); + List results = (List)jpaExecutor.poll(MessageBuilder.withPayload("").build()); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + } + + @Test + public void testResultStartingFromThirdRecordUsingEntity() throws Exception { + final JpaExecutor jpaExecutor = new JpaExecutor(entityManager); + jpaExecutor.setEntityClass(StudentDomain.class); + jpaExecutor.setFirstResultExpression(new LiteralExpression("2")); + jpaExecutor.afterPropertiesSet(); + List results = (List)jpaExecutor.poll(MessageBuilder.withPayload("").build()); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + } } \ No newline at end of file diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests-context.xml b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests-context.xml new file mode 100644 index 0000000000..d4d4138fcf --- /dev/null +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests-context.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests.java new file mode 100644 index 0000000000..9610b34f88 --- /dev/null +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayIntegrationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2013 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.outbound; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.integration.Message; +import org.springframework.integration.MessagingException; +import org.springframework.integration.core.MessageHandler; +import org.springframework.integration.core.SubscribableChannel; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * The test cases for testing out a complete flow of the JPA gateways/adapters with all + * the components integrated. + * + * @author Amol Nayak + * @since 3.0 + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JpaOutboundGatewayIntegrationTests { + + @Autowired + @Qualifier("in") + private SubscribableChannel requestChannel; + + @Autowired + @Qualifier("out") + private SubscribableChannel responseChannel; + + /** + * Sends a message with the payload as a integer representing the start number in the result + * set. + * @throws Exception + */ + @Test + public void retrieveFromSecondRecord() throws Exception { + responseChannel.subscribe(new MessageHandler() { + @SuppressWarnings("rawtypes") + @Override + public void handleMessage(Message message) throws MessagingException { + assertEquals(1, ((List)message.getPayload()).size()); + } + }); + Message message = MessageBuilder.withPayload(2).build(); + requestChannel.send(message); + } +} diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests-context.xml b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests-context.xml index fee9ef9d55..1fe177b46e 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests-context.xml +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests-context.xml @@ -30,6 +30,7 @@ + @@ -43,6 +44,7 @@ + @@ -198,6 +200,26 @@ + + + + + + + + + + + + + + + + + + students = studentService.getAllStudentsFromGivenRecord(1); + Assert.assertNotNull(students); + Assert.assertEquals(2, students.size()); + } + @Test public void deleteNonExistingStudent() { diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/StudentService.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/StudentService.java index 5759f5be3e..021d4ef829 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/StudentService.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/StudentService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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 @@ -29,6 +29,8 @@ public interface StudentService { @Payload("new java.util.Date()") List getAllStudents(); + List getAllStudentsFromGivenRecord(int recordNumber); + StudentDomain persistStudent(StudentDomain student); StudentDomain persistStudentUsingMerge(StudentDomain studentToPersist); diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/test/entity/StudentDomain.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/test/entity/StudentDomain.java index 6451b261fe..6aebe59170 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/test/entity/StudentDomain.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/test/entity/StudentDomain.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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,10 +36,13 @@ import javax.persistence.TemporalType; * @author Amol Nayak * @author Gunnar Hillert * + * @since 2.2 + * */ @Entity(name="Student") @Table(name="Student") @NamedQueries({ + @NamedQuery(name="selectAllStudents", query="select s from Student s"), @NamedQuery(name="selectStudent", query="select s from Student s where s.lastName = 'Last One'"), @NamedQuery(name="updateStudent", query="update Student s set s.lastName = :lastName, s.lastUpdated = :lastUpdated where s.rollNumber in (select max(a.rollNumber) from Student a)") }) diff --git a/src/reference/docbook/jpa.xml b/src/reference/docbook/jpa.xml index ce7d21bb3a..51eed2a158 100644 --- a/src/reference/docbook/jpa.xml +++ b/src/reference/docbook/jpa.xml @@ -1138,6 +1138,8 @@ public class Student { jpa-operations="" jpa-query="" max-number-of-results="" ]]>Optional. + + + This non zero, non negative integer value tells the adapter the first record from which the + results are to be retrieved This attribute is mutually exclusive to first-result-expression. + This attribute is introduced since version 3.0. Optional. + + + + + This expression is evaluated against the message to find the position of first record in the + result set to be retrieved This attribute is mutually exclusive to first-result. + This attribute is introduced since version 3.0. Optional. + + diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index c3e1a0eee4..ae3898899b 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -452,5 +452,14 @@ least 1. +
+ JPA Adapters: first-result attribute + + Retrieving gateways had no mechanism to specify the first record to be retrieved which + is a common use case. The retrieving gateways now support specifying this parameter + using a first-result and first-result-expression attributes + to the gateway definition. + +