cachesConfig;
+
+}
diff --git a/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000000..ba4cb54091c21
--- /dev/null
+++ b/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,14 @@
+---
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+name: "Infinispan Cache"
+metadata:
+ keywords:
+ - "infinispan"
+ - "cache"
+ guide: "https://quarkus.io/guides/cache-infinispan-reference"
+ categories:
+ - "data"
+ - "reactive"
+ status: "preview"
+ config:
+ - "quarkus.cache.infinispan"
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java
index d3056b4ef2c6b..3cdfc250512c6 100644
--- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java
@@ -18,11 +18,14 @@
* This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always
* be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally
* {@link CacheResult}.
+ *
+ * @deprecated Use Infinispan Cache Extension
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
+@Deprecated(forRemoval = true)
public @interface CacheInvalidate {
/**
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java
index e25b9311ccd36..e8572eb1fda8e 100644
--- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java
@@ -18,11 +18,14 @@
* This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always
* be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally
* {@link CacheResult}.
+ *
+ * @deprecated Use Infinispan Cache Extension
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
+@Deprecated(forRemoval = true)
public @interface CacheInvalidateAll {
/**
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java
index 6c1ac7a1e5c8e..95970dd85cd26 100644
--- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java
@@ -28,10 +28,13 @@
* annotations on a single method. Caching operations will always be executed in the same order: {@link CacheInvalidateAll}
* first, then {@link CacheInvalidate} and finally {@link CacheResult}.
*
+ *
+ * @deprecated Use Infinispan Cache Extension
*/
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
+@Deprecated(forRemoval = true)
public @interface CacheResult {
/**
diff --git a/extensions/pom.xml b/extensions/pom.xml
index ccd152cf03acc..aa266acf915e5 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -150,6 +150,7 @@
infinispan-client
+ infinispan-cache
caffeine
diff --git a/integration-tests/infinispan-cache/pom.xml b/integration-tests/infinispan-cache/pom.xml
new file mode 100644
index 0000000000000..670df99e995ea
--- /dev/null
+++ b/integration-tests/infinispan-cache/pom.xml
@@ -0,0 +1,144 @@
+
+
+ 4.0.0
+
+ io.quarkus
+ quarkus-integration-tests-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-integration-test-infinispan-cache
+ Quarkus - Integration Tests - Infinispan Cache
+
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.quarkus
+ quarkus-rest-client-jackson
+
+
+ io.quarkus
+ quarkus-infinispan-cache
+
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+ io.quarkus
+ quarkus-infinispan-cache-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+ io.quarkus
+ quarkus-rest-jackson-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+ io.quarkus
+ quarkus-rest-client-jackson-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ true
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+
+ test-infinispan
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+
+
+ maven-failsafe-plugin
+
+ false
+
+
+
+
+
+
+
+
diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java
new file mode 100644
index 0000000000000..326c01a3c7b45
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java
@@ -0,0 +1,53 @@
+package io.quarkus.it.cache.infinispan;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+
+import org.infinispan.protostream.GeneratedSchema;
+import org.infinispan.protostream.annotations.Proto;
+import org.infinispan.protostream.annotations.ProtoSchema;
+
+import io.quarkus.cache.CacheInvalidateAll;
+import io.quarkus.cache.CacheKey;
+import io.quarkus.cache.CacheResult;
+
+@Path("/expensive-resource")
+public class ExpensiveResource {
+
+ private final AtomicInteger invocations = new AtomicInteger(0);
+
+ @GET
+ @Path("/{keyElement1}/{keyElement2}/{keyElement3}")
+ @CacheResult(cacheName = "expensiveResourceCache")
+ public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1,
+ @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3,
+ @QueryParam("foo") String foo) {
+ invocations.incrementAndGet();
+ return new ExpensiveResponse(keyElement1 + " " + keyElement2 + " " + keyElement3 + " too!");
+ }
+
+ @POST
+ @CacheInvalidateAll(cacheName = "expensiveResourceCache")
+ public void invalidateAll() {
+
+ }
+
+ @GET
+ @Path("/invocations")
+ public int getInvocations() {
+ return invocations.get();
+ }
+
+ @Proto
+ public record ExpensiveResponse(String result) {
+ }
+
+ @ProtoSchema(includeClasses = { ExpensiveResponse.class })
+ interface Schema extends GeneratedSchema {
+ }
+}
diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java
new file mode 100644
index 0000000000000..09d8ea5978660
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java
@@ -0,0 +1,88 @@
+package io.quarkus.it.cache.infinispan;
+
+import java.util.Set;
+import java.util.function.Function;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.HttpHeaders;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.jboss.resteasy.reactive.RestPath;
+import org.jboss.resteasy.reactive.RestQuery;
+import org.jboss.resteasy.reactive.RestResponse;
+
+import io.quarkus.runtime.BlockingOperationControl;
+import io.smallrye.mutiny.Uni;
+
+@Path("rest-client")
+public class RestClientResource {
+
+ @RestClient
+ SunriseRestClient sunriseRestClient;
+
+ @Inject
+ HttpHeaders headers; // used in order to make sure that @RequestScoped beans continue to work despite the cache coming into play
+
+ @GET
+ @Path("time/{city}")
+ public RestResponse getSunriseTime(@RestPath String city, @RestQuery String date) {
+ Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet();
+ String restResponse = sunriseRestClient.getSunriseTime(city, date);
+ Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet();
+ return RestResponse.ResponseBuilder
+ .ok(restResponse)
+ .header("before", String.join(", ", incomingHeadersBeforeRestCall))
+ .header("after", String.join(", ", incomingHeadersAfterRestCall))
+ .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed())
+ .build();
+ }
+
+ @GET
+ @Path("async/time/{city}")
+ public Uni> getAsyncSunriseTime(@RestPath String city, @RestQuery String date) {
+ Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet();
+ return sunriseRestClient.getAsyncSunriseTime(city, date).onItem().transform(new Function<>() {
+ @Override
+ public RestResponse apply(String restResponse) {
+ Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet();
+ return RestResponse.ResponseBuilder
+ .ok(restResponse)
+ .header("before", String.join(", ", incomingHeadersBeforeRestCall))
+ .header("after", String.join(", ", incomingHeadersAfterRestCall))
+ .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed())
+ .build();
+ }
+ });
+ }
+
+ @GET
+ @Path("invocations")
+ public Integer getSunriseTimeInvocations() {
+ return sunriseRestClient.getSunriseTimeInvocations();
+ }
+
+ @DELETE
+ @Path("invalidate/{city}")
+ public Uni> invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey,
+ @RestQuery String date) {
+ return sunriseRestClient.invalidate(city, notPartOfTheCacheKey, date).onItem().transform(
+ new Function<>() {
+ @Override
+ public RestResponse apply(Void unused) {
+ return RestResponse.ResponseBuilder. create(RestResponse.Status.NO_CONTENT)
+ .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed())
+ .header("incoming", String.join(", ", headers.getRequestHeaders().keySet()))
+ .build();
+ }
+ });
+ }
+
+ @DELETE
+ @Path("invalidate")
+ public void invalidateAll() {
+ sunriseRestClient.invalidateAll();
+ }
+}
diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java
new file mode 100644
index 0000000000000..f8ee388c9c2ee
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java
@@ -0,0 +1,52 @@
+package io.quarkus.it.cache.infinispan;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.jboss.resteasy.reactive.RestPath;
+import org.jboss.resteasy.reactive.RestQuery;
+
+import io.quarkus.cache.CacheInvalidate;
+import io.quarkus.cache.CacheInvalidateAll;
+import io.quarkus.cache.CacheKey;
+import io.quarkus.cache.CacheResult;
+import io.smallrye.mutiny.Uni;
+
+@RegisterRestClient
+@Path("sunrise")
+public interface SunriseRestClient {
+
+ String CACHE_NAME = "sunrise-cache";
+
+ @GET
+ @Path("time/{city}")
+ @CacheResult(cacheName = CACHE_NAME)
+ String getSunriseTime(@RestPath String city, @RestQuery String date);
+
+ @GET
+ @Path("time/{city}")
+ @CacheResult(cacheName = CACHE_NAME)
+ Uni getAsyncSunriseTime(@RestPath String city, @RestQuery String date);
+
+ @GET
+ @Path("invocations")
+ Integer getSunriseTimeInvocations();
+
+ /*
+ * The following methods wouldn't make sense in a real-life application but it's not relevant here. We only need to check if
+ * the caching annotations work as intended with the rest-client extension.
+ */
+
+ @DELETE
+ @Path("invalidate/{city}")
+ @CacheInvalidate(cacheName = CACHE_NAME)
+ Uni invalidate(@CacheKey @RestPath String city, @RestQuery String notPartOfTheCacheKey,
+ @CacheKey @RestPath String date);
+
+ @DELETE
+ @Path("invalidate")
+ @CacheInvalidateAll(cacheName = CACHE_NAME)
+ void invalidateAll();
+}
diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java
new file mode 100644
index 0000000000000..fb21d7c9e7e91
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java
@@ -0,0 +1,41 @@
+package io.quarkus.it.cache.infinispan;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.jboss.resteasy.reactive.RestPath;
+import org.jboss.resteasy.reactive.RestQuery;
+
+@ApplicationScoped
+@Path("sunrise")
+public class SunriseRestServerResource {
+
+ private int sunriseTimeInvocations;
+
+ @GET
+ @Path("time/{city}")
+ public String getSunriseTime(@RestPath String city, @RestQuery String date) {
+ sunriseTimeInvocations++;
+ return "2020-12-20T10:15:30";
+ }
+
+ @GET
+ @Path("invocations")
+ public Integer getSunriseTimeInvocations() {
+ return sunriseTimeInvocations;
+ }
+
+ @DELETE
+ @Path("invalidate/{city}")
+ public void invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey, @RestQuery String date) {
+ // Do nothing. We only need to test the caching annotation on the client side.
+ }
+
+ @DELETE
+ @Path("invalidate")
+ public void invalidateAll() {
+ // Do nothing. We only need to test the caching annotation on the client side.
+ }
+}
diff --git a/integration-tests/infinispan-cache/src/main/resources/application.properties b/integration-tests/infinispan-cache/src/main/resources/application.properties
new file mode 100644
index 0000000000000..25525ca3c8e4f
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/main/resources/application.properties
@@ -0,0 +1 @@
+io.quarkus.it.cache.infinispan.SunriseRestClient/mp-rest/url=${test.url}
diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java
new file mode 100644
index 0000000000000..de4fb795de42b
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.it.cache.infinispan;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+public class CacheIT extends CacheTest {
+}
diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java
new file mode 100644
index 0000000000000..b5a73d4558a6f
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java
@@ -0,0 +1,33 @@
+package io.quarkus.it.cache.infinispan;
+
+import static io.restassured.RestAssured.when;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class CacheTest {
+
+ @Test
+ public void testCache() {
+ runExpensiveRequest();
+ runExpensiveRequest();
+ runExpensiveRequest();
+ when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1"));
+
+ when()
+ .post("/expensive-resource")
+ .then()
+ .statusCode(204);
+ }
+
+ private void runExpensiveRequest() {
+ when()
+ .get("/expensive-resource/I/love/Quarkus?foo=bar")
+ .then()
+ .statusCode(200)
+ .body("result", is("I love Quarkus too!"));
+ }
+}
diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java
new file mode 100644
index 0000000000000..ccfa3c8dd5fc8
--- /dev/null
+++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java
@@ -0,0 +1,87 @@
+package io.quarkus.it.cache.infinispan;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.Headers;
+
+@QuarkusTest
+@DisplayName("Tests the integration between the infinispan cache and the rest-client extensions")
+public class InfinspanCacheClientTestCase {
+
+ private static final String CITY = "Toulouse";
+ private static final String TODAY = "2020-12-20";
+
+ @Test
+ public void test() {
+ assertInvocations("0");
+ getSunriseTimeInvocations();
+ assertInvocations("1");
+ getSunriseTimeInvocations();
+ assertInvocations("1");
+ getAsyncSunriseTimeInvocations();
+ assertInvocations("1");
+ invalidate();
+ getSunriseTimeInvocations();
+ assertInvocations("2");
+ invalidateAll();
+ getSunriseTimeInvocations();
+ assertInvocations("3");
+ }
+
+ private void assertInvocations(String expectedInvocations) {
+ given()
+ .when()
+ .get("/rest-client/invocations")
+ .then()
+ .statusCode(200)
+ .body(equalTo(expectedInvocations));
+ }
+
+ private void getSunriseTimeInvocations() {
+ doGetSunriseTimeInvocations("/rest-client/time/{city}", true);
+ }
+
+ private void getAsyncSunriseTimeInvocations() {
+ doGetSunriseTimeInvocations("/rest-client/async/time/{city}", false);
+ }
+
+ private void doGetSunriseTimeInvocations(String path, Boolean blockingAllowed) {
+ Headers headers = given()
+ .queryParam("date", TODAY)
+ .when()
+ .get(path, CITY)
+ .then()
+ .statusCode(200)
+ .extract().headers();
+ assertEquals(headers.get("before").getValue(), headers.get("after").getValue());
+ assertEquals(blockingAllowed.toString(), headers.get("blockingAllowed").getValue());
+ }
+
+ private void invalidate() {
+ Headers headers = given()
+ .queryParam("date", TODAY)
+ .queryParam("notPartOfTheCacheKey", "notPartOfTheCacheKey")
+ .when()
+ .delete("/rest-client/invalidate/{city}", CITY)
+ .then()
+ .statusCode(204)
+ .extract().headers();
+ assertNotNull(headers.get("incoming").getValue());
+ assertEquals("false", headers.get("blockingAllowed").getValue());
+ }
+
+ private void invalidateAll() {
+ given()
+ .when()
+ .delete("/rest-client/invalidate")
+ .then()
+ .statusCode(204);
+ }
+}
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index a17b168d5cd73..e963d51266bd8 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -176,6 +176,7 @@
hibernate-validator-resteasy-reactive
common-jpa-entities
infinispan-client
+ infinispan-cache
devtools
devtools-registry-client
gradle