You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2023/05/02 12:53:30 UTC

[camel] branch main updated: CAMEL-19304: camel-jpa implement paging (#9970)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new bbd1140ebd5 CAMEL-19304: camel-jpa implement paging (#9970)
bbd1140ebd5 is described below

commit bbd1140ebd5b3e135c1f66c0658ca4a75c650b9a
Author: jacekszymanski <ja...@gmail.com>
AuthorDate: Tue May 2 14:53:15 2023 +0200

    CAMEL-19304: camel-jpa implement paging (#9970)
    
    * 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 f0bd06ea639..a5642ebc6cf 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 93e8d7a4c16..8ea5e470d86 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": "jakarta.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 cf60c41e768..ad0c761777e 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 1482ac7c015..992385285c7 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 3b039b649a1..7cee7c0f25c 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
@@ -202,10 +202,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..dbd70b3b4dd
--- /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 jakarta.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 019a37f1e5d..a07f51b1169 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 jakarta.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();
+    }
+
+}