diff --git a/bom/camel-bom/pom.xml b/bom/camel-bom/pom.xml index 46f420cbe4fd5..c5565a4c5616c 100644 --- a/bom/camel-bom/pom.xml +++ b/bom/camel-bom/pom.xml @@ -2257,6 +2257,26 @@ camel-ssh 4.19.0-SNAPSHOT + + org.apache.camel + camel-state-store + 4.19.0-SNAPSHOT + + + org.apache.camel + camel-state-store-caffeine + 4.19.0-SNAPSHOT + + + org.apache.camel + camel-state-store-infinispan + 4.19.0-SNAPSHOT + + + org.apache.camel + camel-state-store-redis + 4.19.0-SNAPSHOT + org.apache.camel camel-stax diff --git a/catalog/camel-allcomponents/pom.xml b/catalog/camel-allcomponents/pom.xml index 7f8782e412b68..c3924a1231262 100644 --- a/catalog/camel-allcomponents/pom.xml +++ b/catalog/camel-allcomponents/pom.xml @@ -2037,6 +2037,26 @@ camel-ssh ${project.version} + + org.apache.camel + camel-state-store + ${project.version} + + + org.apache.camel + camel-state-store-caffeine + ${project.version} + + + org.apache.camel + camel-state-store-infinispan + ${project.version} + + + org.apache.camel + camel-state-store-redis + ${project.version} + org.apache.camel camel-stax diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components.properties index 42086ed03ffa9..976216400d54b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components.properties @@ -351,6 +351,7 @@ spring-ws sql sql-stored ssh +state-store stax stitch stomp 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 new file mode 100644 index 0000000000000..6bb2188903440 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/state-store.json @@ -0,0 +1,42 @@ +{ + "component": { + "kind": "component", + "name": "state-store", + "title": "State Store", + "description": "Perform key-value operations against a pluggable state store backend.", + "deprecated": false, + "firstVersion": "4.19.0", + "label": "cache", + "javaType": "org.apache.camel.component.statestore.StateStoreComponent", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-state-store", + "version": "4.19.0-SNAPSHOT", + "scheme": "state-store", + "extendsScheme": "", + "syntax": "state-store:storeName", + "async": false, + "api": false, + "consumerOnly": false, + "producerOnly": true, + "lenientProperties": false, + "browsable": false, + "remote": false + }, + "componentProperties": { + "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "autowiredEnabled": { "index": 1, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." } + }, + "headers": { + "CamelStateStoreOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#OPERATION" }, + "CamelStateStoreKey": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The key to use for the operation", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#KEY" }, + "CamelStateStoreTtl": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Per-message TTL override in milliseconds. Takes precedence over the endpoint ttl option.", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#TTL" } + }, + "properties": { + "storeName": { "index": 0, "kind": "path", "displayName": "Store Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the state store" }, + "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "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, "defaultValue": "memory", "description": "The backend to use. Default is an in-memory store. Set to a bean reference (e.g. #myBackend) for custom backends." } + } +} diff --git a/components/camel-state-store/camel-state-store-caffeine/pom.xml b/components/camel-state-store/camel-state-store-caffeine/pom.xml new file mode 100644 index 0000000000000..30f9cbc9883c0 --- /dev/null +++ b/components/camel-state-store/camel-state-store-caffeine/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + + org.apache.camel + camel-state-store-parent + 4.19.0-SNAPSHOT + + + camel-state-store-caffeine + jar + + Camel :: State Store :: Caffeine + Camel State Store backend using Caffeine cache + + + 4.19.0 + + + + + + org.apache.camel + camel-state-store + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine-version} + + + + + org.apache.camel + camel-test-junit5 + test + + + + 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 new file mode 100644 index 0000000000000..aa9f225f7339a --- /dev/null +++ b/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -0,0 +1,7 @@ +# 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.19.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 new file mode 100644 index 0000000000000..51788f695f299 --- /dev/null +++ b/components/camel-state-store/camel-state-store-caffeine/src/generated/resources/state-store-caffeine.json @@ -0,0 +1,14 @@ +{ + "other": { + "kind": "other", + "name": "state-store-caffeine", + "title": "State Store Caffeine", + "description": "Camel State Store backend using Caffeine cache", + "deprecated": false, + "firstVersion": "4.19.0", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-state-store-caffeine", + "version": "4.19.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 new file mode 100644 index 0000000000000..f46abd48481fc --- /dev/null +++ b/components/camel-state-store/camel-state-store-caffeine/src/main/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackend.java @@ -0,0 +1,133 @@ +/* + * 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. + */ +public class CaffeineStateStoreBackend implements StateStoreBackend { + + private Cache cache; + private int maximumSize = 10_000; + + @Override + public Object put(String key, Object value, long ttlMillis) { + TimedValue previous = cache.getIfPresent(key); + cache.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.getIfPresent(key); + cache.invalidate(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 existing = cache.getIfPresent(key); + if (existing != null) { + return existing.value(); + } + cache.put(key, new TimedValue(value, ttlMillis)); + return null; + } + + @Override + public int size() { + cache.cleanUp(); + return (int) cache.estimatedSize(); + } + + @Override + public Set keys() { + return Set.copyOf(cache.asMap().keySet()); + } + + @Override + public void clear() { + cache.invalidateAll(); + } + + @Override + public void start() { + cache = Caffeine.newBuilder() + .maximumSize(maximumSize) + .expireAfter(new Expiry() { + @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 new file mode 100644 index 0000000000000..8e370d5de6357 --- /dev/null +++ b/components/camel-state-store/camel-state-store-caffeine/src/test/java/org/apache/camel/component/statestore/caffeine/CaffeineStateStoreBackendTest.java @@ -0,0 +1,197 @@ +/* + * 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 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +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")); + assertEquals("hello", result); + } + + @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")); + assertEquals("first", previous); + } + + @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")); + assertEquals("value", removed); + + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "key1")); + assertNull(result); + } + + @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")); + assertEquals(true, exists); + } + + @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 keys = (Set) template.requestBodyAndHeaders( + "direct:dynamic", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys)); + assertEquals(Set.of("a", "b"), keys); + + Object size = template.requestBodyAndHeaders( + "direct:dynamic", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size)); + assertEquals(2, size); + } + + @Test + void testPutIfAbsent() { + Object result = template.requestBodyAndHeaders( + "direct:dynamic", "first", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertNull(result); + + result = template.requestBodyAndHeaders( + "direct:dynamic", "second", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertEquals("first", result); + } + + @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")); + assertNull(result); + } + + @Test + void testTtlExpiry() throws Exception { + template.requestBodyAndHeaders( + "direct:put-ttl", "expiring", + Map.of(StateStoreConstants.KEY, "ttlKey")); + + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertEquals("expiring", result); + + Thread.sleep(400); + backend.keys(); // trigger Caffeine cleanup + + result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(result); + } + + @Test + void testPerMessageTtlHeader() throws Exception { + 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")); + assertEquals("expiring", result); + + Thread.sleep(400); + backend.keys(); + + result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(result); + } + + @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-infinispan/pom.xml b/components/camel-state-store/camel-state-store-infinispan/pom.xml new file mode 100644 index 0000000000000..c6e86892e78ad --- /dev/null +++ b/components/camel-state-store/camel-state-store-infinispan/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + org.apache.camel + camel-state-store-parent + 4.19.0-SNAPSHOT + + + camel-state-store-infinispan + jar + + Camel :: State Store :: Infinispan + Camel State Store backend using Infinispan remote (Hot Rod) + + + 4.19.0 + + + + + + org.apache.camel + camel-state-store + + + + org.infinispan + infinispan-client-hotrod + ${infinispan-version} + + + + + org.apache.camel + camel-test-junit5 + test + + + org.apache.camel + camel-test-infra-infinispan + ${project.version} + test + + + + 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 new file mode 100644 index 0000000000000..e5bca35a3389e --- /dev/null +++ b/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -0,0 +1,7 @@ +# 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.19.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 new file mode 100644 index 0000000000000..054dbadd945de --- /dev/null +++ b/components/camel-state-store/camel-state-store-infinispan/src/generated/resources/state-store-infinispan.json @@ -0,0 +1,14 @@ +{ + "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.19.0", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-state-store-infinispan", + "version": "4.19.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 new file mode 100644 index 0000000000000..1a7f6b9b4b26a --- /dev/null +++ b/components/camel-state-store/camel-state-store-infinispan/src/main/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackend.java @@ -0,0 +1,144 @@ +/* + * 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. + */ +public class InfinispanStateStoreBackend implements StateStoreBackend { + + private RemoteCacheManager cacheManager; + private RemoteCache 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 keys() { + return Set.copyOf(cache.keySet()); + } + + @Override + public int size() { + return cache.size(); + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public void start() { + if (cacheManager == null) { + ConfigurationBuilder builder = new ConfigurationBuilder(); + builder.addServers(hosts); + builder.forceReturnValues(true); + cacheManager = new RemoteCacheManager(builder.build(), true); + managedCacheManager = true; + } + // Retry cache creation to handle server startup race conditions + Exception lastException = null; + for (int i = 0; i < 10; i++) { + try { + cache = cacheManager.administration().getOrCreateCache(cacheName, (String) null); + return; + } catch (Exception e) { + lastException = e; + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for cache to be ready", ie); + } + } + } + throw new RuntimeException("Failed to create or access cache '" + cacheName + "' after retries", lastException); + } + + @Override + public void stop() { + if (managedCacheManager && cacheManager != null) { + cacheManager.stop(); + cacheManager = null; + } + } + + 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 new file mode 100644 index 0000000000000..748b7d6f39bcf --- /dev/null +++ b/components/camel-state-store/camel-state-store-infinispan/src/test/java/org/apache/camel/component/statestore/infinispan/InfinispanStateStoreBackendIT.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.statestore.infinispan; + +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +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.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +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; + + @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); + } + RemoteCacheManager cacheManager = new RemoteCacheManager(builder.build(), true); + backend.setCacheManager(cacheManager); + 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")); + assertEquals("hello", result); + } + + @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")); + assertEquals("first", previous); + } + + @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")); + assertEquals("value", removed); + } + + @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")); + assertEquals(true, exists); + } + + @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 keys = (Set) template.requestBodyAndHeaders( + "direct:op", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys)); + assertEquals(Set.of("a", "b"), keys); + + Object size = template.requestBodyAndHeaders( + "direct:op", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size)); + assertEquals(2, size); + } + + @Test + void testPutIfAbsent() { + Object result = template.requestBodyAndHeaders( + "direct:op", "first", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertNull(result); + + result = template.requestBodyAndHeaders( + "direct:op", "second", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertEquals("first", result); + } + + @Test + void testTtlExpiry() throws Exception { + 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")); + assertEquals("expiring", result); + + Thread.sleep(700); + + result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(result); + } + + @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-redis/pom.xml b/components/camel-state-store/camel-state-store-redis/pom.xml new file mode 100644 index 0000000000000..514326164d490 --- /dev/null +++ b/components/camel-state-store/camel-state-store-redis/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + org.apache.camel + camel-state-store-parent + 4.19.0-SNAPSHOT + + + camel-state-store-redis + jar + + Camel :: State Store :: Redis + Camel State Store backend using Redisson (Redis) + + + 4.19.0 + + + + + + org.apache.camel + camel-state-store + + + + org.redisson + redisson + ${redisson-version} + + + + + org.apache.camel + camel-test-junit5 + test + + + org.apache.camel + camel-test-infra-redis + ${project.version} + test + + + + 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 new file mode 100644 index 0000000000000..050f646fb20c7 --- /dev/null +++ b/components/camel-state-store/camel-state-store-redis/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -0,0 +1,7 @@ +# 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.19.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 new file mode 100644 index 0000000000000..b1d5f1c02abb4 --- /dev/null +++ b/components/camel-state-store/camel-state-store-redis/src/generated/resources/state-store-redis.json @@ -0,0 +1,14 @@ +{ + "other": { + "kind": "other", + "name": "state-store-redis", + "title": "State Store Redis", + "description": "Camel State Store backend using Redisson (Redis)", + "deprecated": false, + "firstVersion": "4.19.0", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-state-store-redis", + "version": "4.19.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 new file mode 100644 index 0000000000000..3d66a45554ef3 --- /dev/null +++ b/components/camel-state-store/camel-state-store-redis/src/main/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackend.java @@ -0,0 +1,128 @@ +/* + * 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. + */ +public class RedisStateStoreBackend implements StateStoreBackend { + + private RedissonClient redisson; + private RMapCache 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 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() { + if (managedRedisson && redisson != null) { + redisson.shutdown(); + redisson = null; + } + } + + 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 new file mode 100644 index 0000000000000..46533e9597344 --- /dev/null +++ b/components/camel-state-store/camel-state-store-redis/src/test/java/org/apache/camel/component/statestore/redis/RedisStateStoreBackendIT.java @@ -0,0 +1,189 @@ +/* + * 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 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +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")); + assertEquals("hello", result); + } + + @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")); + assertEquals("first", previous); + } + + @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")); + assertEquals("value", removed); + } + + @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")); + assertEquals(true, exists); + + Object notExists = template.requestBodyAndHeaders( + "direct:op", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.contains, + StateStoreConstants.KEY, "missing")); + assertEquals(false, notExists); + } + + @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 keys = (Set) template.requestBodyAndHeaders( + "direct:op", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.keys)); + assertEquals(Set.of("a", "b"), keys); + + Object size = template.requestBodyAndHeaders( + "direct:op", null, + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.size)); + assertEquals(2, size); + } + + @Test + void testPutIfAbsent() { + Object result = template.requestBodyAndHeaders( + "direct:op", "first", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertNull(result); + + result = template.requestBodyAndHeaders( + "direct:op", "second", + Map.of(StateStoreConstants.OPERATION, StateStoreOperations.putIfAbsent, + StateStoreConstants.KEY, "key1")); + assertEquals("first", result); + } + + @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")); + assertNull(result); + } + + @Test + void testTtlExpiry() throws Exception { + 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")); + assertEquals("expiring", result); + + Thread.sleep(700); + + result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(result); + } + + @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/pom.xml b/components/camel-state-store/camel-state-store/pom.xml new file mode 100644 index 0000000000000..494dbb5be44f6 --- /dev/null +++ b/components/camel-state-store/camel-state-store/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + + org.apache.camel + camel-state-store-parent + 4.19.0-SNAPSHOT + + + camel-state-store + jar + + Camel :: State Store + Camel State Store component for pluggable key-value store operations + + + + + org.apache.camel + camel-support + + + + + org.apache.camel + camel-test-junit5 + test + + + org.apache.camel + camel-mock + test + + + + diff --git a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java new file mode 100644 index 0000000000000..fa7fc58157002 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreComponentConfigurer.java @@ -0,0 +1,57 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.statestore; + +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.component.PropertyConfigurerSupport; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.EndpointSchemaGeneratorMojo") +@SuppressWarnings("unchecked") +public class StateStoreComponentConfigurer extends PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter { + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + StateStoreComponent target = (StateStoreComponent) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": target.setAutowiredEnabled(property(camelContext, boolean.class, value)); return true; + case "lazystartproducer": + case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + default: return false; + } + } + + @Override + public Class getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": return boolean.class; + case "lazystartproducer": + case "lazyStartProducer": return boolean.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + StateStoreComponent target = (StateStoreComponent) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "autowiredenabled": + case "autowiredEnabled": return target.isAutowiredEnabled(); + case "lazystartproducer": + case "lazyStartProducer": return target.isLazyStartProducer(); + default: return null; + } + } +} + 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 new file mode 100644 index 0000000000000..a9c3c59a868f2 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointConfigurer.java @@ -0,0 +1,60 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.statestore; + +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.component.PropertyConfigurerSupport; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.EndpointSchemaGeneratorMojo") +@SuppressWarnings("unchecked") +public class StateStoreEndpointConfigurer extends PropertyConfigurerSupport implements GeneratedPropertyConfigurer, PropertyConfigurerGetter { + + @Override + 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 "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; + case "ttl": target.setTtl(property(camelContext, long.class, value)); return true; + default: return false; + } + } + + @Override + public Class getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "backend": return org.apache.camel.component.statestore.StateStoreBackend.class; + case "lazystartproducer": + case "lazyStartProducer": return boolean.class; + case "operation": return org.apache.camel.component.statestore.StateStoreOperations.class; + case "ttl": return long.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + StateStoreEndpoint target = (StateStoreEndpoint) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "backend": return target.getBackend(); + case "lazystartproducer": + case "lazyStartProducer": return target.isLazyStartProducer(); + case "operation": return target.getOperation(); + case "ttl": return target.getTtl(); + default: return null; + } + } +} + diff --git a/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java new file mode 100644 index 0000000000000..1966a6702b44f --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/java/org/apache/camel/component/statestore/StateStoreEndpointUriFactory.java @@ -0,0 +1,74 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.component.statestore; + +import javax.annotation.processing.Generated; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.camel.spi.EndpointUriFactory; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateEndpointUriFactoryMojo") +public class StateStoreEndpointUriFactory extends org.apache.camel.support.component.EndpointUriFactorySupport implements EndpointUriFactory { + + private static final String BASE = ":storeName"; + + private static final Set PROPERTY_NAMES; + private static final Set SECRET_PROPERTY_NAMES; + private static final Map MULTI_VALUE_PREFIXES; + static { + Set props = new HashSet<>(5); + props.add("backend"); + props.add("lazyStartProducer"); + props.add("operation"); + props.add("storeName"); + props.add("ttl"); + PROPERTY_NAMES = Collections.unmodifiableSet(props); + SECRET_PROPERTY_NAMES = Collections.emptySet(); + MULTI_VALUE_PREFIXES = Collections.emptyMap(); + } + + @Override + public boolean isEnabled(String scheme) { + return "state-store".equals(scheme); + } + + @Override + public String buildUri(String scheme, Map properties, boolean encode) throws URISyntaxException { + String syntax = scheme + BASE; + String uri = syntax; + + Map copy = new HashMap<>(properties); + + uri = buildPathParameter(syntax, uri, "storeName", null, true, copy); + uri = buildQueryParameters(uri, copy, encode); + return uri; + } + + @Override + public Set propertyNames() { + return PROPERTY_NAMES; + } + + @Override + public Set secretPropertyNames() { + return SECRET_PROPERTY_NAMES; + } + + @Override + public Map multiValuePrefixes() { + return MULTI_VALUE_PREFIXES; + } + + @Override + public boolean isLenientProperties() { + return false; + } +} + 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 new file mode 100644 index 0000000000000..6bb2188903440 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json @@ -0,0 +1,42 @@ +{ + "component": { + "kind": "component", + "name": "state-store", + "title": "State Store", + "description": "Perform key-value operations against a pluggable state store backend.", + "deprecated": false, + "firstVersion": "4.19.0", + "label": "cache", + "javaType": "org.apache.camel.component.statestore.StateStoreComponent", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-state-store", + "version": "4.19.0-SNAPSHOT", + "scheme": "state-store", + "extendsScheme": "", + "syntax": "state-store:storeName", + "async": false, + "api": false, + "consumerOnly": false, + "producerOnly": true, + "lenientProperties": false, + "browsable": false, + "remote": false + }, + "componentProperties": { + "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "autowiredEnabled": { "index": 1, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." } + }, + "headers": { + "CamelStateStoreOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.statestore.StateStoreOperations", "enum": [ "put", "putIfAbsent", "get", "delete", "contains", "keys", "size", "clear" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#OPERATION" }, + "CamelStateStoreKey": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The key to use for the operation", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#KEY" }, + "CamelStateStoreTtl": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Per-message TTL override in milliseconds. Takes precedence over the endpoint ttl option.", "constantName": "org.apache.camel.component.statestore.StateStoreConstants#TTL" } + }, + "properties": { + "storeName": { "index": 0, "kind": "path", "displayName": "Store Name", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the state store" }, + "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "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, "defaultValue": "memory", "description": "The backend to use. Default is an in-memory store. Set to a bean reference (e.g. #myBackend) for custom backends." } + } +} diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component.properties b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component.properties new file mode 100644 index 0000000000000..890436454f515 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +components=state-store +groupId=org.apache.camel +artifactId=camel-state-store +version=4.19.0-SNAPSHOT +projectName=Camel :: State Store +projectDescription=Camel State Store component for pluggable key-value store operations diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component/state-store b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component/state-store new file mode 100644 index 0000000000000..e70c435ac7a3a --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/component/state-store @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.statestore.StateStoreComponent diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-component b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-component new file mode 100644 index 0000000000000..72500e5989c86 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-component @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.statestore.StateStoreComponentConfigurer diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-endpoint b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-endpoint new file mode 100644 index 0000000000000..c7634332bfe43 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/configurer/state-store-endpoint @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.statestore.StateStoreEndpointConfigurer diff --git a/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/urifactory/state-store-endpoint b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/urifactory/state-store-endpoint new file mode 100644 index 0000000000000..0a9d439bef31e --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/generated/resources/META-INF/services/org/apache/camel/urifactory/state-store-endpoint @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.statestore.StateStoreEndpointUriFactory 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 new file mode 100644 index 0000000000000..27c9a5c8d4ba4 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc @@ -0,0 +1,331 @@ += State Store Component +:doctitle: State Store +:shortname: state-store +:artifactid: camel-state-store +:description: Perform key-value operations against a pluggable state store backend. +:since: 4.19 +:supportlevel: Preview +:tabs-sync-option: +:component-header: Only producer is supported + +*Since Camel {since}* + +*{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). + +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: + +== When to use State Store vs individual components + +Camel also provides dedicated components for xref:infinispan-component.adoc[Infinispan], +xref:spring-redis-component.adoc[Redis], and xref:caffeine-cache-component.adoc[Caffeine]. +Use the dedicated component when you need the full feature set of a specific technology +(e.g., Infinispan queries, Redis pub/sub, Caffeine statistics). + +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, get, delete, contains, keys* 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. +|=== + +== URI Format + +---- +state-store:storeName[?options] +---- + +Where `storeName` is a logical name for the store. Endpoints with the same `storeName` +share the same backend instance within the same `CamelContext`. + +// component-configure options: START +// component-configure options: END + +// component options: START +include::partial$component-configure-options.adoc[] +include::partial$component-endpoint-options.adoc[] +include::partial$component-endpoint-headers.adoc[] +// component options: END + +// endpoint options: START +// endpoint options: END + +== Operations + +The operation to perform is set via the URI option `operation` or the header `CamelStateStoreOperation`. + +[width="100%",cols="1,2,2,2",options="header"] +|=== +| Operation | Description | Body (input) | Result (body output) +| `put` | Store a value | The value to store | The previous value (or null) +| `putIfAbsent` | Store only if key absent | The value to store | The existing value (or null if stored) +| `get` | Retrieve a value | ignored | The stored value (or null) +| `delete` | Remove a value | ignored | The removed value (or null) +| `contains` | Check if key exists | ignored | Boolean +| `keys` | List all keys | ignored | Set +| `size` | Count entries | ignored | Integer +| `clear` | Remove all entries | ignored | null +|=== + +The key is specified via the header `CamelStateStoreKey` (required for `put`, `putIfAbsent`, `get`, `delete`, `contains`). + +== TTL (Time-to-Live) + +You can set a TTL (in milliseconds) for entries in two ways: + +* *Endpoint option*: applies to all `put`/`putIfAbsent` operations on that endpoint. ++ +---- +state-store:myStore?operation=put&ttl=60000 +---- + +* *Per-message header*: the header `CamelStateStoreTtl` (Long) overrides the endpoint TTL for that message. ++ +[source,java] +---- +from("direct:store") + .setHeader("CamelStateStoreKey", constant("myKey")) + .setHeader("CamelStateStoreTtl", constant(30000L)) + .to("state-store:myStore?operation=put"); +---- + +A TTL of `0` (default) means no expiry. + +== Examples + +=== Java DSL + +[tabs] +==== +Put and Get:: ++ +[source,java] +---- +from("direct:store") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=put"); + +from("direct:retrieve") + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore?operation=get"); +---- + +Dynamic operation via header:: ++ +[source,java] +---- +from("direct:dynamic") + .setHeader("CamelStateStoreOperation", constant("put")) + .setHeader("CamelStateStoreKey", constant("myKey")) + .to("state-store:myStore"); +---- + +Put if absent:: ++ +[source,java] +---- +from("direct:init") + .setHeader("CamelStateStoreKey", constant("counter")) + .to("state-store:myStore?operation=putIfAbsent"); +---- +==== + +=== YAML DSL + +[source,yaml] +---- +- route: + from: + uri: direct:store + steps: + - setHeader: + name: CamelStateStoreKey + constant: myKey + - to: + uri: state-store:myStore?operation=put +---- + +== Backends + +=== 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. + +No additional dependency is needed beyond `camel-state-store`. + +=== Caffeine + +Add the dependency: + +[source,xml] +---- + + org.apache.camel + camel-state-store-caffeine + +---- + +Register the backend as a bean and reference it: + +[source,java] +---- +@BindToRegistry("caffeineBackend") +public CaffeineStateStoreBackend caffeine() { + CaffeineStateStoreBackend backend = new CaffeineStateStoreBackend(); + backend.setMaximumSize(50_000); // default is 10,000 + return backend; +} +---- + +[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] +---- + + org.apache.camel + camel-state-store-redis + +---- + +Configure and register: + +[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; +} +---- + +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] +---- + + org.apache.camel + camel-state-store-infinispan + +---- + +Configure and register: + +[source,java] +---- +@BindToRegistry("infinispanBackend") +public InfinispanStateStoreBackend infinispan() { + InfinispanStateStoreBackend backend = new InfinispanStateStoreBackend(); + backend.setHosts("myhost:11222"); + backend.setCacheName("my-cache"); + return backend; +} +---- + +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: + +[source,java] +---- +public class MyCustomBackend implements StateStoreBackend { + + @Override + public Object put(String key, Object value, long ttlMillis) { + // store the value, return previous + } + + @Override + public Object get(String key) { + // retrieve the value + } + + @Override + public Object delete(String key) { + // remove and return the value + } + + @Override + public boolean contains(String key) { + // check existence + } + + @Override + public Set keys() { + // return all keys + } + + @Override + public void clear() { + // remove all entries + } + + @Override + public void start() { + // initialize connections + } + + @Override + public void stop() { + // clean up resources + } +} +---- + +The `putIfAbsent` and `size` methods have default implementations but can be overridden +for better performance with backends that support these operations natively. + +Then reference it via bean: + +[source,java] +---- +from("direct:store") + .to("state-store:myStore?operation=put&backend=#myCustomBackend"); +---- 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 new file mode 100644 index 0000000000000..da0c94fb7a5da --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/InMemoryStateStoreBackend.java @@ -0,0 +1,127 @@ +/* + * 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; +import java.util.stream.Collectors; + +/** + * 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). + */ +public class InMemoryStateStoreBackend implements StateStoreBackend { + + private final ConcurrentHashMap 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; + Entry existing = store.putIfAbsent(key, new Entry(value, expiresAt)); + if (existing != null) { + if (existing.isExpired()) { + // expired, replace it + store.replace(key, existing, new Entry(value, expiresAt)); + return null; + } + return existing.value(); + } + return null; + } + + @Override + public int size() { + return (int) store.entrySet().stream() + .filter(e -> !e.getValue().isExpired()) + .count(); + } + + @Override + public Set keys() { + return store.entrySet().stream() + .filter(e -> !e.getValue().isExpired()) + .map(java.util.Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + @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 new file mode 100644 index 0000000000000..f1b4229ab3533 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreBackend.java @@ -0,0 +1,106 @@ +/* + * 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; + +/** + * Interface for pluggable state store backends. + */ +public interface StateStoreBackend { + + /** + * 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. + * + * @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 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(); + + /** + * Start the backend (lifecycle). + */ + void start(); + + /** + * Stop the backend (lifecycle). + */ + void stop(); +} 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 new file mode 100644 index 0000000000000..362995c3bebc6 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreComponent.java @@ -0,0 +1,56 @@ +/* + * 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.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.camel.Endpoint; +import org.apache.camel.support.DefaultComponent; + +/** + * The State Store component provides a simple, unified key-value store API with pluggable backends. + */ +@org.apache.camel.spi.annotations.Component("state-store") +public class StateStoreComponent extends DefaultComponent { + + private final ConcurrentHashMap backends = new ConcurrentHashMap<>(); + + @Override + protected Endpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception { + StateStoreEndpoint endpoint = new StateStoreEndpoint(uri, this); + endpoint.setStoreName(remaining); + setProperties(endpoint, parameters); + return endpoint; + } + + StateStoreBackend getOrCreateBackend(String storeName, StateStoreBackend explicitBackend) { + if (explicitBackend != null) { + return backends.computeIfAbsent(storeName, k -> explicitBackend); + } + return backends.computeIfAbsent(storeName, k -> new InMemoryStateStoreBackend()); + } + + @Override + protected void doStop() throws Exception { + for (StateStoreBackend backend : backends.values()) { + backend.stop(); + } + backends.clear(); + super.doStop(); + } +} diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreConstants.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreConstants.java new file mode 100644 index 0000000000000..77906a95cec80 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreConstants.java @@ -0,0 +1,39 @@ +/* + * 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 org.apache.camel.spi.Metadata; + +/** + * Constants for the State Store component. + */ +public final class StateStoreConstants { + + @Metadata(description = "The operation to perform", javaType = "org.apache.camel.component.statestore.StateStoreOperations", + label = "producer") + public static final String OPERATION = "CamelStateStoreOperation"; + + @Metadata(description = "The key to use for the operation", javaType = "String", label = "producer") + public static final String KEY = "CamelStateStoreKey"; + + @Metadata(description = "Per-message TTL override in milliseconds. Takes precedence over the endpoint ttl option.", + javaType = "Long", label = "producer") + public static final String TTL = "CamelStateStoreTtl"; + + private StateStoreConstants() { + } +} 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 new file mode 100644 index 0000000000000..700c3819087ec --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreEndpoint.java @@ -0,0 +1,105 @@ +/* + * 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 org.apache.camel.Category; +import org.apache.camel.Consumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.UriEndpoint; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriPath; +import org.apache.camel.support.DefaultEndpoint; + +/** + * Perform key-value operations against a pluggable state store backend. + */ +@UriEndpoint(firstVersion = "4.19.0", scheme = "state-store", title = "State Store", + syntax = "state-store:storeName", producerOnly = true, + remote = false, category = { Category.CACHE }, + headersClass = StateStoreConstants.class) +public class StateStoreEndpoint extends DefaultEndpoint { + + @UriPath(description = "The name of the state store") + @Metadata(required = true) + private String storeName; + + @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. Default is an in-memory store. Set to a bean reference (e.g. #myBackend) for custom backends.", + defaultValue = "memory", label = "advanced") + private StateStoreBackend backend; + + @UriParam(description = "Time-to-live in milliseconds for entries. 0 means no expiry.", defaultValue = "0") + private long ttl; + + public StateStoreEndpoint(String endpointUri, StateStoreComponent component) { + super(endpointUri, component); + } + + @Override + public Producer createProducer() { + return new StateStoreProducer(this); + } + + @Override + public Consumer createConsumer(Processor processor) { + throw new UnsupportedOperationException("The state-store component does not support consumers"); + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + StateStoreComponent comp = (StateStoreComponent) getComponent(); + backend = comp.getOrCreateBackend(storeName, backend); + backend.start(); + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public StateStoreOperations getOperation() { + return operation; + } + + public void setOperation(StateStoreOperations operation) { + this.operation = operation; + } + + public StateStoreBackend getBackend() { + return backend; + } + + public void setBackend(StateStoreBackend backend) { + this.backend = backend; + } + + public long getTtl() { + return ttl; + } + + public void setTtl(long ttl) { + this.ttl = ttl; + } +} diff --git a/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreOperations.java b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreOperations.java new file mode 100644 index 0000000000000..0e275cc3ea003 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreOperations.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Operations supported by the State Store component. + */ +public enum StateStoreOperations { + + put, + putIfAbsent, + get, + delete, + contains, + keys, + size, + clear; + +} 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 new file mode 100644 index 0000000000000..da0741b354954 --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/main/java/org/apache/camel/component/statestore/StateStoreProducer.java @@ -0,0 +1,115 @@ +/* + * 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 org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.support.DefaultProducer; + +/** + * Producer for the State Store component that performs key-value operations. + */ +public class StateStoreProducer extends DefaultProducer { + + private final StateStoreEndpoint endpoint; + + public StateStoreProducer(StateStoreEndpoint endpoint) { + super(endpoint); + this.endpoint = endpoint; + } + + @Override + public void process(Exchange exchange) throws Exception { + StateStoreOperations op = determineOperation(exchange); + if (op == null) { + throw new IllegalArgumentException( + "No operation specified. Set the operation via URI option or header " + StateStoreConstants.OPERATION); + } + + StateStoreBackend backend = endpoint.getBackend(); + long ttl = determineTtl(exchange); + Message message = exchange.getMessage(); + + switch (op) { + case put -> { + String key = requireKey(message); + Object value = message.getBody(); + Object previous = backend.put(key, value, ttl); + message.setBody(previous); + } + case putIfAbsent -> { + String key = requireKey(message); + Object value = message.getBody(); + Object existing = backend.putIfAbsent(key, value, ttl); + message.setBody(existing); + } + case get -> { + String key = requireKey(message); + Object value = backend.get(key); + message.setBody(value); + } + case delete -> { + String key = requireKey(message); + Object removed = backend.delete(key); + message.setBody(removed); + } + case contains -> { + String key = requireKey(message); + boolean exists = backend.contains(key); + message.setBody(exists); + } + case keys -> { + message.setBody(backend.keys()); + } + case size -> { + message.setBody(backend.size()); + } + case clear -> { + backend.clear(); + message.setBody(null); + } + default -> throw new IllegalArgumentException("Unsupported operation: " + op); + } + } + + private StateStoreOperations determineOperation(Exchange exchange) { + Object headerOp = exchange.getMessage().getHeader(StateStoreConstants.OPERATION); + if (headerOp != null) { + if (headerOp instanceof StateStoreOperations sso) { + return sso; + } + return StateStoreOperations.valueOf(headerOp.toString()); + } + return endpoint.getOperation(); + } + + private long determineTtl(Exchange exchange) { + Long headerTtl = exchange.getMessage().getHeader(StateStoreConstants.TTL, Long.class); + if (headerTtl != null) { + return headerTtl; + } + return endpoint.getTtl(); + } + + private String requireKey(Message message) { + String key = message.getHeader(StateStoreConstants.KEY, String.class); + if (key == null) { + throw new IllegalArgumentException("Header " + StateStoreConstants.KEY + " is required for this operation"); + } + return key; + } +} diff --git a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTest.java b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTest.java new file mode 100644 index 0000000000000..ad93df093ee7f --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTest.java @@ -0,0 +1,217 @@ +/* + * 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.Map; +import java.util.Set; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class StateStoreTest extends CamelTestSupport { + + @Test + void testPutAndGet() { + Object previous = template.requestBodyAndHeaders( + "direct:put", "hello", + Map.of(StateStoreConstants.KEY, "key1")); + assertNull(previous); + + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "key1")); + assertEquals("hello", result); + } + + @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")); + assertEquals("first", previous); + } + + @Test + void testGetNonExistent() { + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "missing")); + assertNull(result); + } + + @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")); + assertEquals("value", removed); + + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "key1")); + assertNull(result); + } + + @Test + void testContains() { + template.requestBodyAndHeaders( + "direct:put", "value", + Map.of(StateStoreConstants.KEY, "key1")); + + Object exists = template.requestBodyAndHeaders( + "direct:contains", null, + Map.of(StateStoreConstants.KEY, "key1")); + assertEquals(true, exists); + + Object notExists = template.requestBodyAndHeaders( + "direct:contains", null, + Map.of(StateStoreConstants.KEY, "missing")); + assertEquals(false, notExists); + } + + @Test + @SuppressWarnings("unchecked") + void testKeys() { + template.requestBodyAndHeaders( + "direct:put", "v1", + Map.of(StateStoreConstants.KEY, "a")); + template.requestBodyAndHeaders( + "direct:put", "v2", + Map.of(StateStoreConstants.KEY, "b")); + + Set keys = (Set) template.requestBody("direct:keys", (Object) null); + assertEquals(Set.of("a", "b"), keys); + } + + @Test + void testSize() { + template.requestBodyAndHeaders( + "direct:put", "v1", + Map.of(StateStoreConstants.KEY, "a")); + template.requestBodyAndHeaders( + "direct:put", "v2", + Map.of(StateStoreConstants.KEY, "b")); + + Object size = template.requestBody("direct:size", (Object) null); + assertEquals(2, size); + } + + @Test + void testClear() { + template.requestBodyAndHeaders( + "direct:put", "v1", + Map.of(StateStoreConstants.KEY, "a")); + + template.requestBody("direct:clear", (Object) null); + + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "a")); + assertNull(result); + } + + @Test + void testPutIfAbsent() { + // first put should succeed + Object result = template.requestBodyAndHeaders( + "direct:putIfAbsent", "first", + Map.of(StateStoreConstants.KEY, "key1")); + assertNull(result); + + // second put should return existing value + result = template.requestBodyAndHeaders( + "direct:putIfAbsent", "second", + Map.of(StateStoreConstants.KEY, "key1")); + assertEquals("first", result); + + // verify original value is still there + Object value = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "key1")); + assertEquals("first", value); + } + + @Test + void testOperationViaHeader() { + template.requestBodyAndHeaders( + "direct:dynamic", "hello", + Map.of( + StateStoreConstants.OPERATION, StateStoreOperations.put, + StateStoreConstants.KEY, "key1")); + + Object result = template.requestBodyAndHeaders( + "direct:dynamic", null, + Map.of( + StateStoreConstants.OPERATION, "get", + StateStoreConstants.KEY, "key1")); + assertEquals("hello", result); + } + + @Test + void testPerMessageTtlHeader() throws Exception { + // put with per-message TTL of 200ms (no TTL on endpoint) + template.requestBodyAndHeaders( + "direct:put", "expiring", + Map.of( + StateStoreConstants.KEY, "ttlKey", + StateStoreConstants.TTL, 200L)); + + // should be there immediately + Object result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertEquals("expiring", result); + + Thread.sleep(300); + + // should be expired + result = template.requestBodyAndHeaders( + "direct:get", null, + Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(result); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:put").to("state-store:myStore?operation=put"); + from("direct:putIfAbsent").to("state-store:myStore?operation=putIfAbsent"); + from("direct:get").to("state-store:myStore?operation=get"); + from("direct:delete").to("state-store:myStore?operation=delete"); + from("direct:contains").to("state-store:myStore?operation=contains"); + from("direct:keys").to("state-store:myStore?operation=keys"); + from("direct:size").to("state-store:myStore?operation=size"); + from("direct:clear").to("state-store:myStore?operation=clear"); + from("direct:dynamic").to("state-store:myStore"); + } + }; + } +} diff --git a/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTtlTest.java b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTtlTest.java new file mode 100644 index 0000000000000..d60df8a5ca03d --- /dev/null +++ b/components/camel-state-store/camel-state-store/src/test/java/org/apache/camel/component/statestore/StateStoreTtlTest.java @@ -0,0 +1,76 @@ +/* + * 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 org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class StateStoreTtlTest extends CamelTestSupport { + + @Test + void testEntryExpiresAfterTtl() throws Exception { + // put a value with 200ms TTL + template.requestBodyAndHeaders( + "direct:put", "expiring", + java.util.Map.of(StateStoreConstants.KEY, "ttlKey")); + + // should be retrievable immediately + Object result = template.requestBodyAndHeaders( + "direct:get", null, + java.util.Map.of(StateStoreConstants.KEY, "ttlKey")); + assertEquals("expiring", result); + + // wait for TTL to expire + Thread.sleep(300); + + // should be expired now + Object expired = template.requestBodyAndHeaders( + "direct:get", null, + java.util.Map.of(StateStoreConstants.KEY, "ttlKey")); + assertNull(expired); + } + + @Test + void testContainsReturnsFalseAfterTtl() throws Exception { + template.requestBodyAndHeaders( + "direct:put", "expiring", + java.util.Map.of(StateStoreConstants.KEY, "ttlKey")); + + Thread.sleep(300); + + Object exists = template.requestBodyAndHeaders( + "direct:contains", null, + java.util.Map.of(StateStoreConstants.KEY, "ttlKey")); + assertEquals(false, exists); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:put").to("state-store:ttlStore?operation=put&ttl=200"); + from("direct:get").to("state-store:ttlStore?operation=get"); + from("direct:contains").to("state-store:ttlStore?operation=contains"); + } + }; + } +} diff --git a/components/camel-state-store/pom.xml b/components/camel-state-store/pom.xml new file mode 100644 index 0000000000000..787b2d1851ccd --- /dev/null +++ b/components/camel-state-store/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + + org.apache.camel + components + 4.19.0-SNAPSHOT + + + camel-state-store-parent + pom + + Camel :: State Store :: Parent + Camel State Store parent + + + camel-state-store + camel-state-store-caffeine + camel-state-store-redis + camel-state-store-infinispan + + + diff --git a/components/pom.xml b/components/pom.xml index a5303ece65e61..efaab85409cb0 100644 --- a/components/pom.xml +++ b/components/pom.xml @@ -307,6 +307,7 @@ camel-splunk-hec camel-sql camel-ssh + camel-state-store camel-stax camel-stomp camel-stream diff --git a/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties b/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties index 42086ed03ffa9..976216400d54b 100644 --- a/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties +++ b/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties @@ -351,6 +351,7 @@ spring-ws sql sql-stored ssh +state-store stax stitch stomp diff --git a/docs/components/modules/ROOT/examples/json/state-store.json b/docs/components/modules/ROOT/examples/json/state-store.json new file mode 120000 index 0000000000000..41b1898528137 --- /dev/null +++ b/docs/components/modules/ROOT/examples/json/state-store.json @@ -0,0 +1 @@ +../../../../../../components/camel-state-store/camel-state-store/src/generated/resources/META-INF/org/apache/camel/component/statestore/state-store.json \ No newline at end of file diff --git a/docs/components/modules/ROOT/nav.adoc b/docs/components/modules/ROOT/nav.adoc index baad9ec54566c..3a6a97db515c4 100644 --- a/docs/components/modules/ROOT/nav.adoc +++ b/docs/components/modules/ROOT/nav.adoc @@ -361,6 +361,7 @@ ** xref:sql-component.adoc[SQL] ** xref:sql-stored-component.adoc[SQL Stored Procedure] ** xref:ssh-component.adoc[SSH] +** xref:state-store-component.adoc[State Store] ** xref:stax-component.adoc[StAX] ** xref:stitch-component.adoc[Stitch] ** xref:stomp-component.adoc[Stomp] diff --git a/docs/components/modules/ROOT/pages/state-store-component.adoc b/docs/components/modules/ROOT/pages/state-store-component.adoc new file mode 120000 index 0000000000000..2ccd74acd0503 --- /dev/null +++ b/docs/components/modules/ROOT/pages/state-store-component.adoc @@ -0,0 +1 @@ +../../../../../components/camel-state-store/camel-state-store/src/main/docs/state-store-component.adoc \ No newline at end of file diff --git a/docs/components/modules/others/examples/json/state-store-caffeine.json b/docs/components/modules/others/examples/json/state-store-caffeine.json new file mode 120000 index 0000000000000..15277f9fe6688 --- /dev/null +++ b/docs/components/modules/others/examples/json/state-store-caffeine.json @@ -0,0 +1 @@ +../../../../../../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 new file mode 120000 index 0000000000000..9e22e40328f2d --- /dev/null +++ b/docs/components/modules/others/examples/json/state-store-infinispan.json @@ -0,0 +1 @@ +../../../../../../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 new file mode 120000 index 0000000000000..aac04d819206e --- /dev/null +++ b/docs/components/modules/others/examples/json/state-store-redis.json @@ -0,0 +1 @@ +../../../../../../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 92fe76e755006..b84b74476f210 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 @@ -4736,6 +4736,19 @@ static SqlStoredComponentBuilderFactory.SqlStoredComponentBuilder sqlStored() { static SshComponentBuilderFactory.SshComponentBuilder ssh() { return SshComponentBuilderFactory.ssh(); } + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * @return the dsl builder + */ + static StateStoreComponentBuilderFactory.StateStoreComponentBuilder stateStore() { + return StateStoreComponentBuilderFactory.stateStore(); + } /** * StAX (camel-stax) * Process XML payloads by a SAX ContentHandler. 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 new file mode 100644 index 0000000000000..c03dc31862381 --- /dev/null +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/StateStoreComponentBuilderFactory.java @@ -0,0 +1,120 @@ +/* Generated by camel build tools - do NOT edit this file! */ +/* + * 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.builder.component.dsl; + +import javax.annotation.processing.Generated; +import org.apache.camel.Component; +import org.apache.camel.builder.component.AbstractComponentBuilder; +import org.apache.camel.builder.component.ComponentBuilder; +import org.apache.camel.component.statestore.StateStoreComponent; + +/** + * Perform key-value operations against a pluggable state store backend. + * + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.ComponentDslMojo") +public interface StateStoreComponentBuilderFactory { + + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * @return the dsl builder + */ + static StateStoreComponentBuilder stateStore() { + return new StateStoreComponentBuilderImpl(); + } + + /** + * Builder for the State Store component. + */ + interface StateStoreComponentBuilder extends ComponentBuilder { + + + /** + * 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 producer may otherwise fail during + * starting and cause the route to fail being started. By deferring this + * startup to be lazy then the startup failure can be handled during + * routing messages via Camel's routing error handlers. Beware that when + * the first message is processed then creating and starting the + * producer may take a little time and prolong the total processing time + * of the processing. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: producer + * + * @param lazyStartProducer the value to set + * @return the dsl builder + */ + default StateStoreComponentBuilder lazyStartProducer(boolean lazyStartProducer) { + doSetProperty("lazyStartProducer", lazyStartProducer); + return this; + } + + + /** + * Whether autowiring is enabled. This is used for automatic autowiring + * options (the option must be marked as autowired) by looking up in the + * registry to find if there is a single instance of matching type, + * which then gets configured on the component. This can be used for + * automatic configuring JDBC data sources, JMS connection factories, + * AWS Clients, etc. + * + * The option is a: <code>boolean</code> type. + * + * Default: true + * Group: advanced + * + * @param autowiredEnabled the value to set + * @return the dsl builder + */ + default StateStoreComponentBuilder autowiredEnabled(boolean autowiredEnabled) { + doSetProperty("autowiredEnabled", autowiredEnabled); + return this; + } + } + + class StateStoreComponentBuilderImpl + extends AbstractComponentBuilder + implements StateStoreComponentBuilder { + @Override + protected StateStoreComponent buildConcreteComponent() { + return new StateStoreComponent(); + } + @Override + protected boolean setPropertyOnComponent( + Component component, + String name, + Object value) { + switch (name) { + case "lazyStartProducer": ((StateStoreComponent) component).setLazyStartProducer((boolean) value); return true; + case "autowiredEnabled": ((StateStoreComponent) component).setAutowiredEnabled((boolean) value); return true; + default: return false; + } + } + } +} \ No newline at end of file diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilderFactory.java index 12c6a649732bc..d7e53decc9fbb 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilderFactory.java @@ -359,6 +359,7 @@ public interface EndpointBuilderFactory org.apache.camel.builder.endpoint.dsl.Sqs2EndpointBuilderFactory.Sqs2Builders, org.apache.camel.builder.endpoint.dsl.SshEndpointBuilderFactory.SshBuilders, org.apache.camel.builder.endpoint.dsl.StAXEndpointBuilderFactory.StAXBuilders, + org.apache.camel.builder.endpoint.dsl.StateStoreEndpointBuilderFactory.StateStoreBuilders, org.apache.camel.builder.endpoint.dsl.StepFunctions2EndpointBuilderFactory.StepFunctions2Builders, org.apache.camel.builder.endpoint.dsl.StitchEndpointBuilderFactory.StitchBuilders, org.apache.camel.builder.endpoint.dsl.StompEndpointBuilderFactory.StompBuilders, diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilders.java index 49b40e06b59bb..21718cb9b05df 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilders.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointBuilders.java @@ -356,6 +356,7 @@ public interface EndpointBuilders org.apache.camel.builder.endpoint.dsl.Sqs2EndpointBuilderFactory, org.apache.camel.builder.endpoint.dsl.SshEndpointBuilderFactory, org.apache.camel.builder.endpoint.dsl.StAXEndpointBuilderFactory, + org.apache.camel.builder.endpoint.dsl.StateStoreEndpointBuilderFactory, org.apache.camel.builder.endpoint.dsl.StepFunctions2EndpointBuilderFactory, org.apache.camel.builder.endpoint.dsl.StitchEndpointBuilderFactory, org.apache.camel.builder.endpoint.dsl.StompEndpointBuilderFactory, 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 4a0f88874ac6d..c33f8e0e8beb6 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 @@ -15845,6 +15845,46 @@ public static SshEndpointBuilderFactory.SshEndpointBuilder ssh(String path) { public static SshEndpointBuilderFactory.SshEndpointBuilder ssh(String componentName, String path) { return SshEndpointBuilderFactory.endpointBuilder(componentName, path); } + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * Syntax: state-store:storeName + * + * Path parameter: storeName (required) + * The name of the state store + * + * @param path storeName + * @return the dsl builder + */ + public static StateStoreEndpointBuilderFactory.StateStoreEndpointBuilder stateStore(String path) { + return stateStore("state-store", path); + } + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * Syntax: state-store:storeName + * + * Path parameter: storeName (required) + * The name of the state store + * + * @param componentName to use a custom component name for the endpoint + * instead of the default name + * @param path storeName + * @return the dsl builder + */ + public static StateStoreEndpointBuilderFactory.StateStoreEndpointBuilder stateStore(String componentName, String path) { + return StateStoreEndpointBuilderFactory.endpointBuilder(componentName, path); + } /** * StAX (camel-stax) * Process XML payloads by a SAX ContentHandler. 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 new file mode 100644 index 0000000000000..34ebc31c5d52a --- /dev/null +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StateStoreEndpointBuilderFactory.java @@ -0,0 +1,316 @@ +/* Generated by camel build tools - do NOT edit this file! */ +/* + * 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.builder.endpoint.dsl; + +import java.util.*; +import java.util.concurrent.*; +import java.util.function.*; +import java.util.stream.*; +import javax.annotation.processing.Generated; +import org.apache.camel.builder.EndpointConsumerBuilder; +import org.apache.camel.builder.EndpointProducerBuilder; +import org.apache.camel.builder.endpoint.AbstractEndpointBuilder; + +/** + * Perform key-value operations against a pluggable state store backend. + * + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.EndpointDslMojo") +public interface StateStoreEndpointBuilderFactory { + + /** + * Builder for endpoint for the State Store component. + */ + public interface StateStoreEndpointBuilder + extends + EndpointProducerBuilder { + default AdvancedStateStoreEndpointBuilder advanced() { + return (AdvancedStateStoreEndpointBuilder) this; + } + + /** + * The default operation to perform. + * + * The option is a: + * org.apache.camel.component.statestore.StateStoreOperations type. + * + * Group: producer + * + * @param operation the value to set + * @return the dsl builder + */ + default StateStoreEndpointBuilder operation(org.apache.camel.component.statestore.StateStoreOperations operation) { + doSetProperty("operation", operation); + return this; + } + /** + * The default operation to perform. + * + * The option will be converted to a + * org.apache.camel.component.statestore.StateStoreOperations type. + * + * Group: producer + * + * @param operation the value to set + * @return the dsl builder + */ + default StateStoreEndpointBuilder operation(String operation) { + doSetProperty("operation", operation); + return this; + } + /** + * Time-to-live in milliseconds for entries. 0 means no expiry. + * + * The option is a: long type. + * + * Default: 0 + * Group: producer + * + * @param ttl the value to set + * @return the dsl builder + */ + default StateStoreEndpointBuilder ttl(long ttl) { + doSetProperty("ttl", ttl); + return this; + } + /** + * Time-to-live in milliseconds for entries. 0 means no expiry. + * + * The option will be converted to a long type. + * + * Default: 0 + * Group: producer + * + * @param ttl the value to set + * @return the dsl builder + */ + default StateStoreEndpointBuilder ttl(String ttl) { + doSetProperty("ttl", ttl); + return this; + } + } + + /** + * Advanced builder for endpoint for the State Store component. + */ + public interface AdvancedStateStoreEndpointBuilder + extends + EndpointProducerBuilder { + default StateStoreEndpointBuilder basic() { + return (StateStoreEndpointBuilder) this; + } + + /** + * 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 producer may otherwise fail during + * starting and cause the route to fail being started. By deferring this + * startup to be lazy then the startup failure can be handled during + * routing messages via Camel's routing error handlers. Beware that when + * the first message is processed then creating and starting the + * producer may take a little time and prolong the total processing time + * of the processing. + * + * The option is a: boolean type. + * + * Default: false + * Group: producer (advanced) + * + * @param lazyStartProducer the value to set + * @return the dsl builder + */ + default AdvancedStateStoreEndpointBuilder lazyStartProducer(boolean lazyStartProducer) { + doSetProperty("lazyStartProducer", lazyStartProducer); + return this; + } + /** + * 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 producer may otherwise fail during + * starting and cause the route to fail being started. By deferring this + * startup to be lazy then the startup failure can be handled during + * routing messages via Camel's routing error handlers. Beware that when + * the first message is processed then creating and starting the + * producer may take a little time and prolong the total processing time + * of the processing. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: producer (advanced) + * + * @param lazyStartProducer the value to set + * @return the dsl builder + */ + default AdvancedStateStoreEndpointBuilder lazyStartProducer(String lazyStartProducer) { + doSetProperty("lazyStartProducer", lazyStartProducer); + return this; + } + /** + * The backend to use. Default is an in-memory store. Set to a bean + * reference (e.g. #myBackend) for custom backends. + * + * The option is a: + * org.apache.camel.component.statestore.StateStoreBackend + * type. + * + * Default: memory + * Group: advanced + * + * @param backend the value to set + * @return the dsl builder + */ + default AdvancedStateStoreEndpointBuilder backend(org.apache.camel.component.statestore.StateStoreBackend backend) { + doSetProperty("backend", backend); + return this; + } + /** + * The backend to use. Default is an in-memory store. Set to a bean + * reference (e.g. #myBackend) for custom backends. + * + * The option will be converted to a + * org.apache.camel.component.statestore.StateStoreBackend + * type. + * + * Default: memory + * Group: advanced + * + * @param backend the value to set + * @return the dsl builder + */ + default AdvancedStateStoreEndpointBuilder backend(String backend) { + doSetProperty("backend", backend); + return this; + } + } + + public interface StateStoreBuilders { + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * @return the dsl builder for the headers' name. + */ + default StateStoreHeaderNameBuilder stateStore() { + return StateStoreHeaderNameBuilder.INSTANCE; + } + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * Syntax: state-store:storeName + * + * Path parameter: storeName (required) + * The name of the state store + * + * @param path storeName + * @return the dsl builder + */ + default StateStoreEndpointBuilder stateStore(String path) { + return StateStoreEndpointBuilderFactory.endpointBuilder("state-store", path); + } + /** + * State Store (camel-state-store) + * Perform key-value operations against a pluggable state store backend. + * + * Category: cache + * Since: 4.19 + * Maven coordinates: org.apache.camel:camel-state-store + * + * Syntax: state-store:storeName + * + * Path parameter: storeName (required) + * The name of the state store + * + * @param componentName to use a custom component name for the endpoint + * instead of the default name + * @param path storeName + * @return the dsl builder + */ + default StateStoreEndpointBuilder stateStore(String componentName, String path) { + return StateStoreEndpointBuilderFactory.endpointBuilder(componentName, path); + } + + } + /** + * The builder of headers' name for the State Store component. + */ + public static class StateStoreHeaderNameBuilder { + /** + * The internal instance of the builder used to access to all the + * methods representing the name of headers. + */ + private static final StateStoreHeaderNameBuilder INSTANCE = new StateStoreHeaderNameBuilder(); + + /** + * The operation to perform. + * + * The option is a: {@code + * org.apache.camel.component.statestore.StateStoreOperations} type. + * + * Group: producer + * + * @return the name of the header {@code StateStoreOperation}. + */ + public String stateStoreOperation() { + return "CamelStateStoreOperation"; + } + /** + * The key to use for the operation. + * + * The option is a: {@code String} type. + * + * Group: producer + * + * @return the name of the header {@code StateStoreKey}. + */ + public String stateStoreKey() { + return "CamelStateStoreKey"; + } + /** + * Per-message TTL override in milliseconds. Takes precedence over the + * endpoint ttl option. + * + * The option is a: {@code Long} type. + * + * Group: producer + * + * @return the name of the header {@code StateStoreTtl}. + */ + public String stateStoreTtl() { + return "CamelStateStoreTtl"; + } + } + static StateStoreEndpointBuilder endpointBuilder(String componentName, String path) { + class StateStoreEndpointBuilderImpl extends AbstractEndpointBuilder implements StateStoreEndpointBuilder, AdvancedStateStoreEndpointBuilder { + public StateStoreEndpointBuilderImpl(String path) { + super(componentName, path); + } + } + return new StateStoreEndpointBuilderImpl(path); + } +} \ No newline at end of file diff --git a/dsl/camel-kamelet-main/src/generated/resources/camel-component-known-dependencies.properties b/dsl/camel-kamelet-main/src/generated/resources/camel-component-known-dependencies.properties index e452e6d1ec4fb..608dadfc3b2c7 100644 --- a/dsl/camel-kamelet-main/src/generated/resources/camel-component-known-dependencies.properties +++ b/dsl/camel-kamelet-main/src/generated/resources/camel-component-known-dependencies.properties @@ -355,6 +355,7 @@ org.apache.camel.component.springrabbit.SpringRabbitMQComponent=camel:spring-rab org.apache.camel.component.sql.SqlComponent=camel:sql org.apache.camel.component.sql.stored.SqlStoredComponent=camel:sql org.apache.camel.component.ssh.SshComponent=camel:ssh +org.apache.camel.component.statestore.StateStoreComponent=camel:state-store org.apache.camel.component.stax.StAXComponent=camel:stax org.apache.camel.component.stitch.StitchComponent=camel:stitch org.apache.camel.component.stomp.StompComponent=camel:stomp diff --git a/parent/pom.xml b/parent/pom.xml index 625fc58218fed..8d28a44a7ef4f 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -2724,6 +2724,26 @@ camel-ssh ${project.version} + + org.apache.camel + camel-state-store + ${project.version} + + + org.apache.camel + camel-state-store-caffeine + ${project.version} + + + org.apache.camel + camel-state-store-infinispan + ${project.version} + + + org.apache.camel + camel-state-store-redis + ${project.version} + org.apache.camel camel-stax diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java index c4ebaa99c5c3d..30b0012d41f84 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java @@ -71,6 +71,8 @@ public static List getComponentPath(Path dir) { return Collections.singletonList(dir.resolve("camel-fhir-component")); case "camel-infinispan": return Arrays.asList(dir.resolve("camel-infinispan"), dir.resolve("camel-infinispan-embedded")); + case "camel-state-store": + return Collections.singletonList(dir.resolve("camel-state-store")); case "camel-azure": return Arrays.asList(dir.resolve("camel-azure-eventhubs"), dir.resolve("camel-azure-storage-blob"), dir.resolve("camel-azure-storage-datalake"), dir.resolve("camel-azure-cosmosdb"),