diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc
index ca618d298605d..5416de29092d8 100644
--- a/docs/src/main/asciidoc/infinispan-client.adoc
+++ b/docs/src/main/asciidoc/infinispan-client.adoc
@@ -442,6 +442,43 @@ for Kubernetes deployments, Infinispan Console,
https://infinispan.org/docs/stable/titles/rest/rest.html#rest_v2_protobuf_schemas[REST API] or the
https://infinispan.org/docs/stable/titles/encoding/encoding.html#registering-sci-remote-caches_marshalling[Hot Rod Java Client].
+[#annotations-api]
+== Caching using annotations
+
+The Infinispan Client extension offers a set of annotations that can be used in a CDI managed bean to enable caching abilities with Infinispan.
+
+[WARNING]
+====
+Caching annotations are not allowed on private methods.
+They will work fine with any other access modifier including package-private (no explicit modifier).
+====
+
+=== @CacheResult
+
+Loads a method result from the cache without executing the method body whenever possible.
+
+When a method annotated with `@CacheResult` is invoked, Quarkus will compute a cache key and use it to check in the cache whether the method has been already invoked.
+Methods with multiple parameters are not allowed. For composite keys, define a Protobuf schema that will hold multiple values.
+If a value is found in the cache, it is returned and the annotated method is never actually executed.
+If no value is found, the annotated method is invoked and the returned value is stored in the cache using the computed key.
+This annotation cannot be used on a method returning `void`.
+
+[NOTE]
+====
+Infinispan Client extension is not able yet to cache `null` values unlike the Quarkus-Cache extension.
+====
+
+=== @CacheInvalidate
+
+Removes an entry from the cache.
+
+When a method annotated with `@CacheInvalidate` is invoked, Infinispan will use the method argument as a cache key to try to remove an existing entry from the cache.
+If the key does not identify any cache entry, nothing will happen.
+
+=== @CacheInvalidateAll
+
+When a method annotated with `@CacheInvalidateAll` is invoked, Infinispan will remove all entries from the cache.
+
== Querying
diff --git a/extensions/infinispan-client/deployment/pom.xml b/extensions/infinispan-client/deployment/pom.xml
index ba865f639aaaf..a294e6da65c47 100644
--- a/extensions/infinispan-client/deployment/pom.xml
+++ b/extensions/infinispan-client/deployment/pom.xml
@@ -41,6 +41,10 @@
io.quarkusquarkus-jsonp-deployment
+
+ io.quarkus
+ quarkus-mutiny-deployment
+ io.quarkus
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java
new file mode 100644
index 0000000000000..411082aa3dd5f
--- /dev/null
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/CacheNamesBuildItem.java
@@ -0,0 +1,21 @@
+package io.quarkus.infinispan.client.deployment;
+
+import java.util.Set;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+/**
+ * This build item is used to pass the full list of cache names from the validation step to the recording step.
+ */
+public final class CacheNamesBuildItem extends SimpleBuildItem {
+
+ private final Set names;
+
+ public CacheNamesBuildItem(Set names) {
+ this.names = names;
+ }
+
+ public Set getNames() {
+ return names;
+ }
+}
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
index 3404733cc1ac4..732376caa7f5f 100644
--- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java
@@ -56,6 +56,9 @@
import io.quarkus.infinispan.client.runtime.InfinispanClientBuildTimeConfig;
import io.quarkus.infinispan.client.runtime.InfinispanClientProducer;
import io.quarkus.infinispan.client.runtime.InfinispanRecorder;
+import io.quarkus.infinispan.client.runtime.cache.CacheInvalidateAllInterceptor;
+import io.quarkus.infinispan.client.runtime.cache.CacheInvalidateInterceptor;
+import io.quarkus.infinispan.client.runtime.cache.CacheResultInterceptor;
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;
class InfinispanClientProcessor {
@@ -85,6 +88,9 @@ InfinispanPropertiesBuildItem setup(ApplicationArchivesBuildItem applicationArch
feature.produce(new FeatureBuildItem(Feature.INFINISPAN_CLIENT));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(InfinispanClientProducer.class));
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheInvalidateAllInterceptor.class));
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheResultInterceptor.class));
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CacheInvalidateInterceptor.class));
systemProperties.produce(new SystemPropertyBuildItem("io.netty.noUnsafe", "true"));
hotDeployment.produce(new HotDeploymentWatchedFileBuildItem(META_INF + File.separator + HOTROD_CLIENT_PROPERTIES));
@@ -235,7 +241,8 @@ BeanContainerListenerBuildItem build(InfinispanRecorder recorder, InfinispanProp
@BuildStep
UnremovableBeanBuildItem ensureBeanLookupAvailable() {
return UnremovableBeanBuildItem.beanTypes(BaseMarshaller.class, EnumMarshaller.class, MessageMarshaller.class,
- RawProtobufMarshaller.class, FileDescriptorSource.class);
+ RawProtobufMarshaller.class, FileDescriptorSource.class, CacheResultInterceptor.class,
+ CacheInvalidateAllInterceptor.class);
}
@BuildStep
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java
new file mode 100644
index 0000000000000..fa20a016e445a
--- /dev/null
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/CacheDeploymentConstants.java
@@ -0,0 +1,41 @@
+package io.quarkus.infinispan.client.deployment.cache;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.jboss.jandex.DotName;
+
+import io.quarkus.infinispan.client.CacheInvalidate;
+import io.quarkus.infinispan.client.CacheInvalidateAll;
+import io.quarkus.infinispan.client.CacheResult;
+import io.quarkus.infinispan.client.runtime.cache.CacheResultInterceptor;
+import io.smallrye.mutiny.Multi;
+
+public class CacheDeploymentConstants {
+
+ // API annotations names.
+ public static final DotName CACHE_INVALIDATE_ALL = dotName(CacheInvalidateAll.class);
+ public static final DotName CACHE_INVALIDATE_ALL_LIST = dotName(CacheInvalidateAll.List.class);
+ public static final DotName CACHE_INVALIDATE = dotName(CacheInvalidate.class);
+ public static final DotName CACHE_INVALIDATE_LIST = dotName(CacheInvalidate.List.class);
+ public static final DotName CACHE_RESULT = dotName(CacheResult.class);
+ public static final List INTERCEPTOR_BINDINGS = Arrays.asList(CACHE_RESULT, CACHE_INVALIDATE,
+ CACHE_INVALIDATE_ALL);
+ public static final List INTERCEPTOR_BINDING_CONTAINERS = Arrays.asList(CACHE_INVALIDATE_LIST,
+ CACHE_INVALIDATE_ALL_LIST);
+ public static final List INTERCEPTORS = Arrays.asList(dotName(CacheResultInterceptor.class));
+
+ // MicroProfile REST Client.
+ public static final DotName REGISTER_REST_CLIENT = DotName
+ .createSimple("org.eclipse.microprofile.rest.client.inject.RegisterRestClient");
+
+ // Mutiny.
+ public static final DotName MULTI = dotName(Multi.class);
+
+ // Annotations parameters.
+ public static final String CACHE_NAME_PARAM = "cacheName";
+
+ private static DotName dotName(Class> annotationClass) {
+ return DotName.createSimple(annotationClass.getName());
+ }
+}
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java
new file mode 100644
index 0000000000000..9c6f59e0bdb30
--- /dev/null
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/ClassTargetException.java
@@ -0,0 +1,28 @@
+package io.quarkus.infinispan.client.deployment.cache.exception;
+
+import org.jboss.jandex.DotName;
+
+import io.quarkus.infinispan.client.CacheInvalidate;
+import io.quarkus.infinispan.client.CacheInvalidateAll;
+import io.quarkus.infinispan.client.CacheResult;
+
+/**
+ * This exception is thrown at build time during the validation phase if a class is annotated with
+ * {@link CacheInvalidate @CacheInvalidate}, {@link CacheInvalidateAll @CacheInvalidateAll} or
+ * {@link CacheResult @CacheResult}. These annotations are only allowed at type level for the caching
+ * interceptors from this extension.
+ */
+@SuppressWarnings("serial")
+public class ClassTargetException extends RuntimeException {
+
+ private final DotName className;
+
+ public ClassTargetException(DotName className, DotName annotationName) {
+ super("Caching annotations are not allowed on a class [class=" + className + ", annotation=" + annotationName + "]");
+ this.className = className;
+ }
+
+ public DotName getClassName() {
+ return className;
+ }
+}
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java
new file mode 100644
index 0000000000000..d04aa1677756d
--- /dev/null
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/PrivateMethodTargetException.java
@@ -0,0 +1,29 @@
+package io.quarkus.infinispan.client.deployment.cache.exception;
+
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.infinispan.client.CacheInvalidate;
+import io.quarkus.infinispan.client.CacheInvalidateAll;
+import io.quarkus.infinispan.client.CacheResult;
+
+/**
+ * This exception is thrown at build time during the validation phase if a private method is annotated with
+ * {@link CacheInvalidate @CacheInvalidate}, {@link CacheInvalidateAll @CacheInvalidateAll} or
+ * {@link CacheResult @CacheResult}.
+ */
+@SuppressWarnings("serial")
+public class PrivateMethodTargetException extends RuntimeException {
+
+ private final MethodInfo methodInfo;
+
+ public PrivateMethodTargetException(MethodInfo methodInfo, DotName annotationName) {
+ super("Caching annotations are not allowed on a private method [class=" + methodInfo.declaringClass().name()
+ + ", method=" + methodInfo.name() + ", annotation=" + annotationName + "]");
+ this.methodInfo = methodInfo;
+ }
+
+ public MethodInfo getMethodInfo() {
+ return methodInfo;
+ }
+}
diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java
new file mode 100644
index 0000000000000..0b92dc83d98ca
--- /dev/null
+++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/cache/exception/VoidReturnTypeTargetException.java
@@ -0,0 +1,25 @@
+package io.quarkus.infinispan.client.deployment.cache.exception;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.infinispan.client.CacheResult;
+
+/**
+ * This exception is thrown at build time during the validation phase if a method returning void is annotated with
+ * {@link CacheResult @CacheResult}.
+ */
+@SuppressWarnings("serial")
+public class VoidReturnTypeTargetException extends RuntimeException {
+
+ private final MethodInfo methodInfo;
+
+ public VoidReturnTypeTargetException(MethodInfo methodInfo) {
+ super("@CacheResult is not allowed on a method returning void [class=" + methodInfo.declaringClass().name()
+ + ", method=" + methodInfo.name() + "]");
+ this.methodInfo = methodInfo;
+ }
+
+ public MethodInfo getMethodInfo() {
+ return methodInfo;
+ }
+}
diff --git a/extensions/infinispan-client/runtime/pom.xml b/extensions/infinispan-client/runtime/pom.xml
index 4a8df04e0ce6b..3f75089e5463f 100644
--- a/extensions/infinispan-client/runtime/pom.xml
+++ b/extensions/infinispan-client/runtime/pom.xml
@@ -25,6 +25,10 @@
io.quarkusquarkus-caffeine
+
+ io.quarkus
+ quarkus-mutiny
+ io.quarkusquarkus-netty
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
new file mode 100644
index 0000000000000..07eb705b596a7
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java
@@ -0,0 +1,39 @@
+package io.quarkus.infinispan.client;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.enterprise.util.Nonbinding;
+import javax.interceptor.InterceptorBinding;
+
+import io.quarkus.infinispan.client.CacheInvalidate.List;
+
+/**
+ * When a method annotated with {@link CacheInvalidate} is invoked, Quarkus will use the method argument as key to try to
+ * remove an existing entry from the Infinispan cache. If the key does not identify any cache entry, nothing will happen.
+ *
+ * This annotation can be combined with multiple other caching 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}.
+ */
+@InterceptorBinding
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(List.class)
+public @interface CacheInvalidate {
+
+ /**
+ * The name of the cache.
+ */
+ @Nonbinding
+ String cacheName();
+
+ @Target({ ElementType.TYPE, ElementType.METHOD })
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface List {
+ CacheInvalidate[] value();
+ }
+}
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
new file mode 100644
index 0000000000000..2bf19c2e4bcf3
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java
@@ -0,0 +1,39 @@
+package io.quarkus.infinispan.client;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.enterprise.util.Nonbinding;
+import javax.interceptor.InterceptorBinding;
+
+import io.quarkus.infinispan.client.CacheInvalidateAll.List;
+
+/**
+ * When a method annotated with {@link CacheInvalidateAll} is invoked, Quarkus will remove all entries from the Infinispan
+ * cache.
+ *
+ * This annotation can be combined with multiple other caching 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}.
+ */
+@InterceptorBinding
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(List.class)
+public @interface CacheInvalidateAll {
+
+ /**
+ * The name of the cache.
+ */
+ @Nonbinding
+ String cacheName();
+
+ @Target({ ElementType.TYPE, ElementType.METHOD })
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface List {
+ CacheInvalidateAll[] value();
+ }
+}
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
new file mode 100644
index 0000000000000..6ff5b1352569d
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java
@@ -0,0 +1,49 @@
+package io.quarkus.infinispan.client;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.enterprise.util.Nonbinding;
+import javax.interceptor.InterceptorBinding;
+
+/**
+ * When a method annotated with {@link CacheResult} is invoked, Quarkus will use the method argument as key and use it to check
+ * in the
+ * Infinispan cache if whether the method has been already invoked. If a value is found in the cache, it is returned and the
+ * annotated method is never actually
+ * executed. If no value is found, the annotated method is invoked and the returned value is stored in the cache using the
+ * computed key.
+ *
+ * A method annotated with {@link CacheResult} is protected by a lock on cache miss mechanism. If several concurrent
+ * invocations try to retrieve a cache value from the same missing key, the method will only be invoked once. The first
+ * concurrent invocation will trigger the method invocation while the subsequent concurrent invocations will wait for the end
+ * of the method invocation to get the cached result. The {@code lockTimeout} parameter can be used to interrupt the lock after
+ * a given delay. The lock timeout is disabled by default, meaning the lock is never interrupted. See the parameter Javadoc for
+ * more details.
+ *
+ * This annotation cannot be used on a method returning {@code void}. It can be combined with multiple other caching
+ * 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}.
+ *
+ */
+@InterceptorBinding
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CacheResult {
+
+ /**
+ * The name of the cache.
+ */
+ @Nonbinding
+ String cacheName();
+
+ /**
+ * Delay in milliseconds before the lock on cache miss is interrupted. If such interruption happens, the cached method will
+ * be invoked and its result will be returned without being cached. A value of {@code 0} (which is the default one) means
+ * that the lock timeout is disabled.
+ */
+ @Nonbinding
+ long lockTimeout() default 0;
+}
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java
new file mode 100644
index 0000000000000..b61a7ca51af13
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptionContext.java
@@ -0,0 +1,21 @@
+package io.quarkus.infinispan.client.runtime.cache;
+
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class CacheInterceptionContext {
+
+ private final List interceptorBindings;
+
+ public CacheInterceptionContext(List interceptorBindings) {
+ Objects.requireNonNull(interceptorBindings);
+ this.interceptorBindings = Collections.unmodifiableList(interceptorBindings);
+ }
+
+ public List getInterceptorBindings() {
+ return interceptorBindings;
+ }
+
+}
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java
new file mode 100644
index 0000000000000..9279f51b3ee37
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInterceptor.java
@@ -0,0 +1,95 @@
+package io.quarkus.infinispan.client.runtime.cache;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javax.inject.Inject;
+import javax.interceptor.Interceptor.Priority;
+import javax.interceptor.InvocationContext;
+
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.commons.CacheException;
+import org.jboss.logging.Logger;
+
+import io.quarkus.arc.runtime.InterceptorBindings;
+import io.smallrye.mutiny.Uni;
+
+public abstract class CacheInterceptor {
+
+ public static final int BASE_PRIORITY = Priority.PLATFORM_BEFORE;
+
+ private static final Logger LOGGER = Logger.getLogger(CacheInterceptor.class);
+ private static final String PERFORMANCE_WARN_MSG = "Cache key resolution based on reflection calls. Please create a GitHub issue in the Quarkus repository, the maintainers might be able to improve your application performance.";
+
+ @Inject
+ RemoteCacheManager cacheManager;
+
+ /*
+ * The interception is almost always managed by Arc in a Quarkus application. In such a case, we want to retrieve the
+ * interceptor bindings stored by Arc in the invocation context data (very good performance-wise). But sometimes the
+ * interception is managed by another CDI interceptors implementation. It can happen for example while using caching
+ * annotations on a MicroProfile REST Client method. In that case, we have no other choice but to rely on reflection (with
+ * underlying synchronized blocks which are bad for performances) to retrieve the interceptor bindings.
+ */
+ protected CacheInterceptionContext getInterceptionContext(InvocationContext invocationContext,
+ Class interceptorBindingClass) {
+ return getArcCacheInterceptionContext(invocationContext, interceptorBindingClass)
+ .orElseGet(new Supplier>() {
+ @Override
+ public CacheInterceptionContext get() {
+ return getNonArcCacheInterceptionContext(invocationContext, interceptorBindingClass);
+ }
+ });
+ }
+
+ private Optional> getArcCacheInterceptionContext(
+ InvocationContext invocationContext, Class interceptorBindingClass) {
+ Set bindings = InterceptorBindings.getInterceptorBindings(invocationContext);
+ if (bindings == null) {
+ LOGGER.trace("Interceptor bindings not found in ArC");
+ // This should only happen when the interception is not managed by Arc.
+ return Optional.empty();
+ }
+ List interceptorBindings = new ArrayList<>();
+ for (Annotation binding : bindings) {
+ if (interceptorBindingClass.isInstance(binding)) {
+ interceptorBindings.add(cast(binding, interceptorBindingClass));
+ }
+ }
+ return Optional.of(new CacheInterceptionContext<>(interceptorBindings));
+ }
+
+ private CacheInterceptionContext getNonArcCacheInterceptionContext(
+ InvocationContext invocationContext, Class interceptorBindingClass) {
+ LOGGER.trace("Retrieving interceptor bindings using reflection");
+ List interceptorBindings = new ArrayList<>();
+ return new CacheInterceptionContext<>(interceptorBindings);
+ }
+
+ @SuppressWarnings("unchecked")
+ private T cast(Annotation annotation, Class interceptorBindingClass) {
+ return (T) annotation;
+ }
+
+ protected Object getCacheKey(RemoteCache cache, Object[] methodParameterValues) {
+ if (methodParameterValues == null || methodParameterValues.length == 0) {
+ // If the intercepted method doesn't have any parameter, raise an exception.
+ throw new CacheException("Unable to cache without a key");
+ } else if (methodParameterValues.length == 1) {
+ // If the intercepted method has exactly one parameter, then this parameter will be used as the cache key.
+ return methodParameterValues[0];
+ } else {
+ // Protobuf type must be used
+ return new RuntimeException("A single parameter is needed. Create a Protobuf schema to create a Composite Key.");
+ }
+ }
+
+ protected static boolean isUniReturnType(InvocationContext invocationContext) {
+ return Uni.class.isAssignableFrom(invocationContext.getMethod().getReturnType());
+ }
+}
diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java
new file mode 100644
index 0000000000000..18dafbf25de7e
--- /dev/null
+++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/cache/CacheInvalidateAllInterceptor.java
@@ -0,0 +1,78 @@
+package io.quarkus.infinispan.client.runtime.cache;
+
+import java.util.function.Function;
+
+import javax.annotation.Priority;
+import javax.interceptor.AroundInvoke;
+import javax.interceptor.Interceptor;
+import javax.interceptor.InvocationContext;
+
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.commons.CacheException;
+import org.jboss.logging.Logger;
+
+import io.quarkus.infinispan.client.CacheInvalidateAll;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+
+@CacheInvalidateAll(cacheName = "") // The `cacheName` attribute is @Nonbinding.
+@Interceptor
+@Priority(CacheInterceptor.BASE_PRIORITY)
+public class CacheInvalidateAllInterceptor extends CacheInterceptor {
+
+ private static final Logger LOGGER = Logger.getLogger(CacheInvalidateAllInterceptor.class);
+ private static final String INTERCEPTOR_BINDINGS_ERROR_MSG = "The Quarkus Infinispan Client extension is not working properly (CacheInvalidateAll interceptor bindings retrieval failed), please create a GitHub issue in the Quarkus repository to help the maintainers fix this bug";
+
+ @AroundInvoke
+ public Object intercept(InvocationContext invocationContext) throws Exception {
+ CacheInterceptionContext interceptionContext = getInterceptionContext(invocationContext,
+ CacheInvalidateAll.class);
+ if (interceptionContext.getInterceptorBindings().isEmpty()) {
+ // This should never happen.
+ LOGGER.warn(INTERCEPTOR_BINDINGS_ERROR_MSG);
+ return invocationContext.proceed();
+ } else if (isUniReturnType(invocationContext)) {
+ return invalidateAllNonBlocking(invocationContext, interceptionContext);
+ } else {
+ return invalidateAllBlocking(invocationContext, interceptionContext);
+ }
+ }
+
+ private Object invalidateAllNonBlocking(InvocationContext invocationContext,
+ CacheInterceptionContext interceptionContext) {
+ LOGGER.trace("Invalidating all cache entries in a non-blocking way");
+ return Multi.createFrom().iterable(interceptionContext.getInterceptorBindings())
+ .onItem().transformToUniAndMerge(new Function>() {
+ @Override
+ public Uni apply(CacheInvalidateAll binding) {
+ return invalidateAll(binding);
+ }
+ })
+ .onItem().ignoreAsUni()
+ .onItem().transformToUni(new Function
+
+ io.quarkus
+ quarkus-resteasy-jackson
+
@@ -154,6 +158,19 @@
+
+ io.quarkus
+ quarkus-resteasy-jackson-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+ io.quarkusquarkus-smallrye-health-deployment
diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java
new file mode 100644
index 0000000000000..80098f910b337
--- /dev/null
+++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/BookStoreService.java
@@ -0,0 +1,52 @@
+package io.quarkus.it.infinispan.client;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import io.quarkus.infinispan.client.CacheInvalidate;
+import io.quarkus.infinispan.client.CacheInvalidateAll;
+import io.quarkus.infinispan.client.CacheResult;
+
+@Path("/books")
+public class BookStoreService {
+
+ @GET
+ @Path("{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @CacheResult(cacheName = "books")
+ public Book book(@PathParam("id") String id) {
+ return new Book("computed book", "desc", 2022,
+ Collections.singleton(new Author("Wiwi", "B")), Type.FANTASY, new BigDecimal("100.99"));
+
+ }
+
+ @GET
+ @Path("{id}/{extra}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @CacheResult(cacheName = "books")
+ public Book bookWithTwoParams(@PathParam("id") String id, @PathParam("extra") String extra) {
+ return null;
+ }
+
+ @DELETE
+ @Path("{id}")
+ @Produces(MediaType.TEXT_PLAIN)
+ @CacheInvalidate(cacheName = "books")
+ public String remove(@PathParam("id") String id) {
+ return "Nothing to invalidate";
+ }
+
+ @DELETE
+ @Produces(MediaType.TEXT_PLAIN)
+ @CacheInvalidateAll(cacheName = "books")
+ public String removeAll() {
+ return "Invalidate all not needed";
+ }
+}
diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java
index bec47ec793c33..25d5e5e30c8c2 100644
--- a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java
+++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java
@@ -65,6 +65,10 @@ public class TestServlet {
@Remote("magazine")
RemoteCache magazineCache;
+ @Inject
+ @Remote("books")
+ RemoteCache books;
+
@Inject
CounterManager counterManager;
@@ -120,6 +124,14 @@ public void resultUpdated(String key, Book value) {
magazineCache.put("popular-time", new Magazine("TIME", YearMonth.of(1997, 4),
Arrays.asList("Yep, I'm gay", "Backlash against HMOS", "False Hope on Breast Cancer?")));
+ books.put("hp-1", new Book("Harry Potter and the Philosopher's Stone", "After murdering Harry's parents...", 1997,
+ Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99")));
+ books.put("hp-2", new Book("Harry Potter and the Chamber of Secrets", "After a summer spent with the Dursleys...", 1998,
+ Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99")));
+ books.put("hp-3",
+ new Book("Harry Potter and the Prisoner of Azkaban", "Harry ends another insufferable summer...", 1999,
+ Collections.singleton(new Author("J.K", "Rowling")), Type.FANTASY, new BigDecimal("20.99")));
+
log.info("Inserted values");
waitUntilStarted.countDown();
diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java
index fd9e69c2b4c46..2d8ab0526b13a 100644
--- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java
+++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java
@@ -1,5 +1,6 @@
package io.quarkus.it.infinispan.client;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -18,13 +19,11 @@ public class InfinispanClientFunctionalityTest {
@Test
public void testGetAllKeys() {
- System.out.println("Running getAllKeys test");
RestAssured.when().get("/test").then().body(is("[book1, book2]"));
}
@Test
public void testQuery() {
- System.out.println("Running query test");
RestAssured.when().get("/test/query/So").then().body(is("[Son Martin]"));
RestAssured.when().get("/test/query/org").then().body(is("[George Martin]"));
RestAssured.when().get("/test/query/o").then().body(is("[George Martin,Son Martin]"));
@@ -32,7 +31,6 @@ public void testQuery() {
@Test
public void testIckleQuery() {
- System.out.println("Running ickleQuery test");
RestAssured.when().get("/test/icklequery/So").then().body(is("[Son Martin]"));
RestAssured.when().get("/test/icklequery/org").then().body(is("[George Martin]"));
RestAssured.when().get("/test/icklequery/o").then().body(is("[George Martin,Son Martin]"));
@@ -40,7 +38,6 @@ public void testIckleQuery() {
@Test
public void testCounterIncrement() {
- System.out.println("Running counterIncrement test");
String initialValue = RestAssured.when().get("test/incr/somevalue").body().print();
String nextValue = RestAssured.when().get("test/incr/somevalue").body().print();
assertEquals(Integer.parseInt(initialValue) + 1, Integer.parseInt(nextValue));
@@ -48,19 +45,59 @@ public void testCounterIncrement() {
@Test
public void testCQ() {
- System.out.println("Running CQ test");
RestAssured.when().get("/test/cq").then().body(is("2023"));
}
@Test
public void testNearCacheInvalidation() {
- System.out.println("Running nearCacheInvalidation test");
RestAssured.when().get("/test/nearcache").then().body(is("worked"));
}
@Test
public void testQueryWithCustomMarshaller() {
- System.out.println("Running query with custom marshaller test");
RestAssured.when().get("/test/magazinequery/IM").then().body(is("[TIME:1923-03,TIME:1997-04]"));
}
+
+ @Test
+ public void testCacheAnnotations() {
+ RestAssured.when().get("/books/hp-1")
+ .then()
+ .body(containsString("Philosopher's Stone"));
+
+ RestAssured.when().get("/books/hp-2")
+ .then()
+ .body(containsString("Chamber of Secrets"));
+
+ RestAssured.when().get("/books/hp-3")
+ .then()
+ .body(containsString("Prisoner of Azkaban"));
+
+ RestAssured.when().get("/books/hp-4")
+ .then()
+ .body(containsString("computed book"));
+
+ RestAssured.when().get("/books/hp-3/extra-params")
+ .then().statusCode(500);
+
+ RestAssured.when().delete("/books/hp-1")
+ .then()
+ .statusCode(200);
+
+ RestAssured.when().get("/books/hp-1")
+ .then()
+ .body(containsString("computed book"));
+
+ RestAssured.when().delete("/books")
+ .then()
+ .statusCode(200);
+
+ RestAssured.when().get("/books/hp-2")
+ .then()
+ .body(containsString("computed book"));
+
+ RestAssured.when().get("/books/hp-3")
+ .then()
+ .body(containsString("computed book"));
+
+ }
}
diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java
index f88b26a329f3f..7bd425752abfe 100644
--- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java
+++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanServerTestResource.java
@@ -32,7 +32,7 @@ public Map start() {
configurationBuilder);
ecm.defineConfiguration("magazine", configurationBuilder.build());
-
+ ecm.createCache("books", configurationBuilder.build());
// Client connects to a non default port
final HotRodServerConfigurationBuilder hotRodServerConfigurationBuilder = new HotRodServerConfigurationBuilder();
hotRodServerConfigurationBuilder.adminOperationsHandler(new EmbeddedServerAdminOperationHandler());