This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9758b07ea6e4 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 (#29007)
9758b07ea6e4 is described below
commit 9758b07ea6e4737cc73bac9a2fe14148e2a48b45
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 07:39:54 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 (#29007)
* 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.
* CAMEL-26180: camel-plc4x - say "a Map of 1 address", not "1 addresses…
…", in the error of a write tag
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 | 36 +++++++---
.../component/plc4x/Plc4XPollingConsumer.java | 4 +-
.../camel/component/plc4x/Plc4XProducer.java | 6 +-
.../camel/component/plc4x/Plc4XEndpointTest.java | 76 ++++++++++++++++++++++
.../camel/component/plc4x/Plc4XFailureTest.java | 44 +++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 +++
10 files changed, 206 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..c7e43ecd8d0c 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,33 @@ 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) {
+ if (map.isEmpty()) {
+ return "a Map without an address";
+ }
+ return "a Map of " + map.size() + (map.size() == 1 ? " address" :
" 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..28b2d5e80502 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,76 @@ 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"));
+ assertThat(e.getMessage(), containsString("but a Map of 1 address
is"));
+ }
+
+ 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 da68f957b38c..11b88ad9662c 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
@@ -5178,6 +5178,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