diff --git a/README.md b/README.md
index 3190962d8..dc1411ac2 100644
--- a/README.md
+++ b/README.md
@@ -1139,6 +1139,12 @@ It covers different usages:
3. from a blocking endpoint
4. from a reactive endpoint
+### `cache/caffeine-resteasy`
+
+Verifies the `quarkus-cache` extension with RESTEasy.
+It covers different usages:
+1. `@CacheResult`, `@CacheInvalidate` and `@CacheInvalidateAll` set in RESTEasy client
+
### `cache/infinispan`
Verifies the `quarkus-infinispan-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`.
It covers different usages:
diff --git a/cache/caffeine-resteasy/pom.xml b/cache/caffeine-resteasy/pom.xml
new file mode 100644
index 000000000..66032613b
--- /dev/null
+++ b/cache/caffeine-resteasy/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ cache-caffeine-resteasy
+ jar
+ Quarkus QE TS: Cache: Caffeine Resteasy
+
+
+ io.quarkus
+ quarkus-resteasy
+
+
+ io.quarkus
+ quarkus-resteasy-client-jackson
+
+
+ io.quarkus
+ quarkus-resteasy-client-jaxb
+
+
+ io.quarkus
+ quarkus-cache
+
+
+
diff --git a/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/ClientBookResource.java b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/ClientBookResource.java
new file mode 100644
index 000000000..4be4cd190
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/ClientBookResource.java
@@ -0,0 +1,45 @@
+package io.quarkus.ts.cache.caffeine.restclient;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+
+import io.quarkus.ts.cache.caffeine.restclient.types.Book;
+
+@Path("/client/book")
+public class ClientBookResource {
+
+ @Inject
+ @RestClient
+ RestInterface restInterface;
+
+ @GET
+ @Path("/xml-cache")
+ @Produces(MediaType.APPLICATION_XML)
+ public Book getAsXmlWithCache() {
+ return restInterface.getAsXmlWithCache();
+ }
+
+ @GET
+ @Path("/json-cache")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Book getAsJsonWithCache() {
+ return restInterface.getAsJsonWithCache();
+ }
+
+ @GET
+ @Path("/invalidate-xml")
+ public String invalidateXml() {
+ return restInterface.invalidateXml();
+ }
+
+ @GET
+ @Path("/invalidate-json")
+ public String invalidateJson() {
+ return restInterface.invalidateJson();
+ }
+}
diff --git a/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/RestInterface.java b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/RestInterface.java
new file mode 100644
index 000000000..d0ec2b8f6
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/RestInterface.java
@@ -0,0 +1,43 @@
+package io.quarkus.ts.cache.caffeine.restclient;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+import io.quarkus.cache.CacheInvalidate;
+import io.quarkus.cache.CacheInvalidateAll;
+import io.quarkus.cache.CacheResult;
+import io.quarkus.ts.cache.caffeine.restclient.types.Book;
+
+@RegisterRestClient
+@Path("/book")
+@RegisterClientHeaders
+public interface RestInterface {
+
+ @GET
+ @Path("/xml-cache")
+ @CacheResult(cacheName = "xml")
+ @Produces(MediaType.APPLICATION_XML)
+ Book getAsXmlWithCache();
+
+ @GET
+ @Path("/json-cache")
+ @CacheResult(cacheName = "json")
+ @Produces(MediaType.APPLICATION_JSON)
+ Book getAsJsonWithCache();
+
+ @GET
+ @Path("/xml-cache-invalidate")
+ @Produces(MediaType.TEXT_PLAIN)
+ @CacheInvalidateAll(cacheName = "xml")
+ String invalidateXml();
+
+ @GET
+ @Path("/json-cache-invalidate")
+ @CacheInvalidate(cacheName = "json")
+ String invalidateJson();
+}
diff --git a/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/Book.java b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/Book.java
new file mode 100644
index 000000000..c8b1f10ff
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/Book.java
@@ -0,0 +1,24 @@
+package io.quarkus.ts.cache.caffeine.restclient.types;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+public class Book {
+
+ private String title;
+
+ public Book() {
+ }
+
+ public Book(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+}
diff --git a/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsJsonResource.java b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsJsonResource.java
new file mode 100644
index 000000000..ba3fa23b6
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsJsonResource.java
@@ -0,0 +1,33 @@
+package io.quarkus.ts.cache.caffeine.restclient.types;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/book")
+public class BookAsJsonResource {
+
+ public static int counter = 0;
+
+ @GET
+ @Path("/json-cache")
+ @Produces(MediaType.APPLICATION_JSON)
+ public String getCache() throws InterruptedException {
+ counter++;
+ return "{\"title\":\"Title in Json with counter equal to " + counter + "\"}";
+ }
+
+ @GET
+ @Path("/json-cache-invalidate")
+ public String invalidateCache() {
+ return "json cache was invalidated";
+ }
+
+ @GET
+ @Path("/reset-counter-json")
+ public String resetCounter() {
+ counter = 0;
+ return "Counter reset";
+ }
+}
diff --git a/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsXmlResource.java b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsXmlResource.java
new file mode 100644
index 000000000..eb3299007
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/java/io/quarkus/ts/cache/caffeine/restclient/types/BookAsXmlResource.java
@@ -0,0 +1,34 @@
+package io.quarkus.ts.cache.caffeine.restclient.types;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/book")
+public class BookAsXmlResource {
+
+ private static int counter = 0;
+
+ @GET
+ @Path("/xml-cache")
+ @Produces(MediaType.APPLICATION_XML)
+ public String helloCache() throws InterruptedException {
+ counter++;
+ return "Title in Xml with counter equal to " + counter + "";
+ }
+
+ @GET
+ @Path("/xml-cache-invalidate")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String invalidateCache() {
+ return "xml cache was invalidated";
+ }
+
+ @GET
+ @Path("/reset-counter-xml")
+ public String resetCounter() {
+ counter = 0;
+ return "Counter reset";
+ }
+}
diff --git a/cache/caffeine-resteasy/src/main/resources/application.properties b/cache/caffeine-resteasy/src/main/resources/application.properties
new file mode 100644
index 000000000..02bf13d3b
--- /dev/null
+++ b/cache/caffeine-resteasy/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+io.quarkus.ts.cache.caffeine.restclient.RestInterface/mp-rest/url=http://localhost:${quarkus.http.port}
+cache.expire.time=4000
+cache.expire.json.time=2000
+quarkus.cache.caffeine.expire-after-write=${cache.expire.time}ms
+quarkus.cache.caffeine."json".expire-after-write=${cache.expire.json.time}ms
diff --git a/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/OpenShiftRestClientWithCacheIT.java b/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/OpenShiftRestClientWithCacheIT.java
new file mode 100644
index 000000000..1a6af4372
--- /dev/null
+++ b/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/OpenShiftRestClientWithCacheIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.cache.caffeine.restclient;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+public class OpenShiftRestClientWithCacheIT extends RestClientWithCacheIT {
+}
diff --git a/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/RestClientWithCacheIT.java b/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/RestClientWithCacheIT.java
new file mode 100644
index 000000000..792c389ff
--- /dev/null
+++ b/cache/caffeine-resteasy/src/test/java/io/quarkus/ts/cache/caffeine/restclient/RestClientWithCacheIT.java
@@ -0,0 +1,115 @@
+package io.quarkus.ts.cache.caffeine.restclient;
+
+import static org.hamcrest.CoreMatchers.is;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+public class RestClientWithCacheIT {
+
+ @QuarkusApplication
+ static RestService app = new RestService();
+
+ @BeforeEach
+ public void resetCounterAndInvalidateCache() {
+ invalidateCache("xml");
+ invalidateCache("json");
+ resetCounter("xml");
+ resetCounter("json");
+ }
+
+ @Test
+ @Tag("QUARKUS-5068")
+ public void shouldGetBookFromRestClientXmlWithCache() {
+ // Check if request is cached
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 1");
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 1");
+
+ // Invalidate cache
+ invalidateCache("xml");
+
+ // The request shouldn't be cached and counter should be different then previous requests
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 2");
+ }
+
+ @Test
+ @Tag("QUARKUS-5068")
+ public void shouldGetBookFromRestClientJsonWithCache() {
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 1\"}");
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 1\"}");
+
+ // Invalidate cache
+ invalidateCache("json");
+
+ // The request shouldn't be cached and counter should be different then previous requests
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 2\"}");
+ }
+
+ @Test
+ public void testExpireAfterWritePropertyWithSpecificName() throws InterruptedException {
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 1\"}");
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 1\"}");
+
+ // Sleep same time as the cache expiration time + adding 100ms as safe buffer
+ Thread.sleep(ConfigProvider.getConfig().getValue("cache.expire.json.time", Integer.class) + 100);
+
+ // The request shouldn't be cached and counter should be different then previous requests
+ sendRequestAndReturnResponseTime("/client/book/json-cache",
+ "{\"title\":\"Title in Json with counter equal to 2\"}");
+ }
+
+ @Test
+ public void testExpireAfterWritePropertyWith() throws InterruptedException {
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 1");
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 1");
+
+ // Sleep same time as the cache expiration time + adding 100ms as safe buffer
+ Thread.sleep(ConfigProvider.getConfig().getValue("cache.expire.time", Integer.class) + 100);
+
+ // The request shouldn't be cached and counter should be different then previous requests
+ sendRequestAndReturnResponseTime("/client/book/xml-cache",
+ "Title in Xml with counter equal to 2");
+ }
+
+ public void sendRequestAndReturnResponseTime(String path, String expectedBody) {
+ app.given()
+ .get(path)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(is(expectedBody));
+ }
+
+ public void invalidateCache(String cacheName) {
+ app.given()
+ .get("/client/book/invalidate-" + cacheName)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(is(cacheName + " cache was invalidated"));
+ }
+
+ public void resetCounter(String fileType) {
+ app.given()
+ .get("/book/reset-counter-" + fileType)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .body(is("Counter reset"));
+ }
+}
diff --git a/pom.xml b/pom.xml
index fd6de6a70..7e5b26297 100644
--- a/pom.xml
+++ b/pom.xml
@@ -449,6 +449,7 @@
env-info
cache/caffeine
+ cache/caffeine-resteasy
cache/redis
cache/infinispan
infinispan-client