You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gn...@apache.org on 2026/08/27 13:01:57 UTC
(camel) branch main updated: CAMEL-24463: Introduce KeyValueRepository SPI + adapt state-store component
This is an automated email from the ASF dual-hosted git repository.
gnodet 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 1d70c58d6269 CAMEL-24463: Introduce KeyValueRepository SPI + adapt state-store component
1d70c58d6269 is described below
commit 1d70c58d62692723cc0aed88ac69869c9445f0be
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Thu Aug 27 15:01:50 2026 +0200
CAMEL-24463: Introduce KeyValueRepository SPI + adapt state-store component
Introduces a unified KeyValueRepository SPI in camel-api with:
- MemoryKeyValueRepository (ConcurrentHashMap + lazy TTL)
- KeyValueIdempotentRepository adapter (idempotent: prefix)
- KeyValueAggregationRepository adapter (aggregate: prefix)
- Auto-discovery from registry for Aggregator and IdempotentConsumer EIPs
- State-store component adapted to use KeyValueRepository directly
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
bom/camel-bom/pom.xml | 15 --
catalog/camel-allcomponents/pom.xml | 15 --
.../org/apache/camel/catalog/beans.properties | 1 +
.../catalog/beans/MemoryKeyValueRepository.json | 15 ++
.../camel/catalog/components/state-store.json | 4 +-
.../camel/catalog/docs/state-store-component.adoc | 271 ++++-----------------
.../org/apache/camel/catalog/others.properties | 3 -
.../camel/catalog/others/state-store-caffeine.json | 14 --
.../catalog/others/state-store-infinispan.json | 14 --
.../camel/catalog/others/state-store-redis.json | 14 --
.../camel-state-store-caffeine/pom.xml | 64 -----
.../services/org/apache/camel/other.properties | 7 -
.../generated/resources/state-store-caffeine.json | 14 --
.../caffeine/CaffeineStateStoreBackend.java | 133 ----------
.../caffeine/CaffeineStateStoreBackendTest.java | 198 ---------------
.../caffeine/CaffeineStateStorePropertiesTest.java | 95 --------
.../camel-state-store-infinispan/pom.xml | 71 ------
.../services/org/apache/camel/other.properties | 7 -
.../resources/state-store-infinispan.json | 14 --
.../infinispan/InfinispanStateStoreBackend.java | 135 ----------
.../infinispan/InfinispanStateStoreBackendIT.java | 240 ------------------
.../InfinispanStateStorePropertiesIT.java | 133 ----------
.../camel-state-store-redis/pom.xml | 71 ------
.../services/org/apache/camel/other.properties | 7 -
.../src/generated/resources/state-store-redis.json | 14 --
.../statestore/redis/RedisStateStoreBackend.java | 132 ----------
.../statestore/redis/RedisStateStoreBackendIT.java | 204 ----------------
.../redis/RedisStateStorePropertiesIT.java | 83 -------
.../statestore/StateStoreEndpointConfigurer.java | 4 +-
.../camel/component/statestore/state-store.json | 4 +-
.../src/main/docs/state-store-component.adoc | 271 ++++-----------------
.../statestore/InMemoryStateStoreBackend.java | 128 ----------
.../component/statestore/StateStoreBackend.java | 103 --------
.../component/statestore/StateStoreComponent.java | 35 +--
.../component/statestore/StateStoreEndpoint.java | 11 +-
.../component/statestore/StateStoreProducer.java | 5 +-
.../statestore/StateStoreAutoDiscoveryTest.java | 16 +-
.../StateStoreMultiBackendFallbackTest.java | 12 +-
components/camel-state-store/pom.xml | 3 -
.../org/apache/camel/spi/KeyValueRepository.java | 121 +++++++++
.../processor/aggregate/AggregateProcessor.java | 13 +-
.../camel/reifier/IdempotentConsumerReifier.java | 22 +-
.../support/KeyValueAggregationRepositoryTest.java | 214 ++++++++++++++++
core/camel-support/pom.xml | 11 +
.../MemoryKeyValueRepositoryConfigurer.java | 37 +++
.../services/org/apache/camel/bean.properties | 2 +-
.../camel/bean/MemoryKeyValueRepository.json | 15 ++
...g.apache.camel.support.MemoryKeyValueRepository | 2 +
.../support/KeyValueAggregationRepository.java | 238 ++++++++++++++++++
.../support/KeyValueIdempotentRepository.java | 114 +++++++++
.../camel/support/MemoryKeyValueRepository.java | 198 +++++++++++++++
.../support/KeyValueIdempotentRepositoryTest.java | 178 ++++++++++++++
.../support/MemoryKeyValueRepositoryTest.java | 227 +++++++++++++++++
.../others/examples/json/state-store-caffeine.json | 1 -
.../examples/json/state-store-infinispan.json | 1 -
.../others/examples/json/state-store-redis.json | 1 -
.../component/ComponentsBuilderFactory.java | 3 +-
.../dsl/StateStoreComponentBuilderFactory.java | 5 +-
.../builder/endpoint/EndpointHeaderBuilders.java | 3 +-
.../builder/endpoint/StaticEndpointBuilders.java | 6 +-
.../dsl/StateStoreEndpointBuilderFactory.java | 23 +-
parent/pom.xml | 15 --
62 files changed, 1560 insertions(+), 2470 deletions(-)
diff --git a/bom/camel-bom/pom.xml b/bom/camel-bom/pom.xml
index d70d54289386..d1a957d68568 100644
--- a/bom/camel-bom/pom.xml
+++ b/bom/camel-bom/pom.xml
@@ -2327,21 +2327,6 @@
<artifactId>camel-state-store</artifactId>
<version>4.23.0-SNAPSHOT</version>
</dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-caffeine</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-infinispan</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-redis</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-stax</artifactId>
diff --git a/catalog/camel-allcomponents/pom.xml b/catalog/camel-allcomponents/pom.xml
index 6fe7977a95a1..9a6aad45c5a1 100644
--- a/catalog/camel-allcomponents/pom.xml
+++ b/catalog/camel-allcomponents/pom.xml
@@ -2107,21 +2107,6 @@
<artifactId>camel-state-store</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-caffeine</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-infinispan</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-redis</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-stax</artifactId>
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
index a3b23ef4988e..d355b8bfd5e8 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
@@ -34,6 +34,7 @@ KubernetesClusterService
LoggingHttpActivityListener
MemoryAggregationRepository
MemoryIdempotentRepository
+MemoryKeyValueRepository
MongoDbIdempotentRepository
OpensearchBulkRequestAggregationStrategy
RedisAggregationRepository
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/MemoryKeyValueRepository.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/MemoryKeyValueRepository.json
new file mode 100644
index 000000000000..4630dec20634
--- /dev/null
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/MemoryKeyValueRepository.json
@@ -0,0 +1,15 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "MemoryKeyValueRepository",
+ "javaType": "org.apache.camel.support.MemoryKeyValueRepository",
+ "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+ "title": "Memory Key Value Repository",
+ "description": "An in-memory KeyValueRepository with optional TTL support.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-support",
+ "version": "4.23.0-SNAPSHOT"
+ }
+}
+
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
index d90c315d879c..470d0b127426 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json
@@ -3,7 +3,7 @@
"kind": "component",
"name": "state-store",
"title": "State Store",
- "description": "Perform key-value operations against a pluggable state store backend.",
+ "description": "Perform key-value operations against a pluggable KeyValueRepository backend.",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "cache",
@@ -37,6 +37,6 @@
"operation": { "index": 1, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "autowired": false, "secret": false, "description": "The default operation to perform" },
"ttl": { "index": 2, "kind": "parameter", "displayName": "Ttl", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0 means no expiry." },
"lazyStartProducer": { "index": 3, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
- "backend": { "index": 4, "kind": "parameter", "displayName": "Backend", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.statestore.StateStoreBackend", "deprecated": false, "autowired": false, "secret": false, "description": "The backend to use. If not set, auto-discovers a single StateStoreBackend from the registry, or falls back to an in-memory store." }
+ "backend": { "index": 4, "kind": "parameter", "displayName": "Backend", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false, "autowired": false, "secret": false, "description": "The backend to use. If not set, auto-discovers a single KeyValueRepository from the registry, or falls back to an in-memory store." }
}
}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
index c37fb9acaf18..e110ccc0bca5 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/state-store-component.adoc
@@ -2,7 +2,7 @@
:doctitle: State Store
:shortname: state-store
:artifactid: camel-state-store
-:description: Perform key-value operations against a pluggable state store backend.
+:description: Perform key-value operations against a pluggable KeyValueRepository backend.
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
@@ -12,13 +12,14 @@
*{component-header}*
-The State Store component provides a simple, unified key-value store API with pluggable backends.
-It is useful for caching, session state, and scenarios where you need a simple object store
-(similar to MuleSoft's Object Store).
+The State Store component provides a simple, unified key-value store API backed by the
+`KeyValueRepository` SPI from `camel-api`. It is useful for caching, session state, and
+scenarios where you need a simple object store (similar to MuleSoft's Object Store).
-By default, an in-memory backend is used (based on `ConcurrentHashMap`), but you can plug in
-any custom backend by implementing the `StateStoreBackend` interface or using one of the
-provided backend modules:
+By default, an in-memory backend is used (`MemoryKeyValueRepository` from `camel-support`),
+but you can plug in any `KeyValueRepository` implementation. The same `KeyValueRepository`
+can also be used as the backend for the Idempotent Consumer and Aggregator EIPs via the
+`KeyValueIdempotentRepository` and `KeyValueAggregationRepository` adapters.
== When to use State Store vs individual components
@@ -31,17 +32,9 @@ Use State Store when:
* You need a *simple key-value API* and want to switch backends without changing route logic.
* You are *migrating from MuleSoft* and want an Object Store equivalent.
-* You want *backend portability* — develop with in-memory, deploy with Redis or Infinispan.
-* Your use case is limited to *put, putIfAbsent, get, delete, contains, keys, size, clear* operations.
-
-[width="100%",cols="1,2,1",options="header"]
-|===
-| Backend | Module | Description
-| In-Memory | `camel-state-store` | Default. Uses `ConcurrentHashMap` with lazy TTL expiry. No external dependencies.
-| Caffeine | `camel-state-store-caffeine` | High-performance local cache with proper eviction and per-entry TTL.
-| Redis | `camel-state-store-redis` | Distributed store via Redisson. Supports per-entry TTL natively.
-| Infinispan | `camel-state-store-infinispan` | Distributed cache via Hot Rod client. Supports per-entry TTL via lifespan.
-|===
+* You want *backend portability* — develop with in-memory, deploy with any `KeyValueRepository`.
+* Your use case is limited to *put, get, delete, contains, keys* operations.
+* You want a *single backend* to serve state store, idempotent consumer, and aggregation patterns.
== URI Format
@@ -164,226 +157,35 @@ from("direct:init")
=== Backend auto-discovery
If no `backend` option is specified on the endpoint, the component automatically looks up a
-`StateStoreBackend` bean from the Camel registry. If exactly one is found, it is used for all
-endpoints. If none is found, the default in-memory backend is used.
+`KeyValueRepository` bean from the Camel registry. If exactly one is found, it is used for all
+endpoints. If none is found, the default in-memory backend (`MemoryKeyValueRepository`) is used.
This means you can configure a backend once (via Java or properties) and all `state-store`
endpoints will use it without needing `backend=#beanName` on each endpoint URI.
-=== Configuring backends via properties
-
-Backends can be configured entirely via `application.properties` using Camel's `camel.beans.*`
-syntax, without writing any Java code. This is especially useful for YAML DSL or Camel JBang users.
-
-[tabs]
-====
-Caffeine::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.caffeineBackend = #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
-camel.beans.caffeineBackend.maximumSize = 50000
-----
-
-Redis::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.redisBackend = #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
-camel.beans.redisBackend.redisUrl = redis://myhost:6379
-camel.beans.redisBackend.mapName = my-app-state
-----
-
-Infinispan::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.infinispanBackend = #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
-camel.beans.infinispanBackend.hosts = myhost:11222
-camel.beans.infinispanBackend.cacheName = my-cache
-----
-====
-
-With the backend configured via properties and auto-discovery, no `backend=#...` reference
-is needed in the route:
-
-[source,yaml]
-----
-- route:
- from:
- uri: direct:store
- steps:
- - setHeader:
- name: CamelStateStoreKey
- constant: myKey
- - to:
- uri: state-store:myStore?operation=put
-----
-
=== In-Memory (default)
-The default backend stores entries in a `ConcurrentHashMap`. TTL expiry is lazy -- entries are
-checked and removed on access rather than by a background thread.
+The default backend (`MemoryKeyValueRepository`) stores entries in a `ConcurrentHashMap`. TTL
+expiry is lazy -- entries are checked and removed on access rather than by a background thread.
No additional dependency is needed beyond `camel-state-store`.
-=== Caffeine
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-caffeine</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("caffeineBackend")
-public CaffeineStateStoreBackend caffeine() {
- CaffeineStateStoreBackend backend = new CaffeineStateStoreBackend();
- backend.setMaximumSize(50_000); // default is 10,000
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.caffeineBackend = #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
-camel.beans.caffeineBackend.maximumSize = 50000
-----
-====
-
-With auto-discovery, the backend is picked up automatically. You can also reference it explicitly:
-
-[source,java]
-----
-from("direct:store")
- .to("state-store:myStore?operation=put&backend=#caffeineBackend");
-----
-
-Caffeine provides proper eviction policies and active TTL expiration using variable expiry.
-
-=== Redis
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-redis</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("redisBackend")
-public RedisStateStoreBackend redis() {
- RedisStateStoreBackend backend = new RedisStateStoreBackend();
- backend.setRedisUrl("redis://myhost:6379");
- backend.setMapName("my-app-state"); // Redis map name
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.redisBackend = #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
-camel.beans.redisBackend.redisUrl = redis://myhost:6379
-camel.beans.redisBackend.mapName = my-app-state
-----
-====
-
-You can also inject a pre-configured `RedissonClient`:
-
-[source,java]
-----
-backend.setRedisson(existingRedissonClient);
-----
-
-Redis provides native per-entry TTL via `RMapCache`. Data is distributed and persisted according
-to your Redis configuration.
-
-=== Infinispan
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-infinispan</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("infinispanBackend")
-public InfinispanStateStoreBackend infinispan() {
- InfinispanStateStoreBackend backend = new InfinispanStateStoreBackend();
- backend.setHosts("myhost:11222");
- backend.setCacheName("my-cache");
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.infinispanBackend = #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
-camel.beans.infinispanBackend.hosts = myhost:11222
-camel.beans.infinispanBackend.cacheName = my-cache
-----
-====
-
-You can also inject a pre-configured `RemoteCacheManager`:
-
-[source,java]
-----
-backend.setCacheManager(existingCacheManager);
-----
-
-Infinispan provides native per-entry TTL via the lifespan parameter and supports distributed caching.
-
== Custom Backend
-Implement the `StateStoreBackend` interface to create your own backend:
+Implement the `KeyValueRepository` interface (from `org.apache.camel.spi`) to create your own backend:
[source,java]
----
-public class MyCustomBackend implements StateStoreBackend {
+public class MyCustomBackend extends ServiceSupport implements KeyValueRepository {
@Override
- public Object put(String key, Object value, long ttlMillis) {
- // store the value, return previous
+ public Object get(String key) {
+ // retrieve the value
}
@Override
- public Object get(String key) {
- // retrieve the value
+ public Object put(String key, Object value, long ttlMillis) {
+ // store the value; return the previous value or null
}
@Override
@@ -405,16 +207,6 @@ public class MyCustomBackend implements StateStoreBackend {
public void clear() {
// remove all entries
}
-
- @Override
- public void start() {
- // initialize connections
- }
-
- @Override
- public void stop() {
- // clean up resources
- }
}
----
@@ -428,3 +220,24 @@ Then reference it via bean or let auto-discovery find it:
from("direct:store")
.to("state-store:myStore?operation=put&backend=#myCustomBackend");
----
+
+=== Sharing a backend across patterns
+
+Because `KeyValueRepository` is the unified SPI, the same implementation can be reused as the
+backend for the Idempotent Consumer and Aggregator EIPs:
+
+[source,java]
+----
+@BindToRegistry("kvRepo")
+public KeyValueRepository kvRepo() {
+ return new MemoryKeyValueRepository();
+}
+----
+
+With a single `KeyValueRepository` in the registry:
+
+* The State Store component auto-discovers and uses it.
+* The Idempotent Consumer EIP auto-discovers and wraps it in a `KeyValueIdempotentRepository`.
+* The Aggregator EIP auto-discovers and wraps it in a `KeyValueAggregationRepository`.
+
+No explicit wiring is needed — all three patterns share the same backend.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties
index d906c34686b7..a7fef8848b0d 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties
@@ -57,9 +57,6 @@ spring-cloud-config
spring-main
spring-security
spring-xml
-state-store-caffeine
-state-store-infinispan
-state-store-redis
telemetry
telemetry-dev
test-junit5
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-caffeine.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-caffeine.json
deleted file mode 100644
index fc311aa76cb2..000000000000
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-caffeine.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-caffeine",
- "title": "State Store Caffeine",
- "description": "Camel State Store backend using Caffeine cache",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-caffeine",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-infinispan.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-infinispan.json
deleted file mode 100644
index a047688f0bcf..000000000000
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-infinispan.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-infinispan",
- "title": "State Store Infinispan",
- "description": "Camel State Store backend using Infinispan remote (Hot Rod)",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-infinispan",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-redis.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-redis.json
deleted file mode 100644
index 20889cda5326..000000000000
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/state-store-redis.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-redis",
- "title": "State Store Redis",
- "description": "Camel State Store backend using Redisson (Redis)",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-redis",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/components/camel-state-store/camel-state-store-caffeine/pom.xml b/components/camel-state-store/camel-state-store-caffeine/pom.xml
deleted file mode 100644
index bed04dfb1a55..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/pom.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- 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.
-
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-parent</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </parent>
-
- <artifactId>camel-state-store-caffeine</artifactId>
- <packaging>jar</packaging>
-
- <name>Camel :: State Store :: Caffeine</name>
- <description>Camel State Store backend using Caffeine cache</description>
-
- <properties>
- <firstVersion>4.23.0</firstVersion>
- </properties>
-
- <dependencies>
-
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.github.ben-manes.caffeine</groupId>
- <artifactId>caffeine</artifactId>
- </dependency>
-
- <!-- testing -->
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-test-main-junit5</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
-
- </dependencies>
-</project>
diff --git a/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/META-INF/services/org/apache/camel/other.properties
deleted file mode 100644
index f873d149c96f..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/META-INF/services/org/apache/camel/other.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-# Generated by camel build tools - do NOT edit this file!
-name=state-store-caffeine
-groupId=org.apache.camel
-artifactId=camel-state-store-caffeine
-version=4.23.0-SNAPSHOT
-projectName=Camel :: State Store :: Caffeine
-projectDescription=Camel State Store backend using Caffeine cache
diff --git a/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/state-store-caffeine.json b/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/state-store-caffeine.json
deleted file mode 100644
index fc311aa76cb2..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/state-store-caffeine.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-caffeine",
- "title": "State Store Caffeine",
- "description": "Camel State Store backend using Caffeine cache",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-caffeine",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/components/camel-state-store/camel-state-store-caffeine/src/main/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackend.java b/components/camel-state-store/camel-state-store-caffeine/src/main/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackend.java
deleted file mode 100644
index 08a3316cd162..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/src/main/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackend.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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.statestore.caffeine;
-
-import java.time.Duration;
-import java.util.Set;
-
-import com.github.benmanes.caffeine.cache.Cache;
-import com.github.benmanes.caffeine.cache.Caffeine;
-import com.github.benmanes.caffeine.cache.Expiry;
-import org.apache.camel.component.statestore.StateStoreBackend;
-
-/**
- * A {@link StateStoreBackend} implementation backed by Caffeine cache. Supports per-entry TTL using Caffeine's variable
- * expiration.
- *
- * @since 4.23
- */
-public class CaffeineStateStoreBackend implements StateStoreBackend {
-
- private Cache<String, TimedValue> cache;
- private int maximumSize = 10_000;
-
- @Override
- public Object put(String key, Object value, long ttlMillis) {
- TimedValue previous = cache.asMap().put(key, new TimedValue(value, ttlMillis));
- return previous != null ? previous.value() : null;
- }
-
- @Override
- public Object get(String key) {
- TimedValue entry = cache.getIfPresent(key);
- return entry != null ? entry.value() : null;
- }
-
- @Override
- public Object delete(String key) {
- TimedValue previous = cache.asMap().remove(key);
- return previous != null ? previous.value() : null;
- }
-
- @Override
- public boolean contains(String key) {
- return cache.getIfPresent(key) != null;
- }
-
- @Override
- public Object putIfAbsent(String key, Object value, long ttlMillis) {
- TimedValue newValue = new TimedValue(value, ttlMillis);
- TimedValue existing = cache.asMap().putIfAbsent(key, newValue);
- return existing != null ? existing.value() : null;
- }
-
- @Override
- public int size() {
- cache.cleanUp();
- return (int) cache.estimatedSize();
- }
-
- @Override
- public Set<String> keys() {
- return Set.copyOf(cache.asMap().keySet());
- }
-
- @Override
- public void clear() {
- cache.invalidateAll();
- }
-
- @Override
- public void start() {
- if (cache != null) {
- return;
- }
- cache = Caffeine.newBuilder()
- .maximumSize(maximumSize)
- .expireAfter(new Expiry<String, TimedValue>() {
- @Override
- public long expireAfterCreate(String key, TimedValue value, long currentTime) {
- return value.ttlMillis() > 0
- ? Duration.ofMillis(value.ttlMillis()).toNanos()
- : Long.MAX_VALUE;
- }
-
- @Override
- public long expireAfterUpdate(String key, TimedValue value, long currentTime, long currentDuration) {
- return value.ttlMillis() > 0
- ? Duration.ofMillis(value.ttlMillis()).toNanos()
- : Long.MAX_VALUE;
- }
-
- @Override
- public long expireAfterRead(String key, TimedValue value, long currentTime, long currentDuration) {
- return currentDuration;
- }
- })
- .build();
- }
-
- @Override
- public void stop() {
- if (cache != null) {
- cache.invalidateAll();
- cache.cleanUp();
- cache = null;
- }
- }
-
- public int getMaximumSize() {
- return maximumSize;
- }
-
- public void setMaximumSize(int maximumSize) {
- this.maximumSize = maximumSize;
- }
-
- private record TimedValue(Object value, long ttlMillis) {
- }
-}
diff --git a/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackendTest.java b/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackendTest.java
deleted file mode 100644
index 1fed0911079e..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackendTest.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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.statestore.caffeine;
-
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.camel.BindToRegistry;
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.component.statestore.StateStoreOperations;
-import org.apache.camel.test.junit5.CamelTestSupport;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-
-class CaffeineStateStoreBackendTest extends CamelTestSupport {
-
- @BindToRegistry("caffeineBackend")
- private final CaffeineStateStoreBackend backend = new CaffeineStateStoreBackend();
-
- @Test
- void testPutAndGet() {
- template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Test
- void testPutReturnsOldValue() {
- template.requestBodyAndHeaders(
- "direct:put", "first",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "second",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isEqualTo("first");
- }
-
- @Test
- void testDelete() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object removed = template.requestBodyAndHeaders(
- "direct:delete", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(removed).isEqualTo("value");
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isNull();
- }
-
- @Test
- void testContains() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object exists = template.requestBodyAndHeaders(
- "direct:dynamic", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains,
- StateStoreConstants.KEY, "key1"));
- assertThat(exists).isEqualTo(true);
- }
-
- @Test
- @SuppressWarnings("unchecked")
- void testKeysAndSize() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
- template.requestBodyAndHeaders(
- "direct:put", "v2",
- Map.of(StateStoreConstants.KEY, "b"));
-
- Set<String> keys = (Set<String>) template.requestBodyAndHeaders(
- "direct:dynamic", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys));
- assertThat(keys).containsExactlyInAnyOrder("a", "b");
-
- Object size = template.requestBodyAndHeaders(
- "direct:dynamic", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size));
- assertThat(size).isEqualTo(2);
- }
-
- @Test
- void testPutIfAbsent() {
- Object result = template.requestBodyAndHeaders(
- "direct:dynamic", "first",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isNull();
-
- result = template.requestBodyAndHeaders(
- "direct:dynamic", "second",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("first");
- }
-
- @Test
- void testClear() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
-
- template.requestBodyAndHeaders(
- "direct:dynamic", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.clear));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "a"));
- assertThat(result).isNull();
- }
-
- @Test
- void testTtlExpiry() {
- template.requestBodyAndHeaders(
- "direct:put-ttl", "expiring",
- Map.of(StateStoreConstants.KEY, "ttlKey"));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(result).isEqualTo("expiring");
-
- await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
- backend.keys(); // trigger Caffeine cleanup
- Object expired = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(expired).isNull();
- });
- }
-
- @Test
- void testPerMessageTtlHeader() {
- template.requestBodyAndHeaders(
- "direct:put", "expiring",
- Map.of(StateStoreConstants.KEY, "ttlKey",
- StateStoreConstants.TTL, 200L));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(result).isEqualTo("expiring");
-
- await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
- backend.keys();
- Object expired = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(expired).isNull();
- });
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- from("direct:put").to("state-store:myStore?operation=put&backend=#caffeineBackend");
- from("direct:put-ttl").to("state-store:myStore?operation=put&backend=#caffeineBackend&ttl=200");
- from("direct:get").to("state-store:myStore?operation=get&backend=#caffeineBackend");
- from("direct:delete").to("state-store:myStore?operation=delete&backend=#caffeineBackend");
- from("direct:dynamic").to("state-store:myStore?backend=#caffeineBackend");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStorePropertiesTest.java b/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStorePropertiesTest.java
deleted file mode 100644
index 18d760363ecb..000000000000
--- a/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStorePropertiesTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.statestore.caffeine;
-
-import java.util.Map;
-import java.util.Properties;
-
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreBackend;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.test.main.junit5.CamelMainTestSupport;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests that a Caffeine backend can be fully configured via properties (camel.beans.*) and auto-discovered without
- * explicit backend=# reference on the endpoint.
- */
-class CaffeineStateStorePropertiesTest extends CamelMainTestSupport {
-
- @Override
- protected Properties useOverridePropertiesWithPropertiesComponent() {
- Properties props = new Properties();
- props.setProperty("camel.beans.caffeineBackend",
- "#class:" + CaffeineStateStoreBackend.class.getName());
- props.setProperty("camel.beans.caffeineBackend.maximumSize", "5000");
- return props;
- }
-
- @Test
- void testBackendConfiguredViaProperties() {
- // Verify the bean was created and configured
- StateStoreBackend backend = context.getRegistry().lookupByNameAndType("caffeineBackend", StateStoreBackend.class);
- assertThat(backend).isNotNull().isInstanceOf(CaffeineStateStoreBackend.class);
- assertThat(((CaffeineStateStoreBackend) backend).getMaximumSize()).isEqualTo(5000);
- }
-
- @Test
- void testAutoDiscoveryWithPropertiesConfiguredBackend() {
- // No backend=#caffeineBackend in the URI — auto-discovery should find it
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isNull();
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Test
- void testExplicitBeanReferenceWithPropertiesConfiguredBackend() {
- // Explicit backend=#caffeineBackend — should also work
- Object previous = template.requestBodyAndHeaders(
- "direct:put-explicit", "world",
- Map.of(StateStoreConstants.KEY, "key2"));
- assertThat(previous).isNull();
-
- Object result = template.requestBodyAndHeaders(
- "direct:get-explicit", null,
- Map.of(StateStoreConstants.KEY, "key2"));
- assertThat(result).isEqualTo("world");
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- // Auto-discovery (no backend reference)
- from("direct:put").to("state-store:myStore?operation=put");
- from("direct:get").to("state-store:myStore?operation=get");
- // Explicit bean reference
- from("direct:put-explicit").to("state-store:explicitStore?operation=put&backend=#caffeineBackend");
- from("direct:get-explicit").to("state-store:explicitStore?operation=get&backend=#caffeineBackend");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store-infinispan/pom.xml b/components/camel-state-store/camel-state-store-infinispan/pom.xml
deleted file mode 100644
index 724c59b84461..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/pom.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- 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.
-
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-parent</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </parent>
-
- <artifactId>camel-state-store-infinispan</artifactId>
- <packaging>jar</packaging>
-
- <name>Camel :: State Store :: Infinispan</name>
- <description>Camel State Store backend using Infinispan remote (Hot Rod)</description>
-
- <properties>
- <firstVersion>4.23.0</firstVersion>
- </properties>
-
- <dependencies>
-
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.infinispan</groupId>
- <artifactId>infinispan-client-hotrod</artifactId>
- <version>${infinispan-version}</version>
- </dependency>
-
- <!-- testing -->
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-test-main-junit5</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-test-infra-infinispan</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
-
- </dependencies>
-</project>
diff --git a/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/META-INF/services/org/apache/camel/other.properties
deleted file mode 100644
index f178128a44a5..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/META-INF/services/org/apache/camel/other.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-# Generated by camel build tools - do NOT edit this file!
-name=state-store-infinispan
-groupId=org.apache.camel
-artifactId=camel-state-store-infinispan
-version=4.23.0-SNAPSHOT
-projectName=Camel :: State Store :: Infinispan
-projectDescription=Camel State Store backend using Infinispan remote (Hot Rod)
diff --git a/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/state-store-infinispan.json b/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/state-store-infinispan.json
deleted file mode 100644
index a047688f0bcf..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/state-store-infinispan.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-infinispan",
- "title": "State Store Infinispan",
- "description": "Camel State Store backend using Infinispan remote (Hot Rod)",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-infinispan",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/components/camel-state-store/camel-state-store-infinispan/src/main/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackend.java b/components/camel-state-store/camel-state-store-infinispan/src/main/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackend.java
deleted file mode 100644
index 150315a4b64f..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/src/main/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackend.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.statestore.infinispan;
-
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.camel.component.statestore.StateStoreBackend;
-import org.infinispan.client.hotrod.RemoteCache;
-import org.infinispan.client.hotrod.RemoteCacheManager;
-import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
-
-/**
- * A {@link StateStoreBackend} implementation backed by Infinispan remote (Hot Rod). Supports per-entry TTL via
- * Infinispan's lifespan parameter.
- *
- * @since 4.23
- */
-public class InfinispanStateStoreBackend implements StateStoreBackend {
-
- private RemoteCacheManager cacheManager;
- private RemoteCache<String, Object> cache;
- private String hosts = "localhost:11222";
- private String cacheName = "camel-state-store";
- private boolean managedCacheManager;
-
- @Override
- public Object put(String key, Object value, long ttlMillis) {
- if (ttlMillis > 0) {
- return cache.put(key, value, ttlMillis, TimeUnit.MILLISECONDS);
- }
- return cache.put(key, value);
- }
-
- @Override
- public Object putIfAbsent(String key, Object value, long ttlMillis) {
- if (ttlMillis > 0) {
- return cache.putIfAbsent(key, value, ttlMillis, TimeUnit.MILLISECONDS);
- }
- return cache.putIfAbsent(key, value);
- }
-
- @Override
- public Object get(String key) {
- return cache.get(key);
- }
-
- @Override
- public Object delete(String key) {
- return cache.remove(key);
- }
-
- @Override
- public boolean contains(String key) {
- return cache.containsKey(key);
- }
-
- @Override
- public Set<String> keys() {
- return Set.copyOf(cache.keySet());
- }
-
- @Override
- public int size() {
- return cache.size();
- }
-
- @Override
- public void clear() {
- cache.clear();
- }
-
- @Override
- public void start() {
- if (cache != null) {
- return;
- }
- if (cacheManager == null) {
- ConfigurationBuilder builder = new ConfigurationBuilder();
- builder.addServers(hosts);
- builder.forceReturnValues(true);
- cacheManager = new RemoteCacheManager(builder.build(), true);
- managedCacheManager = true;
- }
- cache = cacheManager.administration().getOrCreateCache(cacheName, (String) null);
- }
-
- @Override
- public void stop() {
- cache = null;
- if (managedCacheManager && cacheManager != null) {
- cacheManager.stop();
- cacheManager = null;
- managedCacheManager = false;
- }
- }
-
- public RemoteCacheManager getCacheManager() {
- return cacheManager;
- }
-
- public void setCacheManager(RemoteCacheManager cacheManager) {
- this.cacheManager = cacheManager;
- }
-
- public String getHosts() {
- return hosts;
- }
-
- public void setHosts(String hosts) {
- this.hosts = hosts;
- }
-
- public String getCacheName() {
- return cacheName;
- }
-
- public void setCacheName(String cacheName) {
- this.cacheName = cacheName;
- }
-}
diff --git a/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackendIT.java b/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackendIT.java
deleted file mode 100644
index c4712721f7f2..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackendIT.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * 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.statestore.infinispan;
-
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.camel.BindToRegistry;
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.component.statestore.StateStoreOperations;
-import org.apache.camel.test.infra.infinispan.services.InfinispanService;
-import org.apache.camel.test.infra.infinispan.services.InfinispanServiceFactory;
-import org.apache.camel.test.junit5.CamelTestSupport;
-import org.infinispan.client.hotrod.RemoteCacheManager;
-import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-
-class InfinispanStateStoreBackendIT extends CamelTestSupport {
-
- private static final boolean IS_MAC_OS = System.getProperty("os.name", "").startsWith("Mac");
-
- @RegisterExtension
- static InfinispanService service = InfinispanServiceFactory.createSingletonInfinispanService();
-
- private InfinispanStateStoreBackend backend;
- private RemoteCacheManager testCacheManager;
-
- @AfterEach
- void cleanUp() {
- if (testCacheManager != null) {
- testCacheManager.stop();
- testCacheManager = null;
- }
- }
-
- @BeforeEach
- void clearCache() {
- if (backend != null) {
- template.requestBodyAndHeaders("direct:op", (Object) null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.clear));
- }
- }
-
- @BindToRegistry("infinispanBackend")
- public InfinispanStateStoreBackend createBackend() {
- backend = new InfinispanStateStoreBackend();
-
- ConfigurationBuilder builder = new ConfigurationBuilder();
- builder.forceReturnValues(true);
- builder.addServers(service.host() + ":" + service.port());
- if (service.username() != null) {
- builder.security().authentication()
- .username(service.username())
- .password(service.password())
- .saslMechanism("SCRAM-SHA-512")
- .realm("default")
- .serverName("infinispan");
- }
- if (IS_MAC_OS) {
- Properties properties = new Properties();
- properties.put("infinispan.client.hotrod.client_intelligence", "BASIC");
- builder.withProperties(properties);
- }
- testCacheManager = new RemoteCacheManager(builder.build(), true);
- backend.setCacheManager(testCacheManager);
- backend.setCacheName("state-store-test");
- return backend;
- }
-
- @Test
- void testPutAndGet() {
- template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Test
- void testPutReturnsOldValue() {
- template.requestBodyAndHeaders(
- "direct:put", "first",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "second",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isEqualTo("first");
- }
-
- @Test
- void testDelete() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object removed = template.requestBodyAndHeaders(
- "direct:delete", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(removed).isEqualTo("value");
- }
-
- @Test
- void testContains() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object exists = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains,
- StateStoreConstants.KEY, "key1"));
- assertThat(exists).isEqualTo(true);
-
- Object notExists = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains,
- StateStoreConstants.KEY, "missing"));
- assertThat(notExists).isEqualTo(false);
- }
-
- @Test
- void testClear() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
-
- template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.clear));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "a"));
- assertThat(result).isNull();
- }
-
- @Test
- void testDeleteNonExistentKey() {
- Object removed = template.requestBodyAndHeaders(
- "direct:delete", null,
- Map.of(StateStoreConstants.KEY, "no-such-key"));
- assertThat(removed).isNull();
- }
-
- @Test
- @SuppressWarnings("unchecked")
- void testKeysAndSize() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
- template.requestBodyAndHeaders(
- "direct:put", "v2",
- Map.of(StateStoreConstants.KEY, "b"));
-
- Set<String> keys = (Set<String>) template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys));
- assertThat(keys).containsExactlyInAnyOrder("a", "b");
-
- Object size = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size));
- assertThat(size).isEqualTo(2);
- }
-
- @Test
- void testPutIfAbsent() {
- Object result = template.requestBodyAndHeaders(
- "direct:op", "first",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isNull();
-
- result = template.requestBodyAndHeaders(
- "direct:op", "second",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("first");
- }
-
- @Test
- void testTtlExpiry() {
- template.requestBodyAndHeaders(
- "direct:put", "expiring",
- Map.of(StateStoreConstants.KEY, "ttlKey",
- StateStoreConstants.TTL, 500L));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(result).isEqualTo("expiring");
-
- await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
- Object expired = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(expired).isNull();
- });
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- from("direct:put").to("state-store:ispnStore?operation=put&backend=#infinispanBackend");
- from("direct:get").to("state-store:ispnStore?operation=get&backend=#infinispanBackend");
- from("direct:delete").to("state-store:ispnStore?operation=delete&backend=#infinispanBackend");
- from("direct:op").to("state-store:ispnStore?backend=#infinispanBackend");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStorePropertiesIT.java b/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStorePropertiesIT.java
deleted file mode 100644
index b10e74feec11..000000000000
--- a/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStorePropertiesIT.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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.statestore.infinispan;
-
-import java.util.Map;
-import java.util.Properties;
-
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreBackend;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.spi.Registry;
-import org.apache.camel.test.infra.infinispan.services.InfinispanService;
-import org.apache.camel.test.infra.infinispan.services.InfinispanServiceFactory;
-import org.apache.camel.test.main.junit5.CamelMainTestSupport;
-import org.infinispan.client.hotrod.RemoteCacheManager;
-import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests that an Infinispan backend can be configured via properties (camel.beans.*) and auto-discovered. The backend is
- * created via properties but the {@link RemoteCacheManager} is injected programmatically since it requires
- * authentication and platform-specific configuration that cannot be expressed as simple properties.
- */
-class InfinispanStateStorePropertiesIT extends CamelMainTestSupport {
-
- private static final boolean IS_MAC_OS = System.getProperty("os.name", "").startsWith("Mac");
-
- @RegisterExtension
- static InfinispanService service = InfinispanServiceFactory.createSingletonInfinispanService();
-
- private RemoteCacheManager testCacheManager;
-
- @AfterEach
- void cleanUp() {
- if (testCacheManager != null) {
- testCacheManager.stop();
- testCacheManager = null;
- }
- }
-
- @Override
- protected Properties useOverridePropertiesWithPropertiesComponent() {
- Properties props = new Properties();
- props.setProperty("camel.beans.infinispanBackend",
- "#class:" + InfinispanStateStoreBackend.class.getName());
- props.setProperty("camel.beans.infinispanBackend.cacheName", "state-store-props-test");
- return props;
- }
-
- @Override
- protected void bindToRegistry(Registry registry) throws Exception {
- super.bindToRegistry(registry);
- // Inject a pre-configured RemoteCacheManager into the property-created backend.
- // Authentication and platform-specific settings require programmatic configuration.
- InfinispanStateStoreBackend backend
- = registry.lookupByNameAndType("infinispanBackend", InfinispanStateStoreBackend.class);
- if (backend != null) {
- backend.setCacheManager(createCacheManager());
- }
- }
-
- private RemoteCacheManager createCacheManager() {
- ConfigurationBuilder builder = new ConfigurationBuilder();
- builder.forceReturnValues(true);
- builder.addServers(service.host() + ":" + service.port());
- if (service.username() != null) {
- builder.security().authentication()
- .username(service.username())
- .password(service.password())
- .saslMechanism("SCRAM-SHA-512")
- .realm("default")
- .serverName("infinispan");
- }
- if (IS_MAC_OS) {
- Properties properties = new Properties();
- properties.put("infinispan.client.hotrod.client_intelligence", "BASIC");
- builder.withProperties(properties);
- }
- testCacheManager = new RemoteCacheManager(builder.build(), true);
- return testCacheManager;
- }
-
- @Test
- void testBackendConfiguredViaProperties() {
- StateStoreBackend backend = context.getRegistry().lookupByNameAndType("infinispanBackend", StateStoreBackend.class);
- assertThat(backend).isNotNull().isInstanceOf(InfinispanStateStoreBackend.class);
- assertThat(((InfinispanStateStoreBackend) backend).getCacheName()).isEqualTo("state-store-props-test");
- }
-
- @Test
- void testAutoDiscoveryPutAndGet() {
- // No backend=#infinispanBackend in the URI — auto-discovery should find it
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isNull();
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- // No backend=# reference — relies on auto-discovery
- from("direct:put").to("state-store:ispnStore?operation=put");
- from("direct:get").to("state-store:ispnStore?operation=get");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store-redis/pom.xml b/components/camel-state-store/camel-state-store-redis/pom.xml
deleted file mode 100644
index bac53dba3784..000000000000
--- a/components/camel-state-store/camel-state-store-redis/pom.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- 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.
-
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-parent</artifactId>
- <version>4.23.0-SNAPSHOT</version>
- </parent>
-
- <artifactId>camel-state-store-redis</artifactId>
- <packaging>jar</packaging>
-
- <name>Camel :: State Store :: Redis</name>
- <description>Camel State Store backend using Redisson (Redis)</description>
-
- <properties>
- <firstVersion>4.23.0</firstVersion>
- </properties>
-
- <dependencies>
-
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson</artifactId>
- <version>${redisson-version}</version>
- </dependency>
-
- <!-- testing -->
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-test-main-junit5</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-test-infra-redis</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
-
- </dependencies>
-</project>
diff --git a/components/camel-state-store/camel-state-store-redis/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-state-store/camel-state-store-redis/src/generated/resources/META-INF/services/org/apache/camel/other.properties
deleted file mode 100644
index bd0bb6ee55ee..000000000000
--- a/components/camel-state-store/camel-state-store-redis/src/generated/resources/META-INF/services/org/apache/camel/other.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-# Generated by camel build tools - do NOT edit this file!
-name=state-store-redis
-groupId=org.apache.camel
-artifactId=camel-state-store-redis
-version=4.23.0-SNAPSHOT
-projectName=Camel :: State Store :: Redis
-projectDescription=Camel State Store backend using Redisson (Redis)
diff --git a/components/camel-state-store/camel-state-store-redis/src/generated/resources/state-store-redis.json b/components/camel-state-store/camel-state-store-redis/src/generated/resources/state-store-redis.json
deleted file mode 100644
index 20889cda5326..000000000000
--- a/components/camel-state-store/camel-state-store-redis/src/generated/resources/state-store-redis.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "other": {
- "kind": "other",
- "name": "state-store-redis",
- "title": "State Store Redis",
- "description": "Camel State Store backend using Redisson (Redis)",
- "deprecated": false,
- "firstVersion": "4.23.0",
- "supportLevel": "Preview",
- "groupId": "org.apache.camel",
- "artifactId": "camel-state-store-redis",
- "version": "4.23.0-SNAPSHOT"
- }
-}
diff --git a/components/camel-state-store/camel-state-store-redis/src/main/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackend.java b/components/camel-state-store/camel-state-store-redis/src/main/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackend.java
deleted file mode 100644
index 861e08db92ab..000000000000
--- a/components/camel-state-store/camel-state-store-redis/src/main/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackend.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.statestore.redis;
-
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.camel.component.statestore.StateStoreBackend;
-import org.redisson.Redisson;
-import org.redisson.api.RMapCache;
-import org.redisson.api.RedissonClient;
-import org.redisson.config.Config;
-
-/**
- * A {@link StateStoreBackend} implementation backed by Redis using Redisson. Uses {@link RMapCache} for per-entry TTL
- * support.
- *
- * @since 4.23
- */
-public class RedisStateStoreBackend implements StateStoreBackend {
-
- private RedissonClient redisson;
- private RMapCache<String, Object> mapCache;
- private String redisUrl = "redis://localhost:6379";
- private String mapName = "camel-state-store";
- private boolean managedRedisson;
-
- @Override
- public Object put(String key, Object value, long ttlMillis) {
- if (ttlMillis > 0) {
- return mapCache.put(key, value, ttlMillis, TimeUnit.MILLISECONDS);
- }
- return mapCache.put(key, value);
- }
-
- @Override
- public Object putIfAbsent(String key, Object value, long ttlMillis) {
- if (ttlMillis > 0) {
- return mapCache.putIfAbsent(key, value, ttlMillis, TimeUnit.MILLISECONDS);
- }
- return mapCache.putIfAbsent(key, value);
- }
-
- @Override
- public Object get(String key) {
- return mapCache.get(key);
- }
-
- @Override
- public Object delete(String key) {
- return mapCache.remove(key);
- }
-
- @Override
- public boolean contains(String key) {
- return mapCache.containsKey(key);
- }
-
- @Override
- public Set<String> keys() {
- return Set.copyOf(mapCache.keySet());
- }
-
- @Override
- public int size() {
- return mapCache.size();
- }
-
- @Override
- public void clear() {
- mapCache.clear();
- }
-
- @Override
- public void start() {
- if (redisson == null) {
- Config config = new Config();
- config.useSingleServer().setAddress(redisUrl);
- redisson = Redisson.create(config);
- managedRedisson = true;
- }
- mapCache = redisson.getMapCache(mapName);
- }
-
- @Override
- public void stop() {
- mapCache = null;
- if (managedRedisson && redisson != null) {
- redisson.shutdown();
- redisson = null;
- managedRedisson = false;
- }
- }
-
- public RedissonClient getRedisson() {
- return redisson;
- }
-
- public void setRedisson(RedissonClient redisson) {
- this.redisson = redisson;
- }
-
- public String getRedisUrl() {
- return redisUrl;
- }
-
- public void setRedisUrl(String redisUrl) {
- this.redisUrl = redisUrl;
- }
-
- public String getMapName() {
- return mapName;
- }
-
- public void setMapName(String mapName) {
- this.mapName = mapName;
- }
-}
diff --git a/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackendIT.java b/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackendIT.java
deleted file mode 100644
index 98200385ccfe..000000000000
--- a/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackendIT.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.statestore.redis;
-
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.camel.BindToRegistry;
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.component.statestore.StateStoreOperations;
-import org.apache.camel.test.infra.redis.services.RedisService;
-import org.apache.camel.test.infra.redis.services.RedisServiceFactory;
-import org.apache.camel.test.junit5.CamelTestSupport;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-
-class RedisStateStoreBackendIT extends CamelTestSupport {
-
- @RegisterExtension
- static RedisService service = RedisServiceFactory.createService();
-
- private RedisStateStoreBackend backend;
-
- @BindToRegistry("redisBackend")
- public RedisStateStoreBackend createBackend() {
- backend = new RedisStateStoreBackend();
- backend.setRedisUrl("redis://" + service.getServiceAddress());
- backend.setMapName("test-state-store-" + System.nanoTime());
- return backend;
- }
-
- @Test
- void testPutAndGet() {
- template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Test
- void testPutReturnsOldValue() {
- template.requestBodyAndHeaders(
- "direct:put", "first",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "second",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isEqualTo("first");
- }
-
- @Test
- void testDelete() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object removed = template.requestBodyAndHeaders(
- "direct:delete", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(removed).isEqualTo("value");
-
- // verify key is gone
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isNull();
- }
-
- @Test
- void testDeleteNonExistentKey() {
- Object removed = template.requestBodyAndHeaders(
- "direct:delete", null,
- Map.of(StateStoreConstants.KEY, "no-such-key"));
- assertThat(removed).isNull();
- }
-
- @Test
- void testContains() {
- template.requestBodyAndHeaders(
- "direct:put", "value",
- Map.of(StateStoreConstants.KEY, "key1"));
-
- Object exists = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains,
- StateStoreConstants.KEY, "key1"));
- assertThat(exists).isEqualTo(true);
-
- Object notExists = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains,
- StateStoreConstants.KEY, "missing"));
- assertThat(notExists).isEqualTo(false);
- }
-
- @Test
- @SuppressWarnings("unchecked")
- void testKeysAndSize() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
- template.requestBodyAndHeaders(
- "direct:put", "v2",
- Map.of(StateStoreConstants.KEY, "b"));
-
- Set<String> keys = (Set<String>) template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys));
- assertThat(keys).containsExactlyInAnyOrder("a", "b");
-
- Object size = template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size));
- assertThat(size).isEqualTo(2);
- }
-
- @Test
- void testPutIfAbsent() {
- Object result = template.requestBodyAndHeaders(
- "direct:op", "first",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isNull();
-
- result = template.requestBodyAndHeaders(
- "direct:op", "second",
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent,
- StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("first");
- }
-
- @Test
- void testClear() {
- template.requestBodyAndHeaders(
- "direct:put", "v1",
- Map.of(StateStoreConstants.KEY, "a"));
-
- template.requestBodyAndHeaders(
- "direct:op", null,
- Map.of(StateStoreConstants.OPERATION, StateStoreOperations.clear));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "a"));
- assertThat(result).isNull();
- }
-
- @Test
- void testTtlExpiry() {
- template.requestBodyAndHeaders(
- "direct:put", "expiring",
- Map.of(StateStoreConstants.KEY, "ttlKey",
- StateStoreConstants.TTL, 500L));
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(result).isEqualTo("expiring");
-
- await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
- Object expired = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "ttlKey"));
- assertThat(expired).isNull();
- });
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- from("direct:put").to("state-store:redisStore?operation=put&backend=#redisBackend");
- from("direct:get").to("state-store:redisStore?operation=get&backend=#redisBackend");
- from("direct:delete").to("state-store:redisStore?operation=delete&backend=#redisBackend");
- from("direct:op").to("state-store:redisStore?backend=#redisBackend");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStorePropertiesIT.java b/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStorePropertiesIT.java
deleted file mode 100644
index 07b849e47938..000000000000
--- a/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStorePropertiesIT.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.statestore.redis;
-
-import java.util.Map;
-import java.util.Properties;
-
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.statestore.StateStoreBackend;
-import org.apache.camel.component.statestore.StateStoreConstants;
-import org.apache.camel.test.infra.redis.services.RedisService;
-import org.apache.camel.test.infra.redis.services.RedisServiceFactory;
-import org.apache.camel.test.main.junit5.CamelMainTestSupport;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests that a Redis backend can be fully configured via properties (camel.beans.*) and auto-discovered without
- * explicit backend=# reference on the endpoint.
- */
-class RedisStateStorePropertiesIT extends CamelMainTestSupport {
-
- @RegisterExtension
- static RedisService service = RedisServiceFactory.createService();
-
- @Override
- protected Properties useOverridePropertiesWithPropertiesComponent() {
- Properties props = new Properties();
- props.setProperty("camel.beans.redisBackend",
- "#class:" + RedisStateStoreBackend.class.getName());
- props.setProperty("camel.beans.redisBackend.redisUrl", "redis://" + service.getServiceAddress());
- props.setProperty("camel.beans.redisBackend.mapName", "test-props-" + System.nanoTime());
- return props;
- }
-
- @Test
- void testBackendConfiguredViaProperties() {
- StateStoreBackend backend = context.getRegistry().lookupByNameAndType("redisBackend", StateStoreBackend.class);
- assertThat(backend).isNotNull().isInstanceOf(RedisStateStoreBackend.class);
- }
-
- @Test
- void testAutoDiscoveryPutAndGet() {
- // No backend=#redisBackend in the URI — auto-discovery should find it
- Object previous = template.requestBodyAndHeaders(
- "direct:put", "hello",
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(previous).isNull();
-
- Object result = template.requestBodyAndHeaders(
- "direct:get", null,
- Map.of(StateStoreConstants.KEY, "key1"));
- assertThat(result).isEqualTo("hello");
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
- // No backend=# reference — relies on auto-discovery
- from("direct:put").to("state-store:redisStore?operation=put");
- from("direct:get").to("state-store:redisStore?operation=get");
- }
- };
- }
-}
diff --git a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
index a9c3c59a868f..30ca4c9d160e 100644
--- a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
+++ b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java
@@ -23,7 +23,7 @@ public class StateStoreEndpointConfigurer extends PropertyConfigurerSupport impl
public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) {
StateStoreEndpoint target = (StateStoreEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
- case "backend": target.setBackend(property(camelContext, org.apache.camel.component.statestore.StateStoreBackend.class, value)); return true;
+ case "backend": target.setBackend(property(camelContext, org.apache.camel.spi.KeyValueRepository.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true;
case "operation": target.setOperation(property(camelContext, org.apache.camel.component.statestore.StateStoreOperations.class, value)); return true;
@@ -35,7 +35,7 @@ public class StateStoreEndpointConfigurer extends PropertyConfigurerSupport impl
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
- case "backend": return org.apache.camel.component.statestore.StateStoreBackend.class;
+ case "backend": return org.apache.camel.spi.KeyValueRepository.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
case "operation": return org.apache.camel.component.statestore.StateStoreOperations.class;
diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
index d90c315d879c..470d0b127426 100644
--- a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
+++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json
@@ -3,7 +3,7 @@
"kind": "component",
"name": "state-store",
"title": "State Store",
- "description": "Perform key-value operations against a pluggable state store backend.",
+ "description": "Perform key-value operations against a pluggable KeyValueRepository backend.",
"deprecated": false,
"firstVersion": "4.23.0",
"label": "cache",
@@ -37,6 +37,6 @@
"operation": { "index": 1, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "autowired": false, "secret": false, "description": "The default operation to perform" },
"ttl": { "index": 2, "kind": "parameter", "displayName": "Ttl", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 0, "description": "Time-to-live in milliseconds for entries. 0 means no expiry." },
"lazyStartProducer": { "index": 3, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...]
- "backend": { "index": 4, "kind": "parameter", "displayName": "Backend", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.statestore.StateStoreBackend", "deprecated": false, "autowired": false, "secret": false, "description": "The backend to use. If not set, auto-discovers a single StateStoreBackend from the registry, or falls back to an in-memory store." }
+ "backend": { "index": 4, "kind": "parameter", "displayName": "Backend", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.KeyValueRepository", "deprecated": false, "autowired": false, "secret": false, "description": "The backend to use. If not set, auto-discovers a single KeyValueRepository from the registry, or falls back to an in-memory store." }
}
}
diff --git a/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc b/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
index c37fb9acaf18..e110ccc0bca5 100644
--- a/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
+++ b/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc
@@ -2,7 +2,7 @@
:doctitle: State Store
:shortname: state-store
:artifactid: camel-state-store
-:description: Perform key-value operations against a pluggable state store backend.
+:description: Perform key-value operations against a pluggable KeyValueRepository backend.
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
@@ -12,13 +12,14 @@
*{component-header}*
-The State Store component provides a simple, unified key-value store API with pluggable backends.
-It is useful for caching, session state, and scenarios where you need a simple object store
-(similar to MuleSoft's Object Store).
+The State Store component provides a simple, unified key-value store API backed by the
+`KeyValueRepository` SPI from `camel-api`. It is useful for caching, session state, and
+scenarios where you need a simple object store (similar to MuleSoft's Object Store).
-By default, an in-memory backend is used (based on `ConcurrentHashMap`), but you can plug in
-any custom backend by implementing the `StateStoreBackend` interface or using one of the
-provided backend modules:
+By default, an in-memory backend is used (`MemoryKeyValueRepository` from `camel-support`),
+but you can plug in any `KeyValueRepository` implementation. The same `KeyValueRepository`
+can also be used as the backend for the Idempotent Consumer and Aggregator EIPs via the
+`KeyValueIdempotentRepository` and `KeyValueAggregationRepository` adapters.
== When to use State Store vs individual components
@@ -31,17 +32,9 @@ Use State Store when:
* You need a *simple key-value API* and want to switch backends without changing route logic.
* You are *migrating from MuleSoft* and want an Object Store equivalent.
-* You want *backend portability* — develop with in-memory, deploy with Redis or Infinispan.
-* Your use case is limited to *put, putIfAbsent, get, delete, contains, keys, size, clear* operations.
-
-[width="100%",cols="1,2,1",options="header"]
-|===
-| Backend | Module | Description
-| In-Memory | `camel-state-store` | Default. Uses `ConcurrentHashMap` with lazy TTL expiry. No external dependencies.
-| Caffeine | `camel-state-store-caffeine` | High-performance local cache with proper eviction and per-entry TTL.
-| Redis | `camel-state-store-redis` | Distributed store via Redisson. Supports per-entry TTL natively.
-| Infinispan | `camel-state-store-infinispan` | Distributed cache via Hot Rod client. Supports per-entry TTL via lifespan.
-|===
+* You want *backend portability* — develop with in-memory, deploy with any `KeyValueRepository`.
+* Your use case is limited to *put, get, delete, contains, keys* operations.
+* You want a *single backend* to serve state store, idempotent consumer, and aggregation patterns.
== URI Format
@@ -164,226 +157,35 @@ from("direct:init")
=== Backend auto-discovery
If no `backend` option is specified on the endpoint, the component automatically looks up a
-`StateStoreBackend` bean from the Camel registry. If exactly one is found, it is used for all
-endpoints. If none is found, the default in-memory backend is used.
+`KeyValueRepository` bean from the Camel registry. If exactly one is found, it is used for all
+endpoints. If none is found, the default in-memory backend (`MemoryKeyValueRepository`) is used.
This means you can configure a backend once (via Java or properties) and all `state-store`
endpoints will use it without needing `backend=#beanName` on each endpoint URI.
-=== Configuring backends via properties
-
-Backends can be configured entirely via `application.properties` using Camel's `camel.beans.*`
-syntax, without writing any Java code. This is especially useful for YAML DSL or Camel JBang users.
-
-[tabs]
-====
-Caffeine::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.caffeineBackend = #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
-camel.beans.caffeineBackend.maximumSize = 50000
-----
-
-Redis::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.redisBackend = #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
-camel.beans.redisBackend.redisUrl = redis://myhost:6379
-camel.beans.redisBackend.mapName = my-app-state
-----
-
-Infinispan::
-+
-[source,properties]
-----
-# application.properties
-camel.beans.infinispanBackend = #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
-camel.beans.infinispanBackend.hosts = myhost:11222
-camel.beans.infinispanBackend.cacheName = my-cache
-----
-====
-
-With the backend configured via properties and auto-discovery, no `backend=#...` reference
-is needed in the route:
-
-[source,yaml]
-----
-- route:
- from:
- uri: direct:store
- steps:
- - setHeader:
- name: CamelStateStoreKey
- constant: myKey
- - to:
- uri: state-store:myStore?operation=put
-----
-
=== In-Memory (default)
-The default backend stores entries in a `ConcurrentHashMap`. TTL expiry is lazy -- entries are
-checked and removed on access rather than by a background thread.
+The default backend (`MemoryKeyValueRepository`) stores entries in a `ConcurrentHashMap`. TTL
+expiry is lazy -- entries are checked and removed on access rather than by a background thread.
No additional dependency is needed beyond `camel-state-store`.
-=== Caffeine
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-caffeine</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("caffeineBackend")
-public CaffeineStateStoreBackend caffeine() {
- CaffeineStateStoreBackend backend = new CaffeineStateStoreBackend();
- backend.setMaximumSize(50_000); // default is 10,000
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.caffeineBackend = #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
-camel.beans.caffeineBackend.maximumSize = 50000
-----
-====
-
-With auto-discovery, the backend is picked up automatically. You can also reference it explicitly:
-
-[source,java]
-----
-from("direct:store")
- .to("state-store:myStore?operation=put&backend=#caffeineBackend");
-----
-
-Caffeine provides proper eviction policies and active TTL expiration using variable expiry.
-
-=== Redis
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-redis</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("redisBackend")
-public RedisStateStoreBackend redis() {
- RedisStateStoreBackend backend = new RedisStateStoreBackend();
- backend.setRedisUrl("redis://myhost:6379");
- backend.setMapName("my-app-state"); // Redis map name
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.redisBackend = #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
-camel.beans.redisBackend.redisUrl = redis://myhost:6379
-camel.beans.redisBackend.mapName = my-app-state
-----
-====
-
-You can also inject a pre-configured `RedissonClient`:
-
-[source,java]
-----
-backend.setRedisson(existingRedissonClient);
-----
-
-Redis provides native per-entry TTL via `RMapCache`. Data is distributed and persisted according
-to your Redis configuration.
-
-=== Infinispan
-
-Add the dependency:
-
-[source,xml]
-----
-<dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-infinispan</artifactId>
-</dependency>
-----
-
-[tabs]
-====
-Java::
-+
-[source,java]
-----
-@BindToRegistry("infinispanBackend")
-public InfinispanStateStoreBackend infinispan() {
- InfinispanStateStoreBackend backend = new InfinispanStateStoreBackend();
- backend.setHosts("myhost:11222");
- backend.setCacheName("my-cache");
- return backend;
-}
-----
-
-Properties::
-+
-[source,properties]
-----
-camel.beans.infinispanBackend = #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
-camel.beans.infinispanBackend.hosts = myhost:11222
-camel.beans.infinispanBackend.cacheName = my-cache
-----
-====
-
-You can also inject a pre-configured `RemoteCacheManager`:
-
-[source,java]
-----
-backend.setCacheManager(existingCacheManager);
-----
-
-Infinispan provides native per-entry TTL via the lifespan parameter and supports distributed caching.
-
== Custom Backend
-Implement the `StateStoreBackend` interface to create your own backend:
+Implement the `KeyValueRepository` interface (from `org.apache.camel.spi`) to create your own backend:
[source,java]
----
-public class MyCustomBackend implements StateStoreBackend {
+public class MyCustomBackend extends ServiceSupport implements KeyValueRepository {
@Override
- public Object put(String key, Object value, long ttlMillis) {
- // store the value, return previous
+ public Object get(String key) {
+ // retrieve the value
}
@Override
- public Object get(String key) {
- // retrieve the value
+ public Object put(String key, Object value, long ttlMillis) {
+ // store the value; return the previous value or null
}
@Override
@@ -405,16 +207,6 @@ public class MyCustomBackend implements StateStoreBackend {
public void clear() {
// remove all entries
}
-
- @Override
- public void start() {
- // initialize connections
- }
-
- @Override
- public void stop() {
- // clean up resources
- }
}
----
@@ -428,3 +220,24 @@ Then reference it via bean or let auto-discovery find it:
from("direct:store")
.to("state-store:myStore?operation=put&backend=#myCustomBackend");
----
+
+=== Sharing a backend across patterns
+
+Because `KeyValueRepository` is the unified SPI, the same implementation can be reused as the
+backend for the Idempotent Consumer and Aggregator EIPs:
+
+[source,java]
+----
+@BindToRegistry("kvRepo")
+public KeyValueRepository kvRepo() {
+ return new MemoryKeyValueRepository();
+}
+----
+
+With a single `KeyValueRepository` in the registry:
+
+* The State Store component auto-discovers and uses it.
+* The Idempotent Consumer EIP auto-discovers and wraps it in a `KeyValueIdempotentRepository`.
+* The Aggregator EIP auto-discovers and wraps it in a `KeyValueAggregationRepository`.
+
+No explicit wiring is needed — all three patterns share the same backend.
diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/InMemoryStateStoreBackend.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/InMemoryStateStoreBackend.java
deleted file mode 100644
index e98511c903a5..000000000000
--- a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/InMemoryStateStoreBackend.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.statestore;
-
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Default in-memory implementation of {@link StateStoreBackend} using a {@link ConcurrentHashMap}. Supports optional
- * TTL with lazy expiry (entries are checked on access, no background thread).
- *
- * @since 4.23
- */
-public class InMemoryStateStoreBackend implements StateStoreBackend {
-
- private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
-
- @Override
- public Object put(String key, Object value, long ttlMillis) {
- long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : 0;
- Entry previous = store.put(key, new Entry(value, expiresAt));
- if (previous != null && !previous.isExpired()) {
- return previous.value();
- }
- return null;
- }
-
- @Override
- public Object get(String key) {
- Entry entry = store.get(key);
- if (entry == null) {
- return null;
- }
- if (entry.isExpired()) {
- store.remove(key, entry);
- return null;
- }
- return entry.value();
- }
-
- @Override
- public Object delete(String key) {
- Entry entry = store.remove(key);
- if (entry != null && !entry.isExpired()) {
- return entry.value();
- }
- return null;
- }
-
- @Override
- public boolean contains(String key) {
- Entry entry = store.get(key);
- if (entry == null) {
- return false;
- }
- if (entry.isExpired()) {
- store.remove(key, entry);
- return false;
- }
- return true;
- }
-
- @Override
- public Object putIfAbsent(String key, Object value, long ttlMillis) {
- long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : 0;
- Object[] result = new Object[1];
- store.compute(key, (k, current) -> {
- if (current == null || current.isExpired()) {
- return new Entry(value, expiresAt);
- }
- result[0] = current.value();
- return current;
- });
- return result[0];
- }
-
- @Override
- public int size() {
- evictExpired();
- return store.size();
- }
-
- @Override
- public Set<String> keys() {
- evictExpired();
- return Set.copyOf(store.keySet());
- }
-
- private void evictExpired() {
- store.entrySet().removeIf(e -> e.getValue().isExpired());
- }
-
- @Override
- public void clear() {
- store.clear();
- }
-
- @Override
- public void start() {
- // no-op
- }
-
- @Override
- public void stop() {
- store.clear();
- }
-
- private record Entry(Object value, long expiresAt) {
-
- boolean isExpired() {
- return expiresAt > 0 && System.currentTimeMillis() > expiresAt;
- }
- }
-}
diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreBackend.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreBackend.java
deleted file mode 100644
index 974f95e4f889..000000000000
--- a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreBackend.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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.statestore;
-
-import java.util.Set;
-
-import org.apache.camel.Service;
-
-/**
- * Interface for pluggable state store backends.
- *
- * @since 4.23
- */
-public interface StateStoreBackend extends Service {
-
- /**
- * Store a value with the given key and optional TTL.
- *
- * @param key the key
- * @param value the value to store
- * @param ttlMillis time-to-live in milliseconds (0 = no expiry)
- * @return the previous value associated with the key, or null
- */
- Object put(String key, Object value, long ttlMillis);
-
- /**
- * Store a value only if the key does not already exist.
- * <p>
- * <b>Note:</b> The default implementation is not atomic. Implementations that require thread safety should override
- * this method with an atomic version (e.g., using the backing store's native putIfAbsent).
- *
- * @param key the key
- * @param value the value to store
- * @param ttlMillis time-to-live in milliseconds (0 = no expiry)
- * @return the existing value if the key already exists, or null if the value was stored
- */
- default Object putIfAbsent(String key, Object value, long ttlMillis) {
- if (contains(key)) {
- return get(key);
- }
- put(key, value, ttlMillis);
- return null;
- }
-
- /**
- * Retrieve the value associated with the given key.
- *
- * @param key the key
- * @return the value, or null if not found or expired
- */
- Object get(String key);
-
- /**
- * Remove the value associated with the given key.
- *
- * @param key the key
- * @return the removed value, or null if not found
- */
- Object delete(String key);
-
- /**
- * Check if a key exists in the store.
- *
- * @param key the key
- * @return true if the key exists and has not expired
- */
- boolean contains(String key);
-
- /**
- * Return all keys in the store.
- *
- * @return a set of all keys
- */
- Set<String> keys();
-
- /**
- * Return the number of entries in the store.
- *
- * @return the number of entries
- */
- default int size() {
- return keys().size();
- }
-
- /**
- * Remove all entries from the store.
- */
- void clear();
-}
diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
index 33bdb526a837..ca7dc43d56f4 100644
--- a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
+++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java
@@ -21,13 +21,16 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.camel.Endpoint;
+import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.MemoryKeyValueRepository;
+import org.apache.camel.support.service.ServiceHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * The State Store component provides a simple, unified key-value store API with pluggable backends.
+ * The State Store component provides a simple, unified key-value store API backed by a {@link KeyValueRepository}.
*
* @since 4.23
*/
@@ -36,7 +39,7 @@ public class StateStoreComponent extends DefaultComponent {
private static final Logger LOG = LoggerFactory.getLogger(StateStoreComponent.class);
- private final ConcurrentHashMap<String, StateStoreBackend> backends = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, KeyValueRepository> backends = new ConcurrentHashMap<>();
@Override
protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
@@ -51,9 +54,9 @@ public class StateStoreComponent extends DefaultComponent {
* already exists for this store name, subsequent calls return the existing one regardless of the explicit backend
* passed.
*/
- StateStoreBackend getOrCreateBackend(String storeName, StateStoreBackend explicitBackend) {
+ KeyValueRepository getOrCreateBackend(String storeName, KeyValueRepository explicitBackend) {
if (explicitBackend != null) {
- StateStoreBackend existing = backends.putIfAbsent(storeName, explicitBackend);
+ KeyValueRepository existing = backends.putIfAbsent(storeName, explicitBackend);
if (existing != null) {
if (existing != explicitBackend) {
LOG.warn("Store '{}' already has a backend ({}). Ignoring explicit backend=# reference ({}).",
@@ -61,31 +64,31 @@ public class StateStoreComponent extends DefaultComponent {
}
return existing;
}
- explicitBackend.start();
+ ServiceHelper.startService(explicitBackend);
return explicitBackend;
}
return backends.computeIfAbsent(storeName, k -> {
- // Auto-discover a StateStoreBackend from the registry
- Set<StateStoreBackend> found = getCamelContext().getRegistry().findByType(StateStoreBackend.class);
- StateStoreBackend backend;
+ // Auto-discover a KeyValueRepository from the registry
+ Set<KeyValueRepository> found = getCamelContext().getRegistry().findByType(KeyValueRepository.class);
+ KeyValueRepository backend;
if (found.size() == 1) {
backend = found.iterator().next();
- LOG.debug("Auto-discovered StateStoreBackend '{}' for store '{}'",
+ LOG.debug("Auto-discovered KeyValueRepository '{}' for store '{}'",
backend.getClass().getSimpleName(), storeName);
} else {
if (found.size() > 1) {
LOG.warn(
- "Found {} StateStoreBackend instances in the registry for store '{}'. "
- + "Cannot auto-select — falling back to InMemoryStateStoreBackend. "
+ "Found {} KeyValueRepository instances in the registry for store '{}'. "
+ + "Cannot auto-select — falling back to MemoryKeyValueRepository. "
+ "Use an explicit backend=# reference to choose one.",
found.size(), storeName);
} else {
- LOG.info("No StateStoreBackend found in the registry for store '{}'. Using InMemoryStateStoreBackend.",
+ LOG.info("No KeyValueRepository found in the registry for store '{}'. Using MemoryKeyValueRepository.",
storeName);
}
- backend = new InMemoryStateStoreBackend();
+ backend = new MemoryKeyValueRepository();
}
- backend.start();
+ ServiceHelper.startService(backend);
return backend;
});
}
@@ -93,9 +96,9 @@ public class StateStoreComponent extends DefaultComponent {
@Override
protected void doStop() throws Exception {
Exception firstException = null;
- for (StateStoreBackend backend : backends.values()) {
+ for (KeyValueRepository backend : backends.values()) {
try {
- backend.stop();
+ ServiceHelper.stopService(backend);
} catch (Exception e) {
LOG.warn("Error stopping backend: {}", e.getMessage(), e);
if (firstException == null) {
diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
index 9b4fc426ec43..62747afce2e9 100644
--- a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
+++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java
@@ -20,6 +20,7 @@ import org.apache.camel.Category;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
+import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
@@ -27,7 +28,7 @@ import org.apache.camel.spi.UriPath;
import org.apache.camel.support.DefaultEndpoint;
/**
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable {@link KeyValueRepository} backend.
*
* @since 4.23
*/
@@ -44,9 +45,9 @@ public class StateStoreEndpoint extends DefaultEndpoint {
@UriParam(description = "The default operation to perform", enums = "put,putIfAbsent,get,delete,contains,keys,size,clear")
private StateStoreOperations operation;
- @UriParam(description = "The backend to use. If not set, auto-discovers a single StateStoreBackend from the registry, or falls back to an in-memory store.",
+ @UriParam(description = "The backend to use. If not set, auto-discovers a single KeyValueRepository from the registry, or falls back to an in-memory store.",
label = "advanced")
- private StateStoreBackend backend;
+ private KeyValueRepository backend;
@UriParam(description = "Time-to-live in milliseconds for entries. 0 means no expiry.", defaultValue = "0")
private long ttl;
@@ -88,11 +89,11 @@ public class StateStoreEndpoint extends DefaultEndpoint {
this.operation = operation;
}
- public StateStoreBackend getBackend() {
+ public KeyValueRepository getBackend() {
return backend;
}
- public void setBackend(StateStoreBackend backend) {
+ public void setBackend(KeyValueRepository backend) {
this.backend = backend;
}
diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreProducer.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreProducer.java
index 4c8a23128595..4f4a175c6b11 100644
--- a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreProducer.java
+++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreProducer.java
@@ -20,10 +20,11 @@ import java.util.Arrays;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.support.DefaultProducer;
/**
- * Producer for the State Store component that performs key-value operations.
+ * Producer for the State Store component that performs key-value operations against a {@link KeyValueRepository}.
*
* @since 4.23
*/
@@ -44,7 +45,7 @@ public class StateStoreProducer extends DefaultProducer {
"No operation specified. Set the operation via URI option or header " + StateStoreConstants.OPERATION);
}
- StateStoreBackend backend = endpoint.getBackend();
+ KeyValueRepository backend = endpoint.getBackend();
long ttl = determineTtl(exchange);
Message message = exchange.getMessage();
diff --git a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
index f16c429d950c..8f1cfba27b4b 100644
--- a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
+++ b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreAutoDiscoveryTest.java
@@ -20,21 +20,23 @@ import java.util.Map;
import org.apache.camel.BindToRegistry;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.support.MemoryKeyValueRepository;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Tests that a single {@link StateStoreBackend} in the registry is auto-discovered when no explicit backend is
+ * Tests that a single {@link KeyValueRepository} in the registry is auto-discovered when no explicit backend is
* specified on the endpoint.
*/
class StateStoreAutoDiscoveryTest extends CamelTestSupport {
- private final TrackingInMemoryBackend trackingBackend = new TrackingInMemoryBackend();
+ private final TrackingKeyValueRepository trackingBackend = new TrackingKeyValueRepository();
@BindToRegistry("myBackend")
- public StateStoreBackend getBackend() {
+ public KeyValueRepository getBackend() {
return trackingBackend;
}
@@ -50,8 +52,8 @@ class StateStoreAutoDiscoveryTest extends CamelTestSupport {
Map.of(StateStoreConstants.KEY, "key1"));
assertThat(result).isEqualTo("hello");
- // Verify our tracking backend was used, not the default InMemoryStateStoreBackend
- assertThat(trackingBackend).isInstanceOf(TrackingInMemoryBackend.class);
+ // Verify our tracking backend was used, not the default MemoryKeyValueRepository
+ assertThat(trackingBackend).isInstanceOf(TrackingKeyValueRepository.class);
assertThat(trackingBackend.keys()).containsExactly("key1");
}
@@ -68,8 +70,8 @@ class StateStoreAutoDiscoveryTest extends CamelTestSupport {
}
/**
- * A simple wrapper around InMemoryStateStoreBackend to verify it was used instead of a fresh default instance.
+ * A simple wrapper around MemoryKeyValueRepository to verify it was used instead of a fresh default instance.
*/
- static class TrackingInMemoryBackend extends InMemoryStateStoreBackend {
+ static class TrackingKeyValueRepository extends MemoryKeyValueRepository {
}
}
diff --git a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreMultiBackendFallbackTest.java b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreMultiBackendFallbackTest.java
index 1795462e00dc..516a80154e00 100644
--- a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreMultiBackendFallbackTest.java
+++ b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreMultiBackendFallbackTest.java
@@ -20,27 +20,29 @@ import java.util.Map;
import org.apache.camel.BindToRegistry;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.MemoryKeyValueRepository;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Tests that when multiple {@link StateStoreBackend} instances are in the registry and no explicit backend is
- * specified, the component falls back to {@link InMemoryStateStoreBackend} and logs a warning.
+ * Tests that when multiple {@link org.apache.camel.spi.KeyValueRepository} instances are in the registry and no
+ * explicit backend is specified, the component falls back to a fresh {@link MemoryKeyValueRepository} and logs a
+ * warning.
*/
class StateStoreMultiBackendFallbackTest extends CamelTestSupport {
@BindToRegistry("backend1")
- private final InMemoryStateStoreBackend backendOne = new InMemoryStateStoreBackend();
+ private final MemoryKeyValueRepository backendOne = new MemoryKeyValueRepository();
@BindToRegistry("backend2")
- private final InMemoryStateStoreBackend backendTwo = new InMemoryStateStoreBackend();
+ private final MemoryKeyValueRepository backendTwo = new MemoryKeyValueRepository();
@Test
void testFallsBackToInMemoryWhenMultipleBackends() {
// With two backends in the registry and no explicit reference,
- // auto-discovery should fall back to a fresh InMemoryStateStoreBackend
+ // auto-discovery should fall back to a fresh MemoryKeyValueRepository
Object previous = template.requestBodyAndHeaders(
"direct:put", "hello",
Map.of(StateStoreConstants.KEY, "key1"));
diff --git a/components/camel-state-store/pom.xml b/components/camel-state-store/pom.xml
index 2e28ec5e33c4..dcfa858ff763 100644
--- a/components/camel-state-store/pom.xml
+++ b/components/camel-state-store/pom.xml
@@ -34,9 +34,6 @@
<modules>
<module>camel-state-store</module>
- <module>camel-state-store-caffeine</module>
- <module>camel-state-store-redis</module>
- <module>camel-state-store-infinispan</module>
</modules>
</project>
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java b/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java
new file mode 100644
index 000000000000..cd7d6f8a1ac6
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/KeyValueRepository.java
@@ -0,0 +1,121 @@
+/*
+ * 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.spi;
+
+import java.util.Set;
+
+import org.apache.camel.Service;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A generic key-value repository SPI that provides a unified abstraction over various state stores.
+ * <p/>
+ * This interface can be used as the single underlying storage mechanism for multiple Camel patterns, including:
+ * <ul>
+ * <li><b>Idempotent Consumer</b> -- via {@code KeyValueIdempotentRepository} adapter</li>
+ * <li><b>Aggregation</b> -- via {@code KeyValueAggregationRepository} adapter</li>
+ * </ul>
+ * Instead of implementing both {@link IdempotentRepository} and {@link AggregationRepository} separately for each
+ * storage technology (Redis, Hazelcast, Infinispan, JDBC, etc.), a single {@code KeyValueRepository} implementation can
+ * be wrapped by the appropriate adapter.
+ * <p/>
+ * Implementations must be thread-safe. Entries may optionally have a time-to-live (TTL); a TTL of {@code 0} or less
+ * means the entry does not expire.
+ *
+ * @since 4.23
+ */
+public interface KeyValueRepository extends Service {
+
+ /**
+ * Retrieves the value associated with the given key.
+ *
+ * @param key the key to look up
+ * @return the value, or {@code null} if the key is not present or has expired
+ */
+ @Nullable
+ Object get(String key);
+
+ /**
+ * Stores a value under the given key with an optional time-to-live.
+ *
+ * @param key the key
+ * @param value the value to store
+ * @param ttlMillis the time-to-live in milliseconds; {@code 0} or negative means no expiration
+ * @return the previous value associated with the key, or {@code null} if there was no mapping
+ */
+ @Nullable
+ Object put(String key, Object value, long ttlMillis);
+
+ /**
+ * Removes the entry for the given key.
+ *
+ * @param key the key to remove
+ * @return the previous value associated with the key, or {@code null} if no mapping existed
+ */
+ @Nullable
+ Object delete(String key);
+
+ /**
+ * Tests whether the repository contains a non-expired entry for the given key.
+ *
+ * @param key the key to test
+ * @return {@code true} if the key is present and has not expired
+ */
+ boolean contains(String key);
+
+ /**
+ * Returns the set of all non-expired keys in the repository.
+ *
+ * @return a snapshot of the current key set
+ */
+ Set<String> keys();
+
+ /**
+ * Removes all entries from the repository.
+ */
+ void clear();
+
+ /**
+ * Stores the value under the given key only if no non-expired mapping already exists.
+ * <p/>
+ * The default implementation is not atomic. Implementations backed by stores that support atomic compare-and-set
+ * operations should override this method for better concurrency guarantees.
+ *
+ * @param key the key
+ * @param value the value to store
+ * @param ttlMillis the time-to-live in milliseconds; {@code 0} or negative means no expiration
+ * @return the existing value if the key was already present, or {@code null} if the put succeeded
+ */
+ @Nullable
+ default Object putIfAbsent(String key, Object value, long ttlMillis) {
+ Object existing = get(key);
+ if (existing != null) {
+ return existing;
+ }
+ put(key, value, ttlMillis);
+ return null;
+ }
+
+ /**
+ * Returns the number of non-expired entries in the repository.
+ *
+ * @return the entry count
+ */
+ default int size() {
+ return keys().size();
+ }
+}
diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
index 7bc663d7283b..1f5d8b856f41 100644
--- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
+++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregateProcessor.java
@@ -54,6 +54,7 @@ import org.apache.camel.processor.BaseProcessorSupport;
import org.apache.camel.spi.AggregationRepository;
import org.apache.camel.spi.ExceptionHandler;
import org.apache.camel.spi.IdAware;
+import org.apache.camel.spi.KeyValueRepository;
import org.apache.camel.spi.OptimisticLockingAggregationRepository;
import org.apache.camel.spi.ReactiveExecutor;
import org.apache.camel.spi.RecoverableAggregationRepository;
@@ -63,6 +64,7 @@ import org.apache.camel.spi.StepIdAware;
import org.apache.camel.spi.Synchronization;
import org.apache.camel.support.DefaultTimeoutMap;
import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.KeyValueAggregationRepository;
import org.apache.camel.support.LRUCacheFactory;
import org.apache.camel.support.LoggingExceptionHandler;
import org.apache.camel.support.NoLock;
@@ -1617,8 +1619,15 @@ public class AggregateProcessor extends BaseProcessorSupport
}
if (aggregationRepository == null) {
- aggregationRepository = new MemoryAggregationRepository(optimisticLocking);
- LOG.info("Defaulting to MemoryAggregationRepository");
+ // Auto-discover a KeyValueRepository from the registry and wrap it
+ KeyValueRepository kvRepo = camelContext.getRegistry().findSingleByType(KeyValueRepository.class);
+ if (kvRepo != null) {
+ aggregationRepository = new KeyValueAggregationRepository(kvRepo);
+ LOG.info("Auto-discovered KeyValueRepository from registry, wrapping as AggregationRepository");
+ } else {
+ aggregationRepository = new MemoryAggregationRepository(optimisticLocking);
+ LOG.info("Defaulting to MemoryAggregationRepository");
+ }
}
if (optimisticLocking) {
diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/IdempotentConsumerReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/IdempotentConsumerReifier.java
index 24e163a2ca83..5773d481f287 100644
--- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/IdempotentConsumerReifier.java
+++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/IdempotentConsumerReifier.java
@@ -23,10 +23,16 @@ import org.apache.camel.model.IdempotentConsumerDefinition;
import org.apache.camel.model.ProcessorDefinition;
import org.apache.camel.processor.idempotent.IdempotentConsumer;
import org.apache.camel.spi.IdempotentRepository;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.support.KeyValueIdempotentRepository;
import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class IdempotentConsumerReifier extends ExpressionReifier<IdempotentConsumerDefinition> {
+ private static final Logger LOG = LoggerFactory.getLogger(IdempotentConsumerReifier.class);
+
public IdempotentConsumerReifier(Route route, ProcessorDefinition<?> definition) {
super(route, IdempotentConsumerDefinition.class.cast(definition));
}
@@ -54,9 +60,13 @@ public class IdempotentConsumerReifier extends ExpressionReifier<IdempotentConsu
}
/**
- * Strategy method to resolve the {@link org.apache.camel.spi.IdempotentRepository} to use
+ * Strategy method to resolve the {@link org.apache.camel.spi.IdempotentRepository} to use.
+ * <p/>
+ * If no explicit {@link IdempotentRepository} is configured (neither as a bean reference nor inline), this method
+ * falls back to looking up a {@link KeyValueRepository} from the Camel registry and wrapping it in a
+ * {@link KeyValueIdempotentRepository} adapter.
*
- * @return the repository
+ * @return the repository, or {@code null} if none could be resolved
*/
protected <T> IdempotentRepository resolveIdempotentRepository() {
IdempotentRepository repo = definition.getIdempotentRepositoryBean();
@@ -64,6 +74,14 @@ public class IdempotentConsumerReifier extends ExpressionReifier<IdempotentConsu
if (repo == null && ref != null) {
repo = mandatoryLookup(ref, IdempotentRepository.class);
}
+ if (repo == null) {
+ // Fallback: auto-discover a KeyValueRepository from the registry
+ KeyValueRepository kvRepo = camelContext.getRegistry().findSingleByType(KeyValueRepository.class);
+ if (kvRepo != null) {
+ LOG.info("Auto-discovered KeyValueRepository from registry, wrapping as IdempotentRepository");
+ repo = new KeyValueIdempotentRepository(kvRepo);
+ }
+ }
return repo;
}
}
diff --git a/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
new file mode 100644
index 000000000000..4debfc8d8ad7
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/support/KeyValueAggregationRepositoryTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class KeyValueAggregationRepositoryTest {
+
+ private CamelContext camelContext;
+ private MemoryKeyValueRepository kvRepository;
+ private KeyValueAggregationRepository aggregationRepository;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ camelContext.start();
+
+ kvRepository = new MemoryKeyValueRepository();
+ kvRepository.start();
+
+ aggregationRepository = new KeyValueAggregationRepository(kvRepository);
+ aggregationRepository.setCamelContext(camelContext);
+ aggregationRepository.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ aggregationRepository.stop();
+ camelContext.stop();
+ }
+
+ @Test
+ void testAddAndGet() {
+ Exchange exchange = createExchange("Hello World");
+
+ aggregationRepository.add(camelContext, "key1", exchange);
+
+ Exchange retrieved = aggregationRepository.get(camelContext, "key1");
+ assertThat(retrieved).isNotNull();
+ assertThat(retrieved.getIn().getBody(String.class)).isEqualTo("Hello World");
+ }
+
+ @Test
+ void testGetMissingKeyReturnsNull() {
+ Exchange retrieved = aggregationRepository.get(camelContext, "nonexistent");
+ assertThat(retrieved).isNull();
+ }
+
+ @Test
+ void testAddReturnsOldExchange() {
+ Exchange first = createExchange("First");
+ Exchange second = createExchange("Second");
+
+ Exchange oldFromFirst = aggregationRepository.add(camelContext, "key1", first);
+ assertThat(oldFromFirst).isNull();
+
+ Exchange oldFromSecond = aggregationRepository.add(camelContext, "key1", second);
+ assertThat(oldFromSecond).isNotNull();
+ assertThat(oldFromSecond.getIn().getBody(String.class)).isEqualTo("First");
+
+ // Current value should be the second exchange
+ Exchange current = aggregationRepository.get(camelContext, "key1");
+ assertThat(current.getIn().getBody(String.class)).isEqualTo("Second");
+ }
+
+ @Test
+ void testRemove() {
+ Exchange exchange = createExchange("Hello");
+ aggregationRepository.add(camelContext, "key1", exchange);
+
+ aggregationRepository.remove(camelContext, "key1", exchange);
+
+ assertThat(aggregationRepository.get(camelContext, "key1")).isNull();
+ }
+
+ @Test
+ void testGetKeys() {
+ aggregationRepository.add(camelContext, "key1", createExchange("One"));
+ aggregationRepository.add(camelContext, "key2", createExchange("Two"));
+ aggregationRepository.add(camelContext, "key3", createExchange("Three"));
+
+ Set<String> keys = aggregationRepository.getKeys();
+
+ assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+ }
+
+ @Test
+ void testGetKeysExcludesCompletedEntries() {
+ Exchange ex1 = createExchange("One");
+ Exchange ex2 = createExchange("Two");
+
+ aggregationRepository.add(camelContext, "key1", ex1);
+ aggregationRepository.add(camelContext, "key2", ex2);
+
+ // Remove key1 (moves to completed for recovery)
+ aggregationRepository.remove(camelContext, "key1", ex1);
+
+ Set<String> keys = aggregationRepository.getKeys();
+
+ // Only active aggregates should be returned
+ assertThat(keys).containsExactly("key2");
+ }
+
+ @Test
+ void testConfirmRemovesCompletedEntry() {
+ Exchange exchange = createExchange("Hello");
+ String exchangeId = exchange.getExchangeId();
+
+ aggregationRepository.add(camelContext, "key1", exchange);
+ aggregationRepository.remove(camelContext, "key1", exchange);
+
+ // The exchange should be recoverable before confirmation
+ Exchange recovered = aggregationRepository.recover(camelContext, exchangeId);
+ assertThat(recovered).isNotNull();
+
+ // After confirmation, it should no longer be recoverable
+ aggregationRepository.confirm(camelContext, exchangeId);
+ assertThat(aggregationRepository.recover(camelContext, exchangeId)).isNull();
+ }
+
+ @Test
+ void testScanReturnsCompletedExchangeIds() {
+ Exchange ex1 = createExchange("One");
+ Exchange ex2 = createExchange("Two");
+
+ aggregationRepository.add(camelContext, "key1", ex1);
+ aggregationRepository.add(camelContext, "key2", ex2);
+
+ // Remove both (move to completed)
+ aggregationRepository.remove(camelContext, "key1", ex1);
+ aggregationRepository.remove(camelContext, "key2", ex2);
+
+ Set<String> scanned = aggregationRepository.scan(camelContext);
+
+ assertThat(scanned).containsExactlyInAnyOrder(ex1.getExchangeId(), ex2.getExchangeId());
+ }
+
+ @Test
+ void testRecoverWithRecoveryDisabled() {
+ aggregationRepository.setUseRecovery(false);
+
+ Exchange exchange = createExchange("Hello");
+ aggregationRepository.add(camelContext, "key1", exchange);
+ aggregationRepository.remove(camelContext, "key1", exchange);
+
+ assertThat(aggregationRepository.recover(camelContext, exchange.getExchangeId())).isNull();
+ }
+
+ @Test
+ void testRecoverySettings() {
+ aggregationRepository.setRecoveryInterval(10000);
+ assertThat(aggregationRepository.getRecoveryInterval()).isEqualTo(10000);
+
+ aggregationRepository.setMaximumRedeliveries(5);
+ assertThat(aggregationRepository.getMaximumRedeliveries()).isEqualTo(5);
+
+ aggregationRepository.setDeadLetterUri("mock:dead");
+ assertThat(aggregationRepository.getDeadLetterUri()).isEqualTo("mock:dead");
+
+ aggregationRepository.setUseRecovery(false);
+ assertThat(aggregationRepository.isUseRecovery()).isFalse();
+ }
+
+ @Test
+ void testExchangeHeadersArePreserved() {
+ Exchange exchange = createExchange("Body");
+ exchange.getIn().setHeader("myHeader", "headerValue");
+ exchange.getIn().setHeader("myNumber", 42);
+
+ aggregationRepository.add(camelContext, "key1", exchange);
+
+ Exchange retrieved = aggregationRepository.get(camelContext, "key1");
+ assertThat(retrieved.getIn().getHeader("myHeader", String.class)).isEqualTo("headerValue");
+ assertThat(retrieved.getIn().getHeader("myNumber", Integer.class)).isEqualTo(42);
+ }
+
+ @Test
+ void testFactoryMethod() {
+ KeyValueAggregationRepository created
+ = KeyValueAggregationRepository.keyValueAggregationRepository(kvRepository);
+
+ assertThat(created).isNotNull();
+ assertThat(created.getRepository()).isSameAs(kvRepository);
+ }
+
+ private Exchange createExchange(String body) {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getIn().setBody(body);
+ return exchange;
+ }
+}
diff --git a/core/camel-support/pom.xml b/core/camel-support/pom.xml
index 1d41d48c2436..a4c1793693ff 100644
--- a/core/camel-support/pom.xml
+++ b/core/camel-support/pom.xml
@@ -71,6 +71,17 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>${awaitility-version}</version>
+ <scope>test</scope>
+ </dependency>
<!-- logging -->
<dependency>
diff --git a/core/camel-support/src/generated/java/org/apache/camel/support/MemoryKeyValueRepositoryConfigurer.java b/core/camel-support/src/generated/java/org/apache/camel/support/MemoryKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..c78589d14375
--- /dev/null
+++ b/core/camel-support/src/generated/java/org/apache/camel/support/MemoryKeyValueRepositoryConfigurer.java
@@ -0,0 +1,37 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.support;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.support.MemoryKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class MemoryKeyValueRepositoryConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) {
+ return false;
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ return null;
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ return null;
+ }
+}
+
diff --git a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 6ab75a7fb32f..2b3850b73b6b 100644
--- a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-bean=AcceptAllHeaderFilterStrategy DefaultHeaderFilterStrategy FileIdempotentRepository MemoryIdempotentRepository ThrottlingExceptionRoutePolicy ThrottlingInflightRoutePolicy
+bean=AcceptAllHeaderFilterStrategy DefaultHeaderFilterStrategy FileIdempotentRepository MemoryIdempotentRepository MemoryKeyValueRepository ThrottlingExceptionRoutePolicy ThrottlingInflightRoutePolicy
groupId=org.apache.camel
artifactId=camel-support
version=4.23.0-SNAPSHOT
diff --git a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/MemoryKeyValueRepository.json b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/MemoryKeyValueRepository.json
new file mode 100644
index 000000000000..4630dec20634
--- /dev/null
+++ b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/bean/MemoryKeyValueRepository.json
@@ -0,0 +1,15 @@
+{
+ "bean": {
+ "kind": "bean",
+ "name": "MemoryKeyValueRepository",
+ "javaType": "org.apache.camel.support.MemoryKeyValueRepository",
+ "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+ "title": "Memory Key Value Repository",
+ "description": "An in-memory KeyValueRepository with optional TTL support.",
+ "deprecated": false,
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-support",
+ "version": "4.23.0-SNAPSHOT"
+ }
+}
+
diff --git a/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.MemoryKeyValueRepository b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.MemoryKeyValueRepository
new file mode 100644
index 000000000000..8ef5d9621fee
--- /dev/null
+++ b/core/camel-support/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.support.MemoryKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.support.MemoryKeyValueRepositoryConfigurer
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
new file mode 100644
index 000000000000..da6dbb6ac269
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueAggregationRepository.java
@@ -0,0 +1,238 @@
+/*
+ * 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.support;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.RecoverableAggregationRepository;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An adapter that implements {@link RecoverableAggregationRepository} by delegating to a {@link KeyValueRepository}.
+ * <p/>
+ * Exchanges are serialized and deserialized using {@link DefaultExchangeHolder}. Active (in-progress) aggregates are
+ * stored under the correlation key. Completed-but-unconfirmed aggregates (for recovery) are stored under a separate key
+ * derived from the exchange ID.
+ * <p/>
+ * This allows any {@link KeyValueRepository} implementation to be used as an aggregation repository without
+ * implementing the {@link org.apache.camel.spi.AggregationRepository} interface directly.
+ *
+ * @since 4.23
+ */
+public class KeyValueAggregationRepository extends ServiceSupport
+ implements RecoverableAggregationRepository, CamelContextAware {
+
+ private static final Logger LOG = LoggerFactory.getLogger(KeyValueAggregationRepository.class);
+
+ /**
+ * Prefix used to namespace active aggregate entries, preventing key collisions when this adapter shares the same
+ * {@link KeyValueRepository} with other adapters (e.g. {@link KeyValueIdempotentRepository}).
+ */
+ private static final String AGGREGATE_PREFIX = "aggregate:";
+
+ /**
+ * Prefix used to separate completed (pending confirmation) entries from active aggregates in the underlying store.
+ */
+ private static final String COMPLETED_PREFIX = "completed:";
+
+ private final KeyValueRepository repository;
+ private CamelContext camelContext;
+ private boolean useRecovery = true;
+ private String deadLetterUri;
+ private long recoveryInterval = 5000;
+ private int maximumRedeliveries = 3;
+ private boolean allowSerializedHeaders;
+
+ /**
+ * Creates an aggregation repository adapter backed by the given key-value repository.
+ *
+ * @param repository the underlying key-value repository; must not be {@code null}
+ */
+ public KeyValueAggregationRepository(KeyValueRepository repository) {
+ ObjectHelper.notNull(repository, "repository");
+ this.repository = repository;
+ }
+
+ /**
+ * Creates a new {@link KeyValueAggregationRepository} wrapping the given {@link KeyValueRepository}.
+ *
+ * @param repository the underlying key-value repository
+ * @return the adapter
+ */
+ public static KeyValueAggregationRepository keyValueAggregationRepository(KeyValueRepository repository) {
+ return new KeyValueAggregationRepository(repository);
+ }
+
+ @Override
+ public Exchange add(CamelContext camelContext, String key, Exchange exchange) {
+ LOG.trace("Adding an Exchange with ID {} for key {}", exchange.getExchangeId(), key);
+ DefaultExchangeHolder newHolder = DefaultExchangeHolder.marshal(exchange, true, allowSerializedHeaders);
+ DefaultExchangeHolder oldHolder = (DefaultExchangeHolder) repository.put(AGGREGATE_PREFIX + key, newHolder, 0);
+ return unmarshallExchange(camelContext, oldHolder);
+ }
+
+ @Override
+ public Exchange get(CamelContext camelContext, String key) {
+ return unmarshallExchange(camelContext, (DefaultExchangeHolder) repository.get(AGGREGATE_PREFIX + key));
+ }
+
+ @Override
+ public void remove(CamelContext camelContext, String key, Exchange exchange) {
+ LOG.trace("Removing an Exchange with ID {} for key {}", exchange.getExchangeId(), key);
+ DefaultExchangeHolder holder = (DefaultExchangeHolder) repository.delete(AGGREGATE_PREFIX + key);
+ if (useRecovery && holder != null) {
+ // Store under the exchangeId for potential recovery
+ LOG.trace("Moving Exchange with ID {} to completed (pending confirmation)", exchange.getExchangeId());
+ repository.put(COMPLETED_PREFIX + exchange.getExchangeId(), holder, 0);
+ }
+ }
+
+ @Override
+ public void confirm(CamelContext camelContext, String exchangeId) {
+ LOG.trace("Confirming an Exchange with ID {}", exchangeId);
+ repository.delete(COMPLETED_PREFIX + exchangeId);
+ }
+
+ @Override
+ public Set<String> getKeys() {
+ return Collections.unmodifiableSet(
+ repository.keys().stream()
+ .filter(k -> k.startsWith(AGGREGATE_PREFIX))
+ .map(k -> k.substring(AGGREGATE_PREFIX.length()))
+ .collect(Collectors.toSet()));
+ }
+
+ @Override
+ public Set<String> scan(CamelContext camelContext) {
+ LOG.trace("Scanning for exchanges to recover in {} context", camelContext.getName());
+ Set<String> scanned = repository.keys().stream()
+ .filter(k -> k.startsWith(COMPLETED_PREFIX))
+ .map(k -> k.substring(COMPLETED_PREFIX.length()))
+ .collect(Collectors.toUnmodifiableSet());
+ LOG.trace("Found {} keys for exchanges to recover in {} context", scanned.size(), camelContext.getName());
+ return scanned;
+ }
+
+ @Override
+ public Exchange recover(CamelContext camelContext, String exchangeId) {
+ LOG.trace("Recovering an Exchange with ID {}", exchangeId);
+ if (!useRecovery) {
+ return null;
+ }
+ return unmarshallExchange(camelContext, (DefaultExchangeHolder) repository.get(COMPLETED_PREFIX + exchangeId));
+ }
+
+ @Override
+ public void setRecoveryInterval(long interval, TimeUnit timeUnit) {
+ this.recoveryInterval = timeUnit.toMillis(interval);
+ }
+
+ @Override
+ public void setRecoveryInterval(long interval) {
+ this.recoveryInterval = interval;
+ }
+
+ @Override
+ public long getRecoveryInterval() {
+ return recoveryInterval;
+ }
+
+ @Override
+ public void setUseRecovery(boolean useRecovery) {
+ this.useRecovery = useRecovery;
+ }
+
+ @Override
+ public boolean isUseRecovery() {
+ return useRecovery;
+ }
+
+ @Override
+ public void setDeadLetterUri(String deadLetterUri) {
+ this.deadLetterUri = deadLetterUri;
+ }
+
+ @Override
+ public String getDeadLetterUri() {
+ return deadLetterUri;
+ }
+
+ @Override
+ public void setMaximumRedeliveries(int maximumRedeliveries) {
+ this.maximumRedeliveries = maximumRedeliveries;
+ }
+
+ @Override
+ public int getMaximumRedeliveries() {
+ return maximumRedeliveries;
+ }
+
+ public boolean isAllowSerializedHeaders() {
+ return allowSerializedHeaders;
+ }
+
+ public void setAllowSerializedHeaders(boolean allowSerializedHeaders) {
+ this.allowSerializedHeaders = allowSerializedHeaders;
+ }
+
+ /**
+ * Returns the underlying {@link KeyValueRepository}.
+ */
+ public KeyValueRepository getRepository() {
+ return repository;
+ }
+
+ @Override
+ public CamelContext getCamelContext() {
+ return camelContext;
+ }
+
+ @Override
+ public void setCamelContext(CamelContext camelContext) {
+ this.camelContext = camelContext;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ ServiceHelper.startService(repository);
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ ServiceHelper.stopService(repository);
+ }
+
+ private Exchange unmarshallExchange(CamelContext camelContext, DefaultExchangeHolder holder) {
+ if (holder == null) {
+ return null;
+ }
+ Exchange exchange = new DefaultExchange(camelContext);
+ DefaultExchangeHolder.unmarshal(exchange, holder);
+ return exchange;
+ }
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java
new file mode 100644
index 000000000000..d08e4fc38418
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/KeyValueIdempotentRepository.java
@@ -0,0 +1,114 @@
+/*
+ * 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.support;
+
+import org.apache.camel.spi.IdempotentRepository;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * An adapter that implements {@link IdempotentRepository} by delegating to a {@link KeyValueRepository}.
+ * <p/>
+ * This adapter uses {@link Boolean#TRUE} as the marker value stored in the underlying key-value repository. The key
+ * itself is the message identifier used for duplicate detection.
+ * <p/>
+ * This allows any {@link KeyValueRepository} implementation (e.g. backed by Redis, Infinispan, JDBC, etc.) to be used
+ * as an idempotent repository without implementing the {@link IdempotentRepository} interface directly.
+ *
+ * @since 4.23
+ */
+public class KeyValueIdempotentRepository extends ServiceSupport implements IdempotentRepository {
+
+ /**
+ * Prefix used to namespace idempotent entries, preventing key collisions when this adapter shares the same
+ * {@link KeyValueRepository} with other adapters (e.g. {@link KeyValueAggregationRepository}).
+ */
+ private static final String IDEMPOTENT_PREFIX = "idempotent:";
+
+ private final KeyValueRepository repository;
+
+ /**
+ * Creates an idempotent repository adapter backed by the given key-value repository.
+ *
+ * @param repository the underlying key-value repository; must not be {@code null}
+ */
+ public KeyValueIdempotentRepository(KeyValueRepository repository) {
+ ObjectHelper.notNull(repository, "repository");
+ this.repository = repository;
+ }
+
+ /**
+ * Creates a new {@link KeyValueIdempotentRepository} wrapping the given {@link KeyValueRepository}.
+ *
+ * @param repository the underlying key-value repository
+ * @return the adapter
+ */
+ public static KeyValueIdempotentRepository keyValueIdempotentRepository(KeyValueRepository repository) {
+ return new KeyValueIdempotentRepository(repository);
+ }
+
+ @Override
+ public boolean add(String key) {
+ // putIfAbsent returns null if the key was successfully added (not already present)
+ return repository.putIfAbsent(IDEMPOTENT_PREFIX + key, Boolean.TRUE, 0) == null;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return repository.contains(IDEMPOTENT_PREFIX + key);
+ }
+
+ @Override
+ public boolean remove(String key) {
+ return repository.delete(IDEMPOTENT_PREFIX + key) != null;
+ }
+
+ @Override
+ public boolean confirm(String key) {
+ // noop -- the key was already added
+ return true;
+ }
+
+ @Override
+ public void clear() {
+ // Only clear entries with our prefix to avoid affecting other adapters sharing the repository
+ for (String key : repository.keys()) {
+ if (key.startsWith(IDEMPOTENT_PREFIX)) {
+ repository.delete(key);
+ }
+ }
+ }
+
+ /**
+ * Returns the underlying {@link KeyValueRepository}.
+ */
+ public KeyValueRepository getRepository() {
+ return repository;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ ServiceHelper.startService(repository);
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ ServiceHelper.stopService(repository);
+ }
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
new file mode 100644
index 000000000000..f32527ea9094
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/MemoryKeyValueRepository.java
@@ -0,0 +1,198 @@
+/*
+ * 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.support;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.service.ServiceSupport;
+
+/**
+ * An in-memory implementation of {@link KeyValueRepository} backed by a {@link ConcurrentHashMap} with lazy TTL
+ * eviction.
+ * <p/>
+ * Expired entries are removed lazily on access ({@link #get}, {@link #contains}) and periodically during
+ * {@link #keys()} and {@link #size()} scans. This avoids the overhead of a background eviction thread while still
+ * ensuring that expired data is not returned.
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+ description = "An in-memory KeyValueRepository with optional TTL support.",
+ annotations = { "interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Memory based key-value repository")
+public class MemoryKeyValueRepository extends ServiceSupport implements KeyValueRepository {
+
+ private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
+
+ /**
+ * Creates a new in-memory key-value repository.
+ */
+ public MemoryKeyValueRepository() {
+ }
+
+ @Override
+ @ManagedOperation(description = "Get value by key")
+ public Object get(String key) {
+ Entry entry = store.get(key);
+ if (entry == null) {
+ return null;
+ }
+ if (entry.isExpired()) {
+ store.remove(key, entry);
+ return null;
+ }
+ return entry.value();
+ }
+
+ @Override
+ @ManagedOperation(description = "Put a key-value pair with optional TTL")
+ public Object put(String key, Object value, long ttlMillis) {
+ long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : Long.MAX_VALUE;
+ Entry previous = store.put(key, new Entry(value, expiresAt));
+ if (previous == null) {
+ return null;
+ }
+ if (previous.isExpired()) {
+ return null;
+ }
+ return previous.value();
+ }
+
+ @Override
+ @ManagedOperation(description = "Delete a key")
+ public Object delete(String key) {
+ Entry entry = store.remove(key);
+ if (entry == null) {
+ return null;
+ }
+ if (entry.isExpired()) {
+ return null;
+ }
+ return entry.value();
+ }
+
+ @Override
+ @ManagedOperation(description = "Check if key exists")
+ public boolean contains(String key) {
+ Entry entry = store.get(key);
+ if (entry == null) {
+ return false;
+ }
+ if (entry.isExpired()) {
+ store.remove(key, entry);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public Set<String> keys() {
+ evictExpired();
+ return store.keySet().stream()
+ .filter(k -> {
+ Entry e = store.get(k);
+ return e != null && !e.isExpired();
+ })
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ @Override
+ @ManagedOperation(description = "Clear all entries")
+ public void clear() {
+ store.clear();
+ }
+
+ @Override
+ public Object putIfAbsent(String key, Object value, long ttlMillis) {
+ long expiresAt = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : Long.MAX_VALUE;
+ Entry newEntry = new Entry(value, expiresAt);
+ Entry existing = store.putIfAbsent(key, newEntry);
+ if (existing == null) {
+ return null;
+ }
+ if (existing.isExpired()) {
+ // Try to replace the expired entry atomically
+ if (store.replace(key, existing, newEntry)) {
+ return null;
+ }
+ // Another thread beat us -- read what they stored
+ Entry updated = store.get(key);
+ return updated != null && !updated.isExpired() ? updated.value() : null;
+ }
+ return existing.value();
+ }
+
+ @Override
+ @ManagedAttribute(description = "The number of entries in the repository")
+ public int size() {
+ evictExpired();
+ return store.size();
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ store.clear();
+ }
+
+ private void evictExpired() {
+ Iterator<Map.Entry<String, Entry>> it = store.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, Entry> mapEntry = it.next();
+ if (mapEntry.getValue().isExpired()) {
+ it.remove();
+ }
+ }
+ }
+
+ /**
+ * An entry in the repository holding the value and its expiration timestamp.
+ */
+ static final class Entry implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private final Object value;
+ private final long expiresAt;
+
+ Entry(Object value, long expiresAt) {
+ this.value = value;
+ this.expiresAt = expiresAt;
+ }
+
+ Object value() {
+ return value;
+ }
+
+ boolean isExpired() {
+ return System.currentTimeMillis() >= expiresAt;
+ }
+ }
+}
diff --git a/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java
new file mode 100644
index 000000000000..6148fb5444ac
--- /dev/null
+++ b/core/camel-support/src/test/java/org/apache/camel/support/KeyValueIdempotentRepositoryTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.support;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class KeyValueIdempotentRepositoryTest {
+
+ private MemoryKeyValueRepository kvRepository;
+ private KeyValueIdempotentRepository idempotentRepository;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ kvRepository = new MemoryKeyValueRepository();
+ kvRepository.start();
+ idempotentRepository = new KeyValueIdempotentRepository(kvRepository);
+ idempotentRepository.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ idempotentRepository.stop();
+ }
+
+ @Test
+ void testAddNewKey() {
+ assertThat(idempotentRepository.add("msg-001")).isTrue();
+ }
+
+ @Test
+ void testAddDuplicateKey() {
+ idempotentRepository.add("msg-001");
+
+ assertThat(idempotentRepository.add("msg-001")).isFalse();
+ }
+
+ @Test
+ void testContains() {
+ assertThat(idempotentRepository.contains("msg-001")).isFalse();
+
+ idempotentRepository.add("msg-001");
+
+ assertThat(idempotentRepository.contains("msg-001")).isTrue();
+ }
+
+ @Test
+ void testRemove() {
+ idempotentRepository.add("msg-001");
+
+ assertThat(idempotentRepository.remove("msg-001")).isTrue();
+ assertThat(idempotentRepository.contains("msg-001")).isFalse();
+ }
+
+ @Test
+ void testRemoveNonExistentKey() {
+ assertThat(idempotentRepository.remove("msg-001")).isFalse();
+ }
+
+ @Test
+ void testConfirmIsNoop() {
+ idempotentRepository.add("msg-001");
+
+ assertThat(idempotentRepository.confirm("msg-001")).isTrue();
+ // Key should still be present after confirm
+ assertThat(idempotentRepository.contains("msg-001")).isTrue();
+ }
+
+ @Test
+ void testClear() {
+ idempotentRepository.add("msg-001");
+ idempotentRepository.add("msg-002");
+ idempotentRepository.add("msg-003");
+
+ idempotentRepository.clear();
+
+ assertThat(idempotentRepository.contains("msg-001")).isFalse();
+ assertThat(idempotentRepository.contains("msg-002")).isFalse();
+ assertThat(idempotentRepository.contains("msg-003")).isFalse();
+ }
+
+ @Test
+ void testAddAfterRemoveSucceeds() {
+ idempotentRepository.add("msg-001");
+ idempotentRepository.remove("msg-001");
+
+ // Should be able to add the key again
+ assertThat(idempotentRepository.add("msg-001")).isTrue();
+ }
+
+ @Test
+ void testMultipleKeys() {
+ assertThat(idempotentRepository.add("msg-001")).isTrue();
+ assertThat(idempotentRepository.add("msg-002")).isTrue();
+ assertThat(idempotentRepository.add("msg-003")).isTrue();
+
+ assertThat(idempotentRepository.contains("msg-001")).isTrue();
+ assertThat(idempotentRepository.contains("msg-002")).isTrue();
+ assertThat(idempotentRepository.contains("msg-003")).isTrue();
+
+ // Adding duplicates should fail
+ assertThat(idempotentRepository.add("msg-001")).isFalse();
+ assertThat(idempotentRepository.add("msg-002")).isFalse();
+ }
+
+ @Test
+ void testStoresMarkerValueInUnderlyingRepository() {
+ idempotentRepository.add("msg-001");
+
+ // The underlying KV repository should have the prefixed key with Boolean.TRUE as value
+ assertThat(kvRepository.get("idempotent:msg-001")).isEqualTo(Boolean.TRUE);
+ }
+
+ @Test
+ void testGetRepository() {
+ assertThat(idempotentRepository.getRepository()).isSameAs(kvRepository);
+ }
+
+ @Test
+ void testFactoryMethod() {
+ KeyValueIdempotentRepository created
+ = KeyValueIdempotentRepository.keyValueIdempotentRepository(kvRepository);
+
+ assertThat(created).isNotNull();
+ assertThat(created.getRepository()).isSameAs(kvRepository);
+ }
+
+ @Test
+ void testClearDoesNotAffectOtherPrefixes() {
+ // Simulate another adapter storing entries under a different prefix
+ kvRepository.put("aggregate:order-1", "exchange-holder", 0);
+
+ // Add idempotent entries and clear them
+ idempotentRepository.add("msg-001");
+ idempotentRepository.add("msg-002");
+ idempotentRepository.clear();
+
+ // Idempotent entries should be gone
+ assertThat(idempotentRepository.contains("msg-001")).isFalse();
+ assertThat(idempotentRepository.contains("msg-002")).isFalse();
+
+ // But the aggregate entry should still be there
+ assertThat(kvRepository.get("aggregate:order-1")).isEqualTo("exchange-holder");
+ }
+
+ @Test
+ void testKeyIsolationFromOtherAdapters() {
+ // Store an idempotent entry with key "order-1"
+ idempotentRepository.add("order-1");
+
+ // A different adapter storing under its own prefix should not collide
+ kvRepository.put("aggregate:order-1", "exchange-data", 0);
+
+ // The idempotent entry should still resolve correctly
+ assertThat(idempotentRepository.contains("order-1")).isTrue();
+
+ // And the underlying store has both entries under distinct prefixed keys
+ assertThat(kvRepository.get("idempotent:order-1")).isEqualTo(Boolean.TRUE);
+ assertThat(kvRepository.get("aggregate:order-1")).isEqualTo("exchange-data");
+ }
+}
diff --git a/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java b/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..f8f9465cb255
--- /dev/null
+++ b/core/camel-support/src/test/java/org/apache/camel/support/MemoryKeyValueRepositoryTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.support;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class MemoryKeyValueRepositoryTest {
+
+ private MemoryKeyValueRepository repository;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ repository = new MemoryKeyValueRepository();
+ repository.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ repository.stop();
+ }
+
+ @Test
+ void testPutAndGet() {
+ repository.put("key1", "value1", 0);
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ }
+
+ @Test
+ void testGetMissingKeyReturnsNull() {
+ assertThat(repository.get("nonexistent")).isNull();
+ }
+
+ @Test
+ void testPutOverwritesExistingValue() {
+ repository.put("key1", "value1", 0);
+ repository.put("key1", "value2", 0);
+
+ assertThat(repository.get("key1")).isEqualTo("value2");
+ }
+
+ @Test
+ void testDelete() {
+ repository.put("key1", "value1", 0);
+
+ Object deleted = repository.delete("key1");
+
+ assertThat(deleted).isEqualTo("value1");
+ assertThat(repository.get("key1")).isNull();
+ }
+
+ @Test
+ void testDeleteMissingKeyReturnsNull() {
+ assertThat(repository.delete("nonexistent")).isNull();
+ }
+
+ @Test
+ void testContains() {
+ repository.put("key1", "value1", 0);
+
+ assertThat(repository.contains("key1")).isTrue();
+ assertThat(repository.contains("nonexistent")).isFalse();
+ }
+
+ @Test
+ void testKeys() {
+ repository.put("key1", "value1", 0);
+ repository.put("key2", "value2", 0);
+ repository.put("key3", "value3", 0);
+
+ Set<String> keys = repository.keys();
+
+ assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+ }
+
+ @Test
+ void testKeysEmpty() {
+ assertThat(repository.keys()).isEmpty();
+ }
+
+ @Test
+ void testClear() {
+ repository.put("key1", "value1", 0);
+ repository.put("key2", "value2", 0);
+
+ repository.clear();
+
+ assertThat(repository.size()).isZero();
+ assertThat(repository.get("key1")).isNull();
+ assertThat(repository.get("key2")).isNull();
+ }
+
+ @Test
+ void testSize() {
+ assertThat(repository.size()).isZero();
+
+ repository.put("key1", "value1", 0);
+ assertThat(repository.size()).isEqualTo(1);
+
+ repository.put("key2", "value2", 0);
+ assertThat(repository.size()).isEqualTo(2);
+
+ repository.delete("key1");
+ assertThat(repository.size()).isEqualTo(1);
+ }
+
+ @Test
+ void testPutIfAbsentNewKey() {
+ Object result = repository.putIfAbsent("key1", "value1", 0);
+
+ assertThat(result).isNull();
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ }
+
+ @Test
+ void testPutIfAbsentExistingKey() {
+ repository.put("key1", "value1", 0);
+
+ Object result = repository.putIfAbsent("key1", "value2", 0);
+
+ assertThat(result).isEqualTo("value1");
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ }
+
+ @Test
+ void testTtlExpiration() {
+ // Use a very short TTL
+ repository.put("key1", "value1", 50);
+
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+
+ // Wait for the entry to expire
+ await().atMost(500, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ assertThat(repository.get("key1")).isNull();
+ assertThat(repository.contains("key1")).isFalse();
+ });
+ }
+
+ @Test
+ void testTtlExpirationOnKeys() {
+ repository.put("key1", "value1", 50);
+ repository.put("key2", "value2", 0); // no expiration
+
+ await().atMost(500, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ assertThat(repository.keys()).containsExactly("key2");
+ assertThat(repository.size()).isEqualTo(1);
+ });
+ }
+
+ @Test
+ void testTtlExpirationOnDelete() {
+ repository.put("key1", "value1", 50);
+
+ await().atMost(500, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ // Deleting an expired entry should return null
+ assertThat(repository.delete("key1")).isNull();
+ });
+ }
+
+ @Test
+ void testPutIfAbsentWithExpiredEntry() {
+ repository.put("key1", "value1", 50);
+
+ await().atMost(500, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ // The entry has expired, so putIfAbsent should succeed
+ Object result = repository.putIfAbsent("key1", "value2", 0);
+ assertThat(result).isNull();
+ assertThat(repository.get("key1")).isEqualTo("value2");
+ });
+ }
+
+ @Test
+ void testNoTtlWithZero() {
+ repository.put("key1", "value1", 0);
+
+ // Entry with TTL=0 should not expire
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ }
+
+ @Test
+ void testNoTtlWithNegative() {
+ repository.put("key1", "value1", -1);
+
+ // Entry with negative TTL should not expire
+ assertThat(repository.get("key1")).isEqualTo("value1");
+ assertThat(repository.contains("key1")).isTrue();
+ }
+
+ @Test
+ void testStoresDifferentValueTypes() {
+ repository.put("string", "hello", 0);
+ repository.put("integer", 42, 0);
+ repository.put("boolean", Boolean.TRUE, 0);
+
+ assertThat(repository.get("string")).isEqualTo("hello");
+ assertThat(repository.get("integer")).isEqualTo(42);
+ assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE);
+ }
+}
diff --git a/docs/components/modules/others/examples/json/state-store-caffeine.json b/docs/components/modules/others/examples/json/state-store-caffeine.json
deleted file mode 120000
index 15277f9fe668..000000000000
--- a/docs/components/modules/others/examples/json/state-store-caffeine.json
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../components/camel-state-store/camel-state-store-caffeine/src/generated/resources/state-store-caffeine.json
\ No newline at end of file
diff --git a/docs/components/modules/others/examples/json/state-store-infinispan.json b/docs/components/modules/others/examples/json/state-store-infinispan.json
deleted file mode 120000
index 9e22e40328f2..000000000000
--- a/docs/components/modules/others/examples/json/state-store-infinispan.json
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../components/camel-state-store/camel-state-store-infinispan/src/generated/resources/state-store-infinispan.json
\ No newline at end of file
diff --git a/docs/components/modules/others/examples/json/state-store-redis.json b/docs/components/modules/others/examples/json/state-store-redis.json
deleted file mode 120000
index aac04d819206..000000000000
--- a/docs/components/modules/others/examples/json/state-store-redis.json
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../components/camel-state-store/camel-state-store-redis/src/generated/resources/state-store-redis.json
\ No newline at end of file
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
index 8b309c3d2a11..11a2db40080a 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
@@ -4820,7 +4820,8 @@ public interface ComponentsBuilderFactory {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
index c7f95c5694d0..cb2444613756 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java
@@ -24,7 +24,7 @@ import org.apache.camel.builder.component.ComponentBuilder;
import org.apache.camel.component.statestore.StateStoreComponent;
/**
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository backend.
*
* Generated by camel build tools - do NOT edit this file!
*/
@@ -33,7 +33,8 @@ public interface StateStoreComponentBuilderFactory {
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java
index 6a742870d0d2..55ee8629deff 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java
@@ -3696,7 +3696,8 @@ public class EndpointHeaderBuilders {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
index 74ec4d16bca6..13705f86aa04 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
@@ -16109,7 +16109,8 @@ public class StaticEndpointBuilders {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
@@ -16128,7 +16129,8 @@ public class StaticEndpointBuilders {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
index 3e9d10aaf56a..884023f13c62 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java
@@ -27,7 +27,7 @@ import org.apache.camel.builder.EndpointProducerBuilder;
import org.apache.camel.builder.endpoint.AbstractEndpointBuilder;
/**
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository backend.
*
* Generated by camel build tools - do NOT edit this file!
*/
@@ -164,11 +164,10 @@ public interface StateStoreEndpointBuilderFactory {
}
/**
* The backend to use. If not set, auto-discovers a single
- * StateStoreBackend from the registry, or falls back to an in-memory
+ * KeyValueRepository from the registry, or falls back to an in-memory
* store.
*
- * The option is a:
- * <code>org.apache.camel.component.statestore.StateStoreBackend</code>
+ * The option is a: <code>org.apache.camel.spi.KeyValueRepository</code>
* type.
*
* Group: advanced
@@ -176,18 +175,17 @@ public interface StateStoreEndpointBuilderFactory {
* @param backend the value to set
* @return the dsl builder
*/
- default AdvancedStateStoreEndpointBuilder backend(org.apache.camel.component.statestore.StateStoreBackend backend) {
+ default AdvancedStateStoreEndpointBuilder backend(org.apache.camel.spi.KeyValueRepository backend) {
doSetProperty("backend", backend);
return this;
}
/**
* The backend to use. If not set, auto-discovers a single
- * StateStoreBackend from the registry, or falls back to an in-memory
+ * KeyValueRepository from the registry, or falls back to an in-memory
* store.
*
* The option will be converted to a
- * <code>org.apache.camel.component.statestore.StateStoreBackend</code>
- * type.
+ * <code>org.apache.camel.spi.KeyValueRepository</code> type.
*
* Group: advanced
*
@@ -203,7 +201,8 @@ public interface StateStoreEndpointBuilderFactory {
public interface StateStoreBuilders {
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
@@ -216,7 +215,8 @@ public interface StateStoreEndpointBuilderFactory {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
@@ -235,7 +235,8 @@ public interface StateStoreEndpointBuilderFactory {
}
/**
* State Store (camel-state-store)
- * Perform key-value operations against a pluggable state store backend.
+ * Perform key-value operations against a pluggable KeyValueRepository
+ * backend.
*
* Category: cache
* Since: 4.23
diff --git a/parent/pom.xml b/parent/pom.xml
index e14c7018b785..818610c8b294 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -2810,21 +2810,6 @@
<artifactId>camel-state-store</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-caffeine</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-infinispan</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-state-store-redis</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-stax</artifactId>