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

Reply via email to