This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26180-plc4x-write-map in repository https://gitbox.apache.org/repos/asf/camel.git
commit 155daf4a765504bbd14b5121c888f207a0a63d58 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 23:40:45 2026 +0100 CAMEL-26180: camel-plc4x - fail a write tag that is not a Map of one address, and fail the exchange of the polling consumer for a tag that cannot be read The producer cast the value of each tag to a Map and wrote only the first entry of it, so a tag such as {t1: 42} failed with a ClassCastException, an empty Map with a NoSuchElementException, and a Map of several addresses wrote one of them and ignored the others. Each tag must now be a Map of exactly one address to the value to write, and any other value fails with a PlcInvalidTagException before anything is written. The polling consumer let the PlcRuntimeException of a tag that the PLC could not read (or of an invalid tag address) escape from receive(); it now fails the returned exchange, as for the other read failures. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/components/plc4x.json | 2 +- .../apache/camel/catalog/docs/plc4x-component.adoc | 24 +++++-- .../org/apache/camel/component/plc4x/plc4x.json | 2 +- .../camel-plc4x/src/main/docs/plc4x-component.adoc | 24 +++++-- .../camel/component/plc4x/Plc4XEndpoint.java | 33 +++++++--- .../component/plc4x/Plc4XPollingConsumer.java | 4 +- .../camel/component/plc4x/Plc4XProducer.java | 6 +- .../camel/component/plc4x/Plc4XEndpointTest.java | 75 ++++++++++++++++++++++ .../camel/component/plc4x/Plc4XFailureTest.java | 44 +++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 +++ 10 files changed, 202 insertions(+), 20 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/plc4x.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/plc4x.json index 0f88ab6c212f..a00275e4c95b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/plc4x.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/plc4x.json @@ -30,7 +30,7 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "map" ], "description": "A Map of the tag names and the values read from the PLC, such as an Integer or a String, as the driver reads them; the polling consumer and a trigger give the same Map" }, - "producerIn": { "carriers": [ "map" ], "description": "A Map of the tag names, each to a Map of the address of the tag and the value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body fails" }, + "producerIn": { "carriers": [ "map" ], "description": "A Map of the tag names, each to a Map of one address of the tag and the value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body, or a tag with no or several addresses, fails" }, "producerOut": { "carriers": [ "pojo" ], "javaType": "org.apache.plc4x.java.api.messages.PlcWriteResponse", "description": "The write response of PLC4X, with the response code of each tag" } }, "properties": { diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc index c273c34409d3..e3293c6eb71f 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/plc4x-component.adoc @@ -131,11 +131,27 @@ coil every 100 milliseconds: === Polling Consumer The polling consumer supports consecutive reading. The input and output are the same as for the regular consumer. +A read that fails, for example because the PLC does not have one of the tags, fails the exchange that it returns. === Producer -To write data to the PLC, we also use a `Map`. The difference with the Producer is that the `Value` of the Map has also to -be a `Map`. Also, this `Map` has to be set into the `Body` of the `Message` +To write data to the PLC, the body of the message must be a `Map<String,Map<String,Object>>`. Its keys are the names of +the tags, and the value of each tag is a `Map` of one entry: the address of the tag and the value to write to it. +To write several addresses, use one tag for each. A body that is not a `Map`, or a tag whose value is not a `Map` of +exactly one address, fails the exchange with a `PlcInvalidTagException`, and nothing is written. -The used `Map` would be a `Map<String,Map<String,Object>` where the `Map<String,Object>` represent the Query and the -data we want to write to it. +The following route writes `21` to the address `%DB1:DINT` under the tag name `temperature`: + +[source,yaml] +---- +- route: + from: + uri: direct:write + steps: + - setBody: + expression: + groovy: + expression: "[temperature: ['%DB1:DINT': 21]]" + - to: + uri: plc4x:s7://192.168.0.10 +---- diff --git a/components/camel-plc4x/src/generated/resources/META-INF/org/apache/camel/component/plc4x/plc4x.json b/components/camel-plc4x/src/generated/resources/META-INF/org/apache/camel/component/plc4x/plc4x.json index 0f88ab6c212f..a00275e4c95b 100644 --- a/components/camel-plc4x/src/generated/resources/META-INF/org/apache/camel/component/plc4x/plc4x.json +++ b/components/camel-plc4x/src/generated/resources/META-INF/org/apache/camel/component/plc4x/plc4x.json @@ -30,7 +30,7 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "map" ], "description": "A Map of the tag names and the values read from the PLC, such as an Integer or a String, as the driver reads them; the polling consumer and a trigger give the same Map" }, - "producerIn": { "carriers": [ "map" ], "description": "A Map of the tag names, each to a Map of the address of the tag and the value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body fails" }, + "producerIn": { "carriers": [ "map" ], "description": "A Map of the tag names, each to a Map of one address of the tag and the value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body, or a tag with no or several addresses, fails" }, "producerOut": { "carriers": [ "pojo" ], "javaType": "org.apache.plc4x.java.api.messages.PlcWriteResponse", "description": "The write response of PLC4X, with the response code of each tag" } }, "properties": { diff --git a/components/camel-plc4x/src/main/docs/plc4x-component.adoc b/components/camel-plc4x/src/main/docs/plc4x-component.adoc index c273c34409d3..e3293c6eb71f 100644 --- a/components/camel-plc4x/src/main/docs/plc4x-component.adoc +++ b/components/camel-plc4x/src/main/docs/plc4x-component.adoc @@ -131,11 +131,27 @@ coil every 100 milliseconds: === Polling Consumer The polling consumer supports consecutive reading. The input and output are the same as for the regular consumer. +A read that fails, for example because the PLC does not have one of the tags, fails the exchange that it returns. === Producer -To write data to the PLC, we also use a `Map`. The difference with the Producer is that the `Value` of the Map has also to -be a `Map`. Also, this `Map` has to be set into the `Body` of the `Message` +To write data to the PLC, the body of the message must be a `Map<String,Map<String,Object>>`. Its keys are the names of +the tags, and the value of each tag is a `Map` of one entry: the address of the tag and the value to write to it. +To write several addresses, use one tag for each. A body that is not a `Map`, or a tag whose value is not a `Map` of +exactly one address, fails the exchange with a `PlcInvalidTagException`, and nothing is written. -The used `Map` would be a `Map<String,Map<String,Object>` where the `Map<String,Object>` represent the Query and the -data we want to write to it. +The following route writes `21` to the address `%DB1:DINT` under the tag name `temperature`: + +[source,yaml] +---- +- route: + from: + uri: direct:write + steps: + - setBody: + expression: + groovy: + expression: "[temperature: ['%DB1:DINT': 21]]" + - to: + uri: plc4x:s7://192.168.0.10 +---- diff --git a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java index 2fd507d342a7..3fd5dd428a81 100644 --- a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java +++ b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XEndpoint.java @@ -43,6 +43,7 @@ import org.apache.plc4x.java.DefaultPlcDriverManager; import org.apache.plc4x.java.api.PlcConnection; import org.apache.plc4x.java.api.exceptions.PlcConnectionException; import org.apache.plc4x.java.api.exceptions.PlcIncompatibleDatatypeException; +import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException; import org.apache.plc4x.java.api.messages.PlcReadRequest; import org.apache.plc4x.java.api.messages.PlcReadResponse; import org.apache.plc4x.java.api.messages.PlcWriteRequest; @@ -59,9 +60,9 @@ import org.slf4j.LoggerFactory; + " Integer or a String, as the driver reads them; the polling consumer" + " and a trigger give the same Map"), producerIn = @BodyType(value = Carrier.MAP, - description = "A Map of the tag names, each to a Map of the address of the tag and the" - + " value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body" - + " fails"), + description = "A Map of the tag names, each to a Map of one address of the tag and the" + + " value to write, such as {temperature: {\"%DB1:DINT\": 21}}; another body," + + " or a tag with no or several addresses, fails"), producerOut = @BodyType(value = Carrier.POJO, javaType = "org.apache.plc4x.java.api.messages.PlcWriteResponse", description = "The write response of PLC4X, with the response code of each tag")) public class Plc4XEndpoint extends DefaultEndpoint implements EndpointServiceLocation { @@ -304,9 +305,11 @@ public class Plc4XEndpoint extends DefaultEndpoint implements EndpointServiceLoc * Build a {@link PlcWriteRequest}. * <p> * - * @param tags tags to add to write request + * @param tags the tags to add to the write request, each to a Map of its address and the value + * to write * @return {@link PlcWriteRequest} * @throws PlcConnectionException if there is no connection + * @throws PlcInvalidTagException if the value of a tag is not a Map of one address to the value to write */ public PlcWriteRequest buildPlcWriteRequest(Map<String, Map<String, Object>> tags) throws PlcConnectionException { // read the field once: a concurrent stop or reconnect can clear it at any time @@ -316,16 +319,30 @@ public class Plc4XEndpoint extends DefaultEndpoint implements EndpointServiceLoc } PlcWriteRequest.Builder builder = current.writeRequestBuilder(); - for (Map.Entry<String, Map<String, Object>> entry : tags.entrySet()) { + for (Map.Entry<String, ?> entry : tags.entrySet()) { //Tags are stored like this --> Map<Tagname,Map<Query,Value>> for writing String name = entry.getKey(); - String query = entry.getValue().keySet().iterator().next(); - Object value = entry.getValue().get(query); - builder.addTagAddress(name, query, value); + // the body of a message is not checked for these types, so a value may be anything + if (!(entry.getValue() instanceof Map<?, ?> queryAndValue) || queryAndValue.size() != 1 + || !(queryAndValue.keySet().iterator().next() instanceof String query)) { + // the exception ends the message with " invalid" + throw new PlcInvalidTagException( + "Tag " + name + ": the value must be a Map of one address of the tag to the value to write," + + " such as {\"%DB1:DINT\": 21}, but " + describe(entry.getValue()) + + " is"); + } + builder.addTagAddress(name, query, queryAndValue.get(query)); } return builder.build(); } + private static String describe(Object value) { + if (value instanceof Map<?, ?> map) { + return map.isEmpty() ? "a Map without an address" : "a Map of " + map.size() + " addresses"; + } + return value == null ? "null" : "a " + value.getClass().getName(); + } + public DefaultPlcDriverManager getPlcDriverManager() { return plcDriverManager; } diff --git a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java index b35c2d306e6e..7efca851931d 100644 --- a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java +++ b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XPollingConsumer.java @@ -25,6 +25,7 @@ import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.support.EventDrivenPollingConsumer; import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; import org.apache.plc4x.java.api.messages.PlcReadRequest; import org.apache.plc4x.java.api.messages.PlcReadResponse; import org.slf4j.Logger; @@ -106,7 +107,8 @@ public class Plc4XPollingConsumer extends EventDrivenPollingConsumer { } catch (InterruptedException e) { Thread.currentThread().interrupt(); exchange.setException(e); - } catch (PlcConnectionException e) { + } catch (PlcConnectionException | PlcRuntimeException e) { + // such as a tag that the PLC does not have, which fails when its value is taken from the response exchange.setException(e); } return exchange; diff --git a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java index 13719137f77e..56d908c8d4e1 100644 --- a/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java +++ b/components/camel-plc4x/src/main/java/org/apache/camel/component/plc4x/Plc4XProducer.java @@ -77,7 +77,11 @@ public class Plc4XProducer extends DefaultAsyncProducer { Map<String, Map<String, Object>> tags = (Map<String, Map<String, Object>>) body; plcWriteRequest = plc4XEndpoint.buildPlcWriteRequest(tags); } else { - throw new PlcInvalidTagException("The body must contain a Map<String,Map<String,Object>"); + // the exception ends the message with " invalid" + throw new PlcInvalidTagException( + "The body must be a Map<String, Map<String, Object>> of the tag names, each to a Map of one address of" + + " the tag to the value to write, but " + + (body == null ? "null" : "a " + body.getClass().getName()) + " is"); } CompletableFuture<? extends PlcWriteResponse> completableFuture = plcWriteRequest.execute(); int currentlyOpenRequests = openRequests.incrementAndGet(); diff --git a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java index 28df776b2c83..cca661b8862f 100644 --- a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java +++ b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XEndpointTest.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.plc4x; +import java.util.LinkedHashMap; import java.util.Map; import org.apache.camel.Component; @@ -23,6 +24,8 @@ import org.apache.camel.Processor; import org.apache.camel.impl.DefaultCamelContext; import org.apache.plc4x.java.api.PlcConnection; import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException; +import org.apache.plc4x.java.api.messages.PlcWriteRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,6 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; +import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -83,4 +87,75 @@ class Plc4XEndpointTest { assertThrows(PlcConnectionException.class, () -> sut.buildPlcWriteRequest(Map.of())); } + @Test + void buildPlcWriteRequestAddsTheAddressAndValueOfEachTag() throws Exception { + PlcWriteRequest.Builder builder = connectWithWriteRequestBuilder(); + + sut.buildPlcWriteRequest(Map.of("temperature", Map.of("%DB1:DINT", 21), "pressure", Map.of("%DB2:DINT", 3))); + + verify(builder).addTagAddress("temperature", "%DB1:DINT", 21); + verify(builder).addTagAddress("pressure", "%DB2:DINT", 3); + verify(builder).build(); + } + + @Test + void buildPlcWriteRequestFailsForATagThatIsNotAMap() throws Exception { + connectWithWriteRequestBuilder(); + + PlcInvalidTagException e + = assertThrows(PlcInvalidTagException.class, () -> sut.buildPlcWriteRequest(untyped(Map.of("t1", 42)))); + + assertThat(e.getMessage(), containsString("Tag t1: the value must be a Map of one address")); + assertThat(e.getMessage(), containsString("but a java.lang.Integer is")); + } + + @Test + void buildPlcWriteRequestFailsForATagWithSeveralAddresses() throws Exception { + PlcWriteRequest.Builder builder = connectWithWriteRequestBuilder(); + Map<String, Object> addresses = new LinkedHashMap<>(); + addresses.put("%DB1:DINT", 21); + addresses.put("%DB2:DINT", 3); + + PlcInvalidTagException e + = assertThrows(PlcInvalidTagException.class, () -> sut.buildPlcWriteRequest(Map.of("t1", addresses))); + + assertThat(e.getMessage(), containsString("but a Map of 2 addresses is")); + // nothing is written, not even the first address + verify(builder, never()).build(); + } + + @Test + void buildPlcWriteRequestFailsForATagWithoutAnAddress() throws Exception { + connectWithWriteRequestBuilder(); + + PlcInvalidTagException e + = assertThrows(PlcInvalidTagException.class, () -> sut.buildPlcWriteRequest(Map.of("t1", Map.of()))); + + assertThat(e.getMessage(), containsString("but a Map without an address is")); + } + + @Test + void buildPlcWriteRequestFailsForAnAddressThatIsNotAString() throws Exception { + connectWithWriteRequestBuilder(); + + PlcInvalidTagException e = assertThrows(PlcInvalidTagException.class, + () -> sut.buildPlcWriteRequest(untyped(Map.of("t1", Map.of(1, 21))))); + + assertThat(e.getMessage(), containsString("Tag t1: the value must be a Map of one address")); + } + + private PlcWriteRequest.Builder connectWithWriteRequestBuilder() { + PlcConnection plcConnectionMock = mock(PlcConnection.class); + PlcWriteRequest.Builder builder = mock(PlcWriteRequest.Builder.class); + when(plcConnectionMock.writeRequestBuilder()).thenReturn(builder); + sut.connection = plcConnectionMock; + return builder; + } + + // the body of a message is not checked for its types, so the tags may hold other values + @SuppressWarnings("unchecked") + private static Map<String, Map<String, Object>> untyped(Map<String, ?> tags) { + return (Map<String, Map<String, Object>>) (Map<String, ?>) tags; + } + } diff --git a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java index e1fd611d8f82..e8176a2e0e69 100644 --- a/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java +++ b/components/camel-plc4x/src/test/java/org/apache/camel/component/plc4x/Plc4XFailureTest.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.plc4x; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -25,8 +26,10 @@ import org.apache.camel.Exchange; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.support.DefaultExchange; import org.apache.plc4x.java.api.exceptions.PlcConnectionException; +import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException; import org.apache.plc4x.java.api.exceptions.PlcRuntimeException; import org.apache.plc4x.java.api.messages.PlcReadRequest; +import org.apache.plc4x.java.api.messages.PlcReadResponse; import org.apache.plc4x.java.api.messages.PlcWriteRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -37,6 +40,7 @@ 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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -94,6 +98,46 @@ class Plc4XFailureTest { assertEquals(tags, exchange.getIn().getBody()); } + @Test + void testProducerFailsForABodyThatIsNotAMap() throws Exception { + Exchange exchange = new DefaultExchange(context); + exchange.getIn().setBody("t1=42"); + + new Plc4XProducer(endpoint).process(exchange, doneSync -> { + }); + + PlcInvalidTagException e = assertInstanceOf(PlcInvalidTagException.class, exchange.getException()); + assertTrue(e.getMessage().contains("but a java.lang.String is"), e.getMessage()); + } + + @Test + void testPollingConsumerFailsForATagThatCannotBeRead() throws Exception { + // the response of the PLC fails when the value of a tag that it could not read is taken, as for an unknown tag + PlcRuntimeException cause = new PlcRuntimeException("Tag 'unknown' could not be fetched, response was NOT_FOUND"); + PlcReadResponse response = mock(PlcReadResponse.class); + when(response.getTagNames()).thenReturn(List.of("unknown")); + when(response.getObject("unknown")).thenThrow(cause); + PlcReadRequest request = mock(PlcReadRequest.class); + when(request.execute()).thenAnswer(invocation -> CompletableFuture.completedFuture(response)); + when(endpoint.buildPlcReadRequest()).thenReturn(request); + + Exchange exchange = new Plc4XPollingConsumer(endpoint).receive(1000); + + assertNotNull(exchange); + assertSame(cause, exchange.getException()); + } + + @Test + void testPollingConsumerFailsForAnInvalidTagAddress() throws Exception { + PlcInvalidTagException cause = new PlcInvalidTagException("NO_SUCH_ADDRESS"); + when(endpoint.buildPlcReadRequest()).thenThrow(cause); + + Exchange exchange = new Plc4XPollingConsumer(endpoint).receiveNoWait(); + + assertNotNull(exchange); + assertSame(cause, exchange.getException()); + } + @Test void testPollingConsumerFailsWhenReconnectFails() throws Exception { PlcConnectionException cause = new PlcConnectionException("PLC unreachable"); 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..0a32fd68a4f8 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 @@ -5136,6 +5136,14 @@ as connection string parameters too. The constant `Constants.PLC_NAME`, which was only used with the PLC4X scraper, was removed. +=== camel-plc4x - the producer checks the tags it writes + +The value of each tag in the body of the producer must now be a `Map` of exactly one address to the value to write. +A tag with several addresses now fails the exchange with a `PlcInvalidTagException`, and nothing is written; before, +only one of its addresses was written and the others were ignored. Use one tag for each address. A tag whose value is +not a `Map`, or an empty `Map`, now also fails with a `PlcInvalidTagException` instead of a `ClassCastException` or a +`NoSuchElementException`. + === camel-stripe - endpoint request options isolate credentials across endpoints The `camel-stripe` producer now applies `apiKey` and `apiBase` using endpoint-scoped `RequestOptions` on each Stripe
