This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch camel-3.x in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-3.x by this push: new 509f0bb775a CAMEL-19304: camel-jpa implement paging (camel-3.x) (#9976) 509f0bb775a is described below commit 509f0bb775a48f123cd42e6e3f3dcc75b9e415be Author: jacekszymanski <jacek.p.szyman...@gmail.com> AuthorDate: Tue May 2 18:36:27 2023 +0200 CAMEL-19304: camel-jpa implement paging (camel-3.x) (#9976) * refactor(test): extract non-test methods from AbstractJpaMethodTest * feat: firstResult in query * test: test firstResult * support both maximumResults and firstResult in headers * test paging * use getHeader() with default value instead of Object.requireNonNullElse --- .../camel/component/jpa/JpaEndpointConfigurer.java | 6 + .../camel/component/jpa/JpaEndpointUriFactory.java | 3 +- .../org/apache/camel/component/jpa/jpa.json | 5 +- .../apache/camel/component/jpa/JpaConstants.java | 6 + .../apache/camel/component/jpa/JpaEndpoint.java | 13 ++ .../apache/camel/component/jpa/JpaProducer.java | 12 +- .../component/jpa/AbstractJpaMethodSupport.java | 92 ++++++++++ .../camel/component/jpa/AbstractJpaMethodTest.java | 63 +------ .../apache/camel/component/jpa/JpaPagingTest.java | 199 +++++++++++++++++++++ 9 files changed, 334 insertions(+), 65 deletions(-) diff --git a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java index 36c90a5a62a..62e856b71f2 100644 --- a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java +++ b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java @@ -44,6 +44,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements case "exchangePattern": target.setExchangePattern(property(camelContext, org.apache.camel.ExchangePattern.class, value)); return true; case "findentity": case "findEntity": target.setFindEntity(property(camelContext, boolean.class, value)); return true; + case "firstresult": + case "firstResult": target.setFirstResult(property(camelContext, int.class, value)); return true; case "flushonsend": case "flushOnSend": target.setFlushOnSend(property(camelContext, boolean.class, value)); return true; case "greedy": target.setGreedy(property(camelContext, boolean.class, value)); return true; @@ -132,6 +134,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements case "exchangePattern": return org.apache.camel.ExchangePattern.class; case "findentity": case "findEntity": return boolean.class; + case "firstresult": + case "firstResult": return int.class; case "flushonsend": case "flushOnSend": return boolean.class; case "greedy": return boolean.class; @@ -221,6 +225,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements case "exchangePattern": return target.getExchangePattern(); case "findentity": case "findEntity": return target.isFindEntity(); + case "firstresult": + case "firstResult": return target.getFirstResult(); case "flushonsend": case "flushOnSend": return target.isFlushOnSend(); case "greedy": return target.isGreedy(); diff --git a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java index 5522644910a..7848eb3859a 100644 --- a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java +++ b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java @@ -21,7 +21,7 @@ public class JpaEndpointUriFactory extends org.apache.camel.support.component.En private static final Set<String> SECRET_PROPERTY_NAMES; private static final Set<String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(45); + Set<String> props = new HashSet<>(46); props.add("backoffErrorThreshold"); props.add("backoffIdleThreshold"); props.add("backoffMultiplier"); @@ -35,6 +35,7 @@ public class JpaEndpointUriFactory extends org.apache.camel.support.component.En props.add("exceptionHandler"); props.add("exchangePattern"); props.add("findEntity"); + props.add("firstResult"); props.add("flushOnSend"); props.add("greedy"); props.add("initialDelay"); diff --git a/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json b/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json index c88de42202c..4c490ed1530 100644 --- a/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json +++ b/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json @@ -34,7 +34,9 @@ }, "headers": { "CamelEntityManager": { "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "javax.persistence.EntityManager", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The JPA EntityManager object.", "constantName": "org.apache.camel.component.jpa.JpaConstants#ENTITY_MANAGER" }, - "CamelJpaParameters": { "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map<String, Object>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Alternative way for passing query parameters as an Exchange header.", "constantName": "org.apache.camel.component.jpa.JpaConstants#JPA_PARAMETERS_HEADER" } + "CamelJpaParameters": { "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map<String, Object>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Alternative way for passing query parameters as an Exchange header.", "constantName": "org.apache.camel.component.jpa.JpaConstants#JPA_PARAMETERS_HEADER" }, + "CamelJpaMaximumResults": { "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Defines the maximum number of results to retrieve on the query; takes precedence over the value set on the endpoint, if any.", "constantName": "org.apache.camel.component.jpa.JpaConstants#JPA_MAXIMUM_RESULTS" }, + "CamelJpaFirstResult": { "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Defines the position of the first result to retrieve; takes precedence over the value set on the endpoint, if any.", "constantName": "org.apache.camel.component.jpa.JpaConstants#JPA_FIRST_RESULT" } }, "properties": { "entityType": { "kind": "path", "displayName": "Entity Type", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.Class<java.lang.Object>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Entity class name" }, @@ -60,6 +62,7 @@ "parameters": { "kind": "parameter", "displayName": "Parameters", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "parameters.", "multiValue": true, "deprecated": false, "autowired": false, "secret": false, "description": "This key\/value mapping is used for building the query parameters. It is expected to be of the generic type java.util.Map where the keys ar [...] "pollStrategy": { "kind": "parameter", "displayName": "Poll Strategy", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.PollingConsumerPollStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "A pluggable org.apache.camel.PollingConsumerPollingStrategy allowing you to provide your custom implementation to control error handling usually occurred during the poll operation [...] "findEntity": { "kind": "parameter", "displayName": "Find Entity", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled then the producer will find a single entity by using the message body as key and entityType as the class type. This can be used instead of a query to find a single entity." }, + "firstResult": { "kind": "parameter", "displayName": "First Result", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": -1, "description": "Set the position of the first result to retrieve." }, "flushOnSend": { "kind": "parameter", "displayName": "Flush On Send", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Flushes the EntityManager after the entity bean has been persisted." }, "remove": { "kind": "parameter", "displayName": "Remove", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Indicates to use entityManager.remove(entity)." }, "useExecuteUpdate": { "kind": "parameter", "displayName": "Use Execute Update", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "description": "To configure whether to use executeUpdate() when producer executes a query. When you use INSERT, UPDATE or DELETE statement as a named query, you need to specify this option to 'true'." }, diff --git a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaConstants.java b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaConstants.java index 4f19738326b..37f9f635a0b 100644 --- a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaConstants.java +++ b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaConstants.java @@ -28,6 +28,12 @@ public final class JpaConstants { @Metadata(label = "producer", description = "Alternative way for passing query parameters as an Exchange header.", javaType = "Map<String, Object>") public static final String JPA_PARAMETERS_HEADER = "CamelJpaParameters"; + @Metadata(label = "producer", description = "Defines the maximum number of results to retrieve on the query; " + + "takes precedence over the value set on the endpoint, if any.") + public static final String JPA_MAXIMUM_RESULTS = "CamelJpaMaximumResults"; + @Metadata(label = "producer", description = "Defines the position of the first result to retrieve; " + + "takes precedence over the value set on the endpoint, if any.") + public static final String JPA_FIRST_RESULT = "CamelJpaFirstResult"; /** * @deprecated use {@link #ENTITY_MANAGER} diff --git a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java index e8b68c6033e..46998eec4a2 100644 --- a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java +++ b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java @@ -70,6 +70,8 @@ public class JpaEndpoint extends ScheduledPollEndpoint { private boolean sharedEntityManager; @UriParam(defaultValue = "-1") private int maximumResults = -1; + @UriParam(label = "producer", defaultValue = "-1") + private int firstResult = -1; @UriParam(label = "consumer", defaultValue = "true") private boolean consumeDelete = true; @UriParam(label = "consumer", defaultValue = "true") @@ -209,6 +211,17 @@ public class JpaEndpoint extends ScheduledPollEndpoint { this.maximumResults = maximumResults; } + public int getFirstResult() { + return firstResult; + } + + /** + * Set the position of the first result to retrieve. + */ + public void setFirstResult(int firstResult) { + this.firstResult = firstResult; + } + public Class<?> getEntityType() { return entityType; } diff --git a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java index 44d6273e047..43287bf7766 100644 --- a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java +++ b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java @@ -200,10 +200,20 @@ public class JpaProducer extends DefaultProducer { @SuppressWarnings("unchecked") private void configureParameters(Query query, Exchange exchange) { - int maxResults = getEndpoint().getMaximumResults(); + final int maxResults = exchange.getIn().getHeader( + JpaConstants.JPA_MAXIMUM_RESULTS, + getEndpoint().getMaximumResults(), + Integer.class); if (maxResults > 0) { query.setMaxResults(maxResults); } + final int firstResult = exchange.getIn().getHeader( + JpaConstants.JPA_FIRST_RESULT, + getEndpoint().getFirstResult(), + Integer.class); + if (firstResult > 0) { + query.setFirstResult(firstResult); + } // setup the parameters Map<String, ?> params; if (parameters != null) { diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodSupport.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodSupport.java new file mode 100644 index 00000000000..ca0e44b2e60 --- /dev/null +++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodSupport.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.component.jpa; + +import java.util.List; + +import javax.persistence.EntityManager; + +import org.apache.camel.Consumer; +import org.apache.camel.examples.Address; +import org.apache.camel.examples.Customer; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.AfterEach; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AbstractJpaMethodSupport extends CamelTestSupport { + + protected JpaEndpoint endpoint; + protected EntityManager entityManager; + protected TransactionTemplate transactionTemplate; + protected Consumer consumer; + + @Override + @AfterEach + public void tearDown() { + if (entityManager != null) { + entityManager.close(); + } + } + + protected void setUp(String endpointUri) throws Exception { + endpoint = context.getEndpoint(endpointUri, JpaEndpoint.class); + + transactionTemplate = endpoint.createTransactionTemplate(); + entityManager = endpoint.getEntityManagerFactory().createEntityManager(); + + transactionTemplate.execute(new TransactionCallback<Object>() { + public Object doInTransaction(TransactionStatus status) { + entityManager.joinTransaction(); + entityManager.createQuery("delete from " + Customer.class.getName()).executeUpdate(); + return null; + } + }); + + assertEntitiesInDatabase(0, Customer.class.getName()); + assertEntitiesInDatabase(0, Address.class.getName()); + } + + protected void save(final Object persistable) { + transactionTemplate.execute(new TransactionCallback<Object>() { + public Object doInTransaction(TransactionStatus status) { + entityManager.joinTransaction(); + entityManager.persist(persistable); + entityManager.flush(); + return null; + } + }); + } + + protected void assertEntitiesInDatabase(int count, String entity) { + List<?> results = entityManager.createQuery("select o from " + entity + " o").getResultList(); + assertEquals(count, results.size()); + } + + protected Customer createDefaultCustomer() { + Customer customer = new Customer(); + customer.setName("Christian Mueller"); + Address address = new Address(); + address.setAddressLine1("Hahnstr. 1"); + address.setAddressLine2("60313 Frankfurt am Main"); + customer.setAddress(address); + return customer; + } +} diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodTest.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodTest.java index 36a144e2f4f..8df47844714 100644 --- a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodTest.java +++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AbstractJpaMethodTest.java @@ -23,40 +23,22 @@ import java.util.concurrent.TimeUnit; import javax.persistence.EntityManager; -import org.apache.camel.Consumer; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.examples.Address; import org.apache.camel.examples.Customer; -import org.apache.camel.test.junit5.CamelTestSupport; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public abstract class AbstractJpaMethodTest extends CamelTestSupport { +public abstract class AbstractJpaMethodTest extends AbstractJpaMethodSupport { - protected JpaEndpoint endpoint; - protected EntityManager entityManager; - protected TransactionTemplate transactionTemplate; - protected Consumer consumer; protected Customer receivedCustomer; abstract boolean usePersist(); - @Override - @AfterEach - public void tearDown() { - if (entityManager != null) { - entityManager.close(); - } - } - @Test public void produceNewEntity() throws Exception { setUp("jpa://" + Customer.class.getName() + "?usePersist=" + (usePersist() ? "true" : "false")); @@ -149,47 +131,4 @@ public abstract class AbstractJpaMethodTest extends CamelTestSupport { assertEntitiesInDatabase(0, Address.class.getName()); } - protected void setUp(String endpointUri) throws Exception { - endpoint = context.getEndpoint(endpointUri, JpaEndpoint.class); - - transactionTemplate = endpoint.createTransactionTemplate(); - entityManager = endpoint.getEntityManagerFactory().createEntityManager(); - - transactionTemplate.execute(new TransactionCallback<Object>() { - public Object doInTransaction(TransactionStatus status) { - entityManager.joinTransaction(); - entityManager.createQuery("delete from " + Customer.class.getName()).executeUpdate(); - return null; - } - }); - - assertEntitiesInDatabase(0, Customer.class.getName()); - assertEntitiesInDatabase(0, Address.class.getName()); - } - - protected void save(final Object persistable) { - transactionTemplate.execute(new TransactionCallback<Object>() { - public Object doInTransaction(TransactionStatus status) { - entityManager.joinTransaction(); - entityManager.persist(persistable); - entityManager.flush(); - return null; - } - }); - } - - protected void assertEntitiesInDatabase(int count, String entity) { - List<?> results = entityManager.createQuery("select o from " + entity + " o").getResultList(); - assertEquals(count, results.size()); - } - - protected Customer createDefaultCustomer() { - Customer customer = new Customer(); - customer.setName("Christian Mueller"); - Address address = new Address(); - address.setAddressLine1("Hahnstr. 1"); - address.setAddressLine2("60313 Frankfurt am Main"); - customer.setAddress(address); - return customer; - } } diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java new file mode 100644 index 00000000000..824c9429c53 --- /dev/null +++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.camel.component.jpa; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.examples.Customer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JpaPagingTest extends AbstractJpaMethodSupport { + + private static final String ENDPOINT_URI = "jpa://" + Customer.class.getName() + + "?query=select c from Customer c order by c.name"; + + // should be less than 1000 as numbers in entries' names are formatted for sorting with %03d (or change format) + private static final int ENTRIES_COUNT = 30; + + private static final String ENTRY_SEQ_FORMAT = "%03d"; + + // both should be less than ENTRIES_COUNT / 2 + private static final int FIRST_RESULT = 5; + private static final int MAXIMUM_RESULTS = 10; + + protected String additionalQueryParameters = ""; + + @Test + public void testUnrestrictedQueryReturnsAll() throws Exception { + final List<Customer> customers = runQueryTest(); + + assertEquals(ENTRIES_COUNT, customers.size()); + } + + @Test + @AdditionalQueryParameters("firstResult=" + FIRST_RESULT) + public void testFirstResultInUri() throws Exception { + final List<Customer> customers = runQueryTest(); + + assertEquals(ENTRIES_COUNT - FIRST_RESULT, customers.size()); + } + + @Test + public void testMaxResultsInHeader() throws Exception { + final List<Customer> customers + = runQueryTest(exchange -> exchange.getIn().setHeader(JpaConstants.JPA_MAXIMUM_RESULTS, MAXIMUM_RESULTS)); + + assertEquals(MAXIMUM_RESULTS, customers.size()); + } + + @Test + @AdditionalQueryParameters("maximumResults=" + MAXIMUM_RESULTS) + public void testFirstInHeaderMaxInUri() throws Exception { + final List<Customer> customers = runQueryTest( + withHeader(JpaConstants.JPA_FIRST_RESULT, FIRST_RESULT)); + + assertEquals(MAXIMUM_RESULTS, customers.size()); + assertFirstCustomerSequence(customers, FIRST_RESULT); + } + + @Test + @AdditionalQueryParameters("maximumResults=" + MAXIMUM_RESULTS) + public void testMaxHeaderPrevailsOverUri() throws Exception { + final List<Customer> customers = runQueryTest( + withHeader(JpaConstants.JPA_MAXIMUM_RESULTS, MAXIMUM_RESULTS * 2)); + + assertEquals(MAXIMUM_RESULTS * 2, customers.size()); + } + + @Test + @AdditionalQueryParameters("firstResult=" + FIRST_RESULT) + public void testFirstHeaderPrevailsOverUri() throws Exception { + final List<Customer> customers = runQueryTest( + withHeader(JpaConstants.JPA_FIRST_RESULT, FIRST_RESULT * 2)); + + assertEquals(ENTRIES_COUNT - (FIRST_RESULT * 2), customers.size()); + assertFirstCustomerSequence(customers, FIRST_RESULT * 2); + } + + @Test + public void testBothInHeader() throws Exception { + final List<Customer> customers = runQueryTest( + withHeader(JpaConstants.JPA_FIRST_RESULT, FIRST_RESULT), + withHeader(JpaConstants.JPA_MAXIMUM_RESULTS, MAXIMUM_RESULTS)); + + assertEquals(MAXIMUM_RESULTS, customers.size()); + assertFirstCustomerSequence(customers, FIRST_RESULT); + } + + @Test + @AdditionalQueryParameters("firstResult=" + ENTRIES_COUNT) + public void testFirstResultAfterTheEnd() throws Exception { + final List<Customer> customers = runQueryTest(); + + assertEquals(0, customers.size()); + } + + private static void assertFirstCustomerSequence(final List<Customer> customers, final int firstResult) { + assertTrue(customers.get(0).getName().endsWith(String.format(ENTRY_SEQ_FORMAT, firstResult))); + } + + @SuppressWarnings("unchecked") + protected List<Customer> runQueryTest(final Processor... preRun) throws Exception { + setUp(getEndpointUri()); + + final Exchange result = template.send("direct:start", exchange -> { + for (Processor processor : preRun) { + processor.process(exchange); + } + }); + + return (List<Customer>) result.getMessage().getBody(List.class); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + super.beforeEach(context); + + final Optional<AnnotatedElement> element = context.getElement(); + + if (element.isPresent()) { + final AnnotatedElement annotatedElement = element.get(); + final AdditionalQueryParameters annotation = annotatedElement.getAnnotation(AdditionalQueryParameters.class); + if (annotation != null && !annotation.value().isBlank()) { + additionalQueryParameters = annotation.value(); + } + } + } + + @Override + protected void setUp(String endpointUri) throws Exception { + super.setUp(endpointUri); + + createCustomers(); + assertEntitiesInDatabase(ENTRIES_COUNT, Customer.class.getName()); + } + + protected void createCustomers() { + IntStream.range(0, ENTRIES_COUNT).forEach(idx -> { + Customer customer = createDefaultCustomer(); + customer.setName(String.format("%s " + ENTRY_SEQ_FORMAT, customer.getName(), idx)); + save(customer); + }); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + final String endpointUri = getEndpointUri(); + return new RouteBuilder() { + public void configure() { + from("direct:start") + .to(endpointUri); + } + }; + } + + protected String getEndpointUri() { + return ENDPOINT_URI + + (additionalQueryParameters.isBlank() ? "" : "&" + additionalQueryParameters); + } + + protected Processor withHeader(final String headerName, final Object headerValue) { + return exchange -> exchange.getIn().setHeader(headerName, headerValue); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD }) + private @interface AdditionalQueryParameters { + String value(); + } + +}