This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26189-jgroups-raft-body in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8a04dd3d027ddc40f4196b8faacda9c90a8a96cf Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 3 00:01:26 2026 +0100 CAMEL-26189: camel-jgroups-raft - a body that does not convert to bytes fails the exchange, and raftId, channelProperties, raftHandle and stateMachine are endpoint options The producer read the body with a non-mandatory conversion to byte[], so a Map, a number or a POJO became null and the producer skipped it with a DEBUG log: nothing was set in the replicated log and the exchange succeeded. It now uses a mandatory conversion for a non-null body, so such a body (and a List) fails with an InvalidPayloadException that names the type of the body. A null body is still skipped. raftId, channelProperties, raftHandle and stateMachine worked as endpoint query parameters but had no @UriParam, so the catalog, the doc and the endpoint DSL did not list them. The doc now says the body is converted to a byte[]. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/components/jgroups-raft.json | 18 +- .../camel/catalog/docs/jgroups-raft-component.adoc | 6 +- .../raft/JGroupsRaftEndpointConfigurer.java | 24 ++ .../raft/JGroupsRaftEndpointUriFactory.java | 6 +- .../camel/component/jgroups/raft/jgroups-raft.json | 18 +- .../src/main/docs/jgroups-raft-component.adoc | 6 +- .../jgroups/raft/JGroupsRaftEndpoint.java | 34 ++- .../jgroups/raft/JGroupsRaftProducer.java | 3 +- .../jgroups/raft/JGroupsRaftEndpointTest.java | 15 ++ .../jgroups/raft/JGroupsRaftProducerBodyTest.java | 91 +++++++ .../jgroups/raft/JGroupsRaftProducerTest.java | 8 + .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 + .../dsl/JGroupsRaftEndpointBuilderFactory.java | 279 +++++++++++++++++++++ 13 files changed, 488 insertions(+), 27 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups-raft.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups-raft.json index 9ae0d9b25edf..a23bdd0b8ec1 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups-raft.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups-raft.json @@ -48,15 +48,19 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "null" ], "description": "No body: an event for each change of the role of this member (only with enableRoleChangeEvents), with LEADER or FOLLOWER in the CamelJGroupsRaftEventType header and the state of the raft in the other headers" }, - "producerIn": { "carriers": [ "bytes", "text", "stream", "file" ], "description": "The bytes to set() in the replicated log: a byte array, or text, a stream or a file converted to bytes. A null body, a Map, a number or a POJO has no conversion to bytes and is skipped without an error: nothing is set" }, - "producerOut": { "carriers": [ "bytes", "unchanged" ], "description": "The byte array the state machine returns for the set (an empty array with the default NopStateMachine), with the state of the raft in the headers. A body that was skipped is left as it was" } + "producerIn": { "carriers": [ "bytes", "text", "stream", "file" ], "description": "The bytes to set() in the replicated log: a byte array, or text, a stream or a file converted to bytes. A null body is skipped: nothing is set. A Map, a List, a number or a POJO has no conversion to bytes and fails with an InvalidPayloadException" }, + "producerOut": { "carriers": [ "bytes", "unchanged" ], "description": "The byte array the state machine returns for the set (an empty array with the default NopStateMachine), with the state of the raft in the headers. A null body is left as it was" } }, "properties": { "clusterName": { "index": 0, "kind": "path", "displayName": "Cluster Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the JGroupsraft cluster the component should connect to." }, - "enableRoleChangeEvents": { "index": 1, "kind": "parameter", "displayName": "Enable Role Change Events", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If set to true, the consumer endpoint will receive roleChange event as well (not just connecting and\/or using the state machine). By default it is set to false." }, - "bridgeErrorHandler": { "index": 2, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] - "exceptionHandler": { "index": 3, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] - "exchangePattern": { "index": 4, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, - "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] + "channelProperties": { "index": 1, "kind": "parameter", "displayName": "Channel Properties", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "raft.xml", "description": "Specifies configuration properties of the RaftHandle JChannel used by the endpoint (ignored if raftHandle ref is provided). Overrides the option of the component." }, + "raftHandle": { "index": 2, "kind": "parameter", "displayName": "Raft Handle", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.jgroups.raft.RaftHandle", "deprecated": false, "autowired": false, "secret": false, "description": "RaftHandle to use. Overrides the option of the component." }, + "raftId": { "index": 3, "kind": "parameter", "displayName": "Raft Id", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Unique raftId to use. Overrides the option of the component." }, + "stateMachine": { "index": 4, "kind": "parameter", "displayName": "State Machine", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.jgroups.raft.StateMachine", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "NopStateMachine", "description": "StateMachine to use (ignored if raftHandle ref is provided). Overrides the option of the component." }, + "enableRoleChangeEvents": { "index": 5, "kind": "parameter", "displayName": "Enable Role Change Events", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If set to true, the consumer endpoint will receive roleChange event as well (not just connecting and\/or using the state machine). By default it is set to false." }, + "bridgeErrorHandler": { "index": 6, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] + "exceptionHandler": { "index": 7, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] + "exchangePattern": { "index": 8, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, + "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-raft-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-raft-component.adoc index 3a9893ca4ce7..7039ae12a0aa 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-raft-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-raft-component.adoc @@ -86,8 +86,10 @@ YAML:: ---- ==== -Using `jgroups-raft` component on the producer side of the route will use the body of the camel exchange (which must be a `byte[]`) -to perform a setX() operation on the raftHandle associated with the endpoint. +Using `jgroups-raft` component on the producer side of the route will use the body of the camel exchange +to perform a setX() operation on the raftHandle associated with the endpoint. The body is converted to a `byte[]` +(such as from text, a stream or a file). A body that cannot be converted, such as a `Map`, a `List`, a number or a POJO, +fails the exchange with an `InvalidPayloadException`. A null body is skipped: nothing is set. [tabs] ==== diff --git a/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointConfigurer.java b/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointConfigurer.java index 25cca40aebd9..a2b7b122816c 100644 --- a/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointConfigurer.java +++ b/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointConfigurer.java @@ -26,6 +26,8 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) { case "bridgeerrorhandler": case "bridgeErrorHandler": target.setBridgeErrorHandler(property(camelContext, boolean.class, value)); return true; + case "channelproperties": + case "channelProperties": target.setChannelProperties(property(camelContext, java.lang.String.class, value)); return true; case "enablerolechangeevents": case "enableRoleChangeEvents": target.setEnableRoleChangeEvents(property(camelContext, boolean.class, value)); return true; case "exceptionhandler": @@ -34,6 +36,12 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp case "exchangePattern": target.setExchangePattern(property(camelContext, org.apache.camel.ExchangePattern.class, value)); return true; case "lazystartproducer": case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + case "rafthandle": + case "raftHandle": target.setRaftHandle(property(camelContext, org.jgroups.raft.RaftHandle.class, value)); return true; + case "raftid": + case "raftId": target.setRaftId(property(camelContext, java.lang.String.class, value)); return true; + case "statemachine": + case "stateMachine": target.setStateMachine(property(camelContext, org.jgroups.raft.StateMachine.class, value)); return true; default: return false; } } @@ -43,6 +51,8 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) { case "bridgeerrorhandler": case "bridgeErrorHandler": return boolean.class; + case "channelproperties": + case "channelProperties": return java.lang.String.class; case "enablerolechangeevents": case "enableRoleChangeEvents": return boolean.class; case "exceptionhandler": @@ -51,6 +61,12 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp case "exchangePattern": return org.apache.camel.ExchangePattern.class; case "lazystartproducer": case "lazyStartProducer": return boolean.class; + case "rafthandle": + case "raftHandle": return org.jgroups.raft.RaftHandle.class; + case "raftid": + case "raftId": return java.lang.String.class; + case "statemachine": + case "stateMachine": return org.jgroups.raft.StateMachine.class; default: return null; } } @@ -61,6 +77,8 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp switch (ignoreCase ? name.toLowerCase(Locale.ROOT) : name) { case "bridgeerrorhandler": case "bridgeErrorHandler": return target.isBridgeErrorHandler(); + case "channelproperties": + case "channelProperties": return target.getChannelProperties(); case "enablerolechangeevents": case "enableRoleChangeEvents": return target.isEnableRoleChangeEvents(); case "exceptionhandler": @@ -69,6 +87,12 @@ public class JGroupsRaftEndpointConfigurer extends PropertyConfigurerSupport imp case "exchangePattern": return target.getExchangePattern(); case "lazystartproducer": case "lazyStartProducer": return target.isLazyStartProducer(); + case "rafthandle": + case "raftHandle": return target.getRaftHandle(); + case "raftid": + case "raftId": return target.getRaftId(); + case "statemachine": + case "stateMachine": return target.getStateMachine(); default: return null; } } diff --git a/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointUriFactory.java b/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointUriFactory.java index 92dfdead6bc1..cdc260ceb4f5 100644 --- a/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointUriFactory.java +++ b/components/camel-jgroups-raft/src/generated/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointUriFactory.java @@ -24,13 +24,17 @@ public class JGroupsRaftEndpointUriFactory extends org.apache.camel.support.comp private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(6); + Set<String> props = new HashSet<>(10); props.add("bridgeErrorHandler"); + props.add("channelProperties"); props.add("clusterName"); props.add("enableRoleChangeEvents"); props.add("exceptionHandler"); props.add("exchangePattern"); props.add("lazyStartProducer"); + props.add("raftHandle"); + props.add("raftId"); + props.add("stateMachine"); PROPERTY_NAMES = Collections.unmodifiableSet(props); SECRET_PROPERTY_NAMES = Collections.emptySet(); ENDPOINT_IDENTITY_PROPERTY_NAMES = Collections.emptySet(); diff --git a/components/camel-jgroups-raft/src/generated/resources/META-INF/org/apache/camel/component/jgroups/raft/jgroups-raft.json b/components/camel-jgroups-raft/src/generated/resources/META-INF/org/apache/camel/component/jgroups/raft/jgroups-raft.json index 9ae0d9b25edf..a23bdd0b8ec1 100644 --- a/components/camel-jgroups-raft/src/generated/resources/META-INF/org/apache/camel/component/jgroups/raft/jgroups-raft.json +++ b/components/camel-jgroups-raft/src/generated/resources/META-INF/org/apache/camel/component/jgroups/raft/jgroups-raft.json @@ -48,15 +48,19 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "null" ], "description": "No body: an event for each change of the role of this member (only with enableRoleChangeEvents), with LEADER or FOLLOWER in the CamelJGroupsRaftEventType header and the state of the raft in the other headers" }, - "producerIn": { "carriers": [ "bytes", "text", "stream", "file" ], "description": "The bytes to set() in the replicated log: a byte array, or text, a stream or a file converted to bytes. A null body, a Map, a number or a POJO has no conversion to bytes and is skipped without an error: nothing is set" }, - "producerOut": { "carriers": [ "bytes", "unchanged" ], "description": "The byte array the state machine returns for the set (an empty array with the default NopStateMachine), with the state of the raft in the headers. A body that was skipped is left as it was" } + "producerIn": { "carriers": [ "bytes", "text", "stream", "file" ], "description": "The bytes to set() in the replicated log: a byte array, or text, a stream or a file converted to bytes. A null body is skipped: nothing is set. A Map, a List, a number or a POJO has no conversion to bytes and fails with an InvalidPayloadException" }, + "producerOut": { "carriers": [ "bytes", "unchanged" ], "description": "The byte array the state machine returns for the set (an empty array with the default NopStateMachine), with the state of the raft in the headers. A null body is left as it was" } }, "properties": { "clusterName": { "index": 0, "kind": "path", "displayName": "Cluster Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the JGroupsraft cluster the component should connect to." }, - "enableRoleChangeEvents": { "index": 1, "kind": "parameter", "displayName": "Enable Role Change Events", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If set to true, the consumer endpoint will receive roleChange event as well (not just connecting and\/or using the state machine). By default it is set to false." }, - "bridgeErrorHandler": { "index": 2, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] - "exceptionHandler": { "index": 3, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] - "exchangePattern": { "index": 4, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, - "lazyStartProducer": { "index": 5, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] + "channelProperties": { "index": 1, "kind": "parameter", "displayName": "Channel Properties", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "raft.xml", "description": "Specifies configuration properties of the RaftHandle JChannel used by the endpoint (ignored if raftHandle ref is provided). Overrides the option of the component." }, + "raftHandle": { "index": 2, "kind": "parameter", "displayName": "Raft Handle", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.jgroups.raft.RaftHandle", "deprecated": false, "autowired": false, "secret": false, "description": "RaftHandle to use. Overrides the option of the component." }, + "raftId": { "index": 3, "kind": "parameter", "displayName": "Raft Id", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Unique raftId to use. Overrides the option of the component." }, + "stateMachine": { "index": 4, "kind": "parameter", "displayName": "State Machine", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.jgroups.raft.StateMachine", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "NopStateMachine", "description": "StateMachine to use (ignored if raftHandle ref is provided). Overrides the option of the component." }, + "enableRoleChangeEvents": { "index": 5, "kind": "parameter", "displayName": "Enable Role Change Events", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If set to true, the consumer endpoint will receive roleChange event as well (not just connecting and\/or using the state machine). By default it is set to false." }, + "bridgeErrorHandler": { "index": 6, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] + "exceptionHandler": { "index": 7, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] + "exchangePattern": { "index": 8, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, + "lazyStartProducer": { "index": 9, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] } } diff --git a/components/camel-jgroups-raft/src/main/docs/jgroups-raft-component.adoc b/components/camel-jgroups-raft/src/main/docs/jgroups-raft-component.adoc index 3a9893ca4ce7..7039ae12a0aa 100644 --- a/components/camel-jgroups-raft/src/main/docs/jgroups-raft-component.adoc +++ b/components/camel-jgroups-raft/src/main/docs/jgroups-raft-component.adoc @@ -86,8 +86,10 @@ YAML:: ---- ==== -Using `jgroups-raft` component on the producer side of the route will use the body of the camel exchange (which must be a `byte[]`) -to perform a setX() operation on the raftHandle associated with the endpoint. +Using `jgroups-raft` component on the producer side of the route will use the body of the camel exchange +to perform a setX() operation on the raftHandle associated with the endpoint. The body is converted to a `byte[]` +(such as from text, a stream or a file). A body that cannot be converted, such as a `Map`, a `List`, a number or a POJO, +fails the exchange with an `InvalidPayloadException`. A null body is skipped: nothing is set. [tabs] ==== diff --git a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java index b08d5e6457fe..cedad3f42b7c 100644 --- a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java +++ b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java @@ -51,12 +51,13 @@ import org.slf4j.LoggerFactory; + " headers"), producerIn = @BodyType(value = { Carrier.BYTES, Carrier.TEXT, Carrier.STREAM, Carrier.FILE }, description = "The bytes to set() in the replicated log: a byte array, or text, a stream or" - + " a file converted to bytes. A null body, a Map, a number or a POJO has no" - + " conversion to bytes and is skipped without an error: nothing is set"), + + " a file converted to bytes. A null body is skipped: nothing is set. A Map," + + " a List, a number or a POJO has no conversion to bytes and fails with an" + + " InvalidPayloadException"), producerOut = @BodyType(value = { Carrier.BYTES, Carrier.UNCHANGED }, description = "The byte array the state machine returns for the set (an empty array with" + " the default NopStateMachine), with the state of the raft in the headers. A" - + " body that was skipped is left as it was")) + + " null body is left as it was")) public class JGroupsRaftEndpoint extends DefaultEndpoint { private static final Logger LOG = LoggerFactory.getLogger(JGroupsRaftEndpoint.class); @@ -64,15 +65,21 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint { private int connectCount; private final Lock connectLock = new ReentrantLock(); - private RaftHandle raftHandle; private RaftHandle resolvedRaftHandle; - private StateMachine stateMachine; - private String raftId; - private String channelProperties; @UriPath @Metadata(required = true) private String clusterName; + @UriParam + private String raftId; + @UriParam + @Metadata(defaultValue = "raft.xml") + private String channelProperties; + @UriParam + private RaftHandle raftHandle; + @UriParam + @Metadata(defaultValue = "NopStateMachine") + private StateMachine stateMachine; @UriParam(label = "consumer", defaultValue = "false") private boolean enableRoleChangeEvents; @@ -274,6 +281,10 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint { return channelProperties; } + /** + * Specifies configuration properties of the RaftHandle JChannel used by the endpoint (ignored if raftHandle ref is + * provided). Overrides the option of the component. + */ public void setChannelProperties(String channelProperties) { this.channelProperties = channelProperties; } @@ -282,6 +293,9 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint { return raftId; } + /** + * Unique raftId to use. Overrides the option of the component. + */ public void setRaftId(String raftId) { this.raftId = raftId; } @@ -290,6 +304,9 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint { return raftHandle; } + /** + * RaftHandle to use. Overrides the option of the component. + */ public void setRaftHandle(RaftHandle raftHandle) { this.raftHandle = raftHandle; } @@ -298,6 +315,9 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint { return stateMachine; } + /** + * StateMachine to use (ignored if raftHandle ref is provided). Overrides the option of the component. + */ public void setStateMachine(StateMachine stateMachine) { this.stateMachine = stateMachine; } diff --git a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java index 5ee0e999d49b..6da6b405af5f 100644 --- a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java +++ b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java @@ -63,7 +63,8 @@ public class JGroupsRaftProducer extends DefaultProducer { @Override public void process(Exchange exchange) throws Exception { //TODO: implement possibility to call CompletableFuture<byte[]> setAsync(byte[] buf, int offset, int length); - byte[] body = exchange.getIn().getBody(byte[].class); + // a body that does not convert to bytes (such as a Map or a POJO) fails, instead of setting nothing + byte[] body = exchange.getIn().getBody() != null ? exchange.getIn().getMandatoryBody(byte[].class) : null; Integer setOffset = exchange.getIn().getHeader(JGroupsRaftConstants.HEADER_JGROUPSRAFT_SET_OFFSET, Integer.class); Integer setLength = exchange.getIn().getHeader(JGroupsRaftConstants.HEADER_JGROUPSRAFT_SET_LENGTH, Integer.class); diff --git a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java index af6901317e12..1c527efca231 100644 --- a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java +++ b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java @@ -18,9 +18,11 @@ package org.apache.camel.component.jgroups.raft; import java.io.DataInput; import java.io.DataOutput; +import java.util.Set; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.jgroups.raft.utils.NopStateMachine; +import org.apache.camel.spi.EndpointUriFactory; import org.apache.camel.spi.Registry; import org.apache.camel.test.junit6.CamelTestSupport; import org.jgroups.JChannel; @@ -30,6 +32,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class JGroupsRaftEndpointTest extends CamelTestSupport { @@ -94,4 +97,16 @@ public class JGroupsRaftEndpointTest extends CamelTestSupport { assertEquals(sm, endpoint2.getStateMachine()); } + + @Test + public void shouldHaveRaftOptionsAsEndpointOptions() { + EndpointUriFactory factory = context.getCamelContextExtension().getEndpointUriFactory("jgroups-raft"); + Set<String> names = factory.propertyNames(); + + assertTrue(names.containsAll(Set.of("raftId", "channelProperties", "raftHandle", "stateMachine")), names.toString()); + + JGroupsRaftEndpoint endpoint2 = getMandatoryEndpoint(CONFIGURED_ENDPOINT_URI2, JGroupsRaftEndpoint.class); + assertEquals("C", endpoint2.getRaftId()); + assertEquals("raftC.xml", endpoint2.getChannelProperties()); + } } diff --git a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerBodyTest.java b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerBodyTest.java new file mode 100644 index 000000000000..d27c18df6fdc --- /dev/null +++ b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerBodyTest.java @@ -0,0 +1,91 @@ +/* + * 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.jgroups.raft; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Exchange; +import org.apache.camel.InvalidPayloadException; +import org.apache.camel.builder.RouteBuilder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A body that does not convert to bytes fails the exchange (no leader is needed, as nothing is set in the log). + */ +public class JGroupsRaftProducerBodyTest extends JGroupsRaftAbstractTest { + private static final String ENDPOINT_URI + = "jgroups-raft:JGroupsRaftProducerBodyTest?raftId=A&channelProperties=raftABC.xml"; + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start").to(ENDPOINT_URI); + } + }; + } + + @Test + public void shouldFailMapBody() { + Exchange res = template.send("direct:start", e -> e.getIn().setBody(Map.of("key", "value"))); + + assertInvalidPayload(res); + } + + @Test + public void shouldFailIntegerBody() { + Exchange res = template.send("direct:start", e -> e.getIn().setBody(123)); + + assertInvalidPayload(res); + } + + @Test + public void shouldFailPojoBody() { + Exchange res = template.send("direct:start", e -> e.getIn().setBody(new Object())); + + assertInvalidPayload(res); + } + + @Test + public void shouldFailListBody() { + Exchange res = template.send("direct:start", e -> e.getIn().setBody(new ArrayList<>(List.of("a", "b")))); + + assertInvalidPayload(res); + assertTrue(res.getException().getMessage().contains("java.util.ArrayList"), res.getException().getMessage()); + } + + @Test + public void shouldSkipNullBody() { + Exchange res = template.send("direct:start", e -> e.getIn().setBody(null)); + + assertNull(res.getException()); + assertNull(res.getIn().getBody()); + } + + private static void assertInvalidPayload(Exchange res) { + assertNotNull(res.getException(), "the exchange should fail"); + assertInstanceOf(InvalidPayloadException.class, res.getException(), res.getException().toString()); + } +} diff --git a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerTest.java b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerTest.java index 183f6111cecd..1cbf0e279e3a 100644 --- a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerTest.java +++ b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducerTest.java @@ -21,7 +21,9 @@ import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class JGroupsRaftProducerTest extends JGroupsRaftAbstractTest { private static final String CLUSTER_NAME = "JGroupsRaftProducerTest"; @@ -66,5 +68,11 @@ public class JGroupsRaftProducerTest extends JGroupsRaftAbstractTest { checkHeaders(res2); assertNotNull(res3.getIn().getBody()); checkHeaders(res3); + + // a text body is converted to bytes + Exchange resText = template.request("direct:start", exchange -> exchange.getIn().setBody(MESSAGE)); + assertNull(resText.getException()); + assertInstanceOf(byte[].class, resText.getIn().getBody()); + checkHeaders(resText); } } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 10f5a47c1b89..1474cfcd82b2 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -804,6 +804,13 @@ CamelContext of that provider. It now closes each of them only when no other `JC it returned the closed cache, whose operations throw `IllegalStateException`. This matters for code that calls a `JCacheManager` (for example one created by a custom `JCacheManagerFactory`) after it was closed. +=== camel-jgroups-raft - a body that does not convert to a byte array fails the exchange + +The `jgroups-raft` producer skipped a message body that has no conversion to `byte[]`, such as a `Map`, a POJO or a +number, without an error, so nothing was set in the replicated log and the exchange succeeded. It now fails the +exchange with an `InvalidPayloadException`. Marshal such a body first (for example to JSON) or convert it to a +`String`. A message without a body is still skipped. + === camel-spring-batch - the CamelSpringBatchJobName header needs jobFromHeader=true The producer now takes the job name from the `CamelSpringBatchJobName` header only when the endpoint has diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JGroupsRaftEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JGroupsRaftEndpointBuilderFactory.java index e67301f88edb..7cbbed33ad7d 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JGroupsRaftEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JGroupsRaftEndpointBuilderFactory.java @@ -43,6 +43,99 @@ public interface JGroupsRaftEndpointBuilderFactory { default AdvancedJGroupsRaftEndpointConsumerBuilder advanced() { return (AdvancedJGroupsRaftEndpointConsumerBuilder) this; } + /** + * Specifies configuration properties of the RaftHandle JChannel used by + * the endpoint (ignored if raftHandle ref is provided). Overrides the + * option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Default: raft.xml + * Group: common + * + * @param channelProperties the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder channelProperties(String channelProperties) { + doSetProperty("channelProperties", channelProperties); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder raftHandle(org.jgroups.raft.RaftHandle raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder raftHandle(String raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * Unique raftId to use. Overrides the option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: common + * + * @param raftId the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder raftId(String raftId) { + doSetProperty("raftId", raftId); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder stateMachine(org.jgroups.raft.StateMachine stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointConsumerBuilder stateMachine(String stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } /** * If set to true, the consumer endpoint will receive roleChange event * as well (not just connecting and/or using the state machine). By @@ -217,6 +310,99 @@ public interface JGroupsRaftEndpointBuilderFactory { return (AdvancedJGroupsRaftEndpointProducerBuilder) this; } + /** + * Specifies configuration properties of the RaftHandle JChannel used by + * the endpoint (ignored if raftHandle ref is provided). Overrides the + * option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Default: raft.xml + * Group: common + * + * @param channelProperties the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder channelProperties(String channelProperties) { + doSetProperty("channelProperties", channelProperties); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder raftHandle(org.jgroups.raft.RaftHandle raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder raftHandle(String raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * Unique raftId to use. Overrides the option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: common + * + * @param raftId the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder raftId(String raftId) { + doSetProperty("raftId", raftId); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder stateMachine(org.jgroups.raft.StateMachine stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointProducerBuilder stateMachine(String stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } } /** @@ -286,6 +472,99 @@ public interface JGroupsRaftEndpointBuilderFactory { return (AdvancedJGroupsRaftEndpointBuilder) this; } + /** + * Specifies configuration properties of the RaftHandle JChannel used by + * the endpoint (ignored if raftHandle ref is provided). Overrides the + * option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Default: raft.xml + * Group: common + * + * @param channelProperties the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder channelProperties(String channelProperties) { + doSetProperty("channelProperties", channelProperties); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder raftHandle(org.jgroups.raft.RaftHandle raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * RaftHandle to use. Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.RaftHandle</code> type. + * + * Group: common + * + * @param raftHandle the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder raftHandle(String raftHandle) { + doSetProperty("raftHandle", raftHandle); + return this; + } + /** + * Unique raftId to use. Overrides the option of the component. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: common + * + * @param raftId the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder raftId(String raftId) { + doSetProperty("raftId", raftId); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option is a: <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder stateMachine(org.jgroups.raft.StateMachine stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } + /** + * StateMachine to use (ignored if raftHandle ref is provided). + * Overrides the option of the component. + * + * The option will be converted to a + * <code>org.jgroups.raft.StateMachine</code> type. + * + * Default: NopStateMachine + * Group: common + * + * @param stateMachine the value to set + * @return the dsl builder + */ + default JGroupsRaftEndpointBuilder stateMachine(String stateMachine) { + doSetProperty("stateMachine", stateMachine); + return this; + } } /**
