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 faa015319ee7 CAMEL-25853: camel-paho, camel-paho-mqtt5, camel-hivemq - 
toD publishes to its own topic (#28326)
faa015319ee7 is described below

commit faa015319ee73edf6f93fd8351e70e49b5a2b5c7
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 23 13:52:29 2026 +0530

    CAMEL-25853: camel-paho, camel-paho-mqtt5, camel-hivemq - toD publishes to 
its own topic (#28326)
    
    The toD optimisation left the override topic header on the message, so a 
second toD of the same exchange published to the topic of the first one. A post 
processor now removes the header that the toD set.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../component/hivemq/HiveMQSendDynamicAware.java   |  14 +-
 .../camel/component/hivemq/HiveMQToDTwiceTest.java | 143 +++++++++++++++++++++
 .../paho/mqtt5/PahoMqtt5SendDynamicAware.java      |  15 ++-
 .../paho/mqtt5/PahoMqtt5ToDTwiceTest.java          |  93 ++++++++++++++
 .../camel/component/paho/PahoSendDynamicAware.java |  15 ++-
 .../camel/component/paho/PahoToDTwiceTest.java     |  93 ++++++++++++++
 6 files changed, 368 insertions(+), 5 deletions(-)

diff --git 
a/components/camel-hivemq/src/main/java/org/apache/camel/component/hivemq/HiveMQSendDynamicAware.java
 
b/components/camel-hivemq/src/main/java/org/apache/camel/component/hivemq/HiveMQSendDynamicAware.java
index c5ae3565750a..e4fb9914fafc 100644
--- 
a/components/camel-hivemq/src/main/java/org/apache/camel/component/hivemq/HiveMQSendDynamicAware.java
+++ 
b/components/camel-hivemq/src/main/java/org/apache/camel/component/hivemq/HiveMQSendDynamicAware.java
@@ -71,7 +71,19 @@ public class HiveMQSendDynamicAware extends 
SendDynamicAwareSupport {
 
     @Override
     public Processor createPostProcessor(Exchange exchange, DynamicAwareEntry 
entry) throws Exception {
-        return null;
+        if (exchange.getMessage().getHeader(HiveMQConstants.OVERRIDE_TOPIC) != 
null) {
+            // the topic header was set before toD, so it is not ours to remove
+            return null;
+        }
+
+        // remove the topic header the pre processor has set, so a later send 
of the same exchange
+        // (another toD, a loop) is not published to this topic
+        return new Processor() {
+            @Override
+            public void process(Exchange exchange) {
+                
exchange.getMessage().removeHeader(HiveMQConstants.OVERRIDE_TOPIC);
+            }
+        };
     }
 
     private String parseTopicName(String uri) {
diff --git 
a/components/camel-hivemq/src/test/java/org/apache/camel/component/hivemq/HiveMQToDTwiceTest.java
 
b/components/camel-hivemq/src/test/java/org/apache/camel/component/hivemq/HiveMQToDTwiceTest.java
new file mode 100644
index 000000000000..4be2d6ce70e9
--- /dev/null
+++ 
b/components/camel-hivemq/src/test/java/org/apache/camel/component/hivemq/HiveMQToDTwiceTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.hivemq;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+import com.hivemq.client.mqtt.datatypes.MqttQos;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Two toD to hivemq in the same exchange must publish to two topics: the 
topic header that the toD optimisation sets
+ * for the first send must not be used by the second send.
+ */
+class HiveMQToDTwiceTest {
+
+    private final List<String> published = new CopyOnWriteArrayList<>();
+
+    @Test
+    void secondToDPublishesToItsOwnTopic() throws Exception {
+        try (CamelContext context = createContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:start")
+                            .toD("hivemq:${header.first}")
+                            .toD("hivemq:${header.second}");
+                }
+            });
+            context.start();
+
+            
context.createProducerTemplate().sendBodyAndHeaders("direct:start", "Hello",
+                    Map.of("first", "status/1", "second", "audit/1"));
+
+            assertThat(published).containsExactly("status/1", "audit/1");
+        }
+    }
+
+    @Test
+    void loopWithToDPublishesEachIterationToItsOwnTopic() throws Exception {
+        try (CamelContext context = createContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:loop")
+                            .loop(3)
+                            
.toD("hivemq:devices/${exchangeProperty.CamelLoopIndex}/cmd")
+                            .end();
+                }
+            });
+            context.start();
+
+            context.createProducerTemplate().sendBody("direct:loop", "Hello");
+
+            assertThat(published).containsExactly("devices/0/cmd", 
"devices/1/cmd", "devices/2/cmd");
+        }
+    }
+
+    private CamelContext createContext() {
+        CamelContext context = new DefaultCamelContext();
+        context.addComponent("hivemq", new HiveMQComponent() {
+            @Override
+            protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters)
+                    throws Exception {
+                HiveMQEndpoint endpoint = new HiveMQEndpoint(uri, this, 
getConfiguration().copy(), remaining) {
+                    @Override
+                    HiveMQClientAdapter createClient() {
+                        return new RecordingClient();
+                    }
+                };
+                setProperties(endpoint, parameters);
+                return endpoint;
+            }
+        });
+        return context;
+    }
+
+    private final class RecordingClient implements HiveMQClientAdapter {
+        @Override
+        public CompletableFuture<?> connect(boolean cleanStart) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        public void stop() {
+        }
+
+        @Override
+        public boolean isConnected() {
+            return true;
+        }
+
+        @Override
+        public boolean isConnectedOrReconnecting() {
+            return true;
+        }
+
+        @Override
+        public CompletableFuture<?> subscribe(String topicFilter, MqttQos qos, 
Consumer<HiveMQMessage> callback) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        public CompletableFuture<?> unsubscribe(String topicFilter) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        public CompletableFuture<?> publish(String topic, byte[] payload, 
MqttQos qos, boolean retained) {
+            published.add(topic);
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        public <T> Optional<T> getClient(Class<T> clazz) {
+            return Optional.empty();
+        }
+    }
+}
diff --git 
a/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5SendDynamicAware.java
 
b/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5SendDynamicAware.java
index 57b66644b526..ce28f16bcb12 100644
--- 
a/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5SendDynamicAware.java
+++ 
b/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5SendDynamicAware.java
@@ -98,8 +98,19 @@ public class PahoMqtt5SendDynamicAware extends 
ServiceSupport implements SendDyn
 
     @Override
     public Processor createPostProcessor(Exchange exchange, DynamicAwareEntry 
entry) throws Exception {
-        // no post processor is needed
-        return null;
+        if 
(exchange.getMessage().getHeader(PahoMqtt5Constants.CAMEL_PAHO_OVERRIDE_TOPIC) 
!= null) {
+            // the topic header was set before toD, so it is not ours to remove
+            return null;
+        }
+
+        // remove the topic header the pre processor has set, so a later send 
of the same exchange
+        // (another toD, a loop) is not published to this topic
+        return new Processor() {
+            @Override
+            public void process(Exchange exchange) {
+                
exchange.getMessage().removeHeader(PahoMqtt5Constants.CAMEL_PAHO_OVERRIDE_TOPIC);
+            }
+        };
     }
 
     private String parseTopicName(String uri) {
diff --git 
a/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ToDTwiceTest.java
 
b/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ToDTwiceTest.java
new file mode 100644
index 000000000000..08f63405133c
--- /dev/null
+++ 
b/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ToDTwiceTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.paho.mqtt5;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.eclipse.paho.mqttv5.client.MqttClient;
+import org.eclipse.paho.mqttv5.common.MqttMessage;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Two toD to paho-mqtt5 in the same exchange must publish to two topics: the 
topic header that the toD optimisation
+ * sets for the first send must not be used by the second send.
+ */
+class PahoMqtt5ToDTwiceTest {
+
+    @Test
+    void secondToDPublishesToItsOwnTopic() throws Exception {
+        MqttClient client = mock(MqttClient.class);
+        try (CamelContext context = new DefaultCamelContext()) {
+            PahoMqtt5Component paho = new PahoMqtt5Component();
+            paho.setClient(client);
+            context.addComponent("paho-mqtt5", paho);
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:start")
+                            .toD("paho-mqtt5:${header.first}")
+                            .toD("paho-mqtt5:${header.second}");
+                }
+            });
+            context.start();
+
+            
context.createProducerTemplate().sendBodyAndHeaders("direct:start", "Hello",
+                    Map.of("first", "status/1", "second", "audit/1"));
+
+            ArgumentCaptor<String> topics = 
ArgumentCaptor.forClass(String.class);
+            verify(client, times(2)).publish(topics.capture(), 
any(MqttMessage.class));
+            assertEquals(List.of("status/1", "audit/1"), 
topics.getAllValues());
+        }
+    }
+
+    @Test
+    void loopWithToDPublishesEachIterationToItsOwnTopic() throws Exception {
+        MqttClient client = mock(MqttClient.class);
+        try (CamelContext context = new DefaultCamelContext()) {
+            PahoMqtt5Component paho = new PahoMqtt5Component();
+            paho.setClient(client);
+            context.addComponent("paho-mqtt5", paho);
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:loop")
+                            .loop(3)
+                            
.toD("paho-mqtt5:devices/${exchangeProperty.CamelLoopIndex}/cmd")
+                            .end();
+                }
+            });
+            context.start();
+
+            context.createProducerTemplate().sendBody("direct:loop", "Hello");
+
+            ArgumentCaptor<String> topics = 
ArgumentCaptor.forClass(String.class);
+            verify(client, times(3)).publish(topics.capture(), 
any(MqttMessage.class));
+            assertEquals(List.of("devices/0/cmd", "devices/1/cmd", 
"devices/2/cmd"), topics.getAllValues());
+        }
+    }
+}
diff --git 
a/components/camel-paho/src/main/java/org/apache/camel/component/paho/PahoSendDynamicAware.java
 
b/components/camel-paho/src/main/java/org/apache/camel/component/paho/PahoSendDynamicAware.java
index 01c59d06a06b..075e0f214d19 100644
--- 
a/components/camel-paho/src/main/java/org/apache/camel/component/paho/PahoSendDynamicAware.java
+++ 
b/components/camel-paho/src/main/java/org/apache/camel/component/paho/PahoSendDynamicAware.java
@@ -99,8 +99,19 @@ public class PahoSendDynamicAware extends ServiceSupport 
implements SendDynamicA
 
     @Override
     public Processor createPostProcessor(Exchange exchange, DynamicAwareEntry 
entry) throws Exception {
-        // no post processor is needed
-        return null;
+        if 
(exchange.getMessage().getHeader(PahoConstants.CAMEL_PAHO_OVERRIDE_TOPIC) != 
null) {
+            // the topic header was set before toD, so it is not ours to remove
+            return null;
+        }
+
+        // remove the topic header the pre processor has set, so a later send 
of the same exchange
+        // (another toD, a loop) is not published to this topic
+        return new Processor() {
+            @Override
+            public void process(Exchange exchange) {
+                
exchange.getMessage().removeHeader(PahoConstants.CAMEL_PAHO_OVERRIDE_TOPIC);
+            }
+        };
     }
 
     private String parseTopicName(String uri) {
diff --git 
a/components/camel-paho/src/test/java/org/apache/camel/component/paho/PahoToDTwiceTest.java
 
b/components/camel-paho/src/test/java/org/apache/camel/component/paho/PahoToDTwiceTest.java
new file mode 100644
index 000000000000..6c26953e6efb
--- /dev/null
+++ 
b/components/camel-paho/src/test/java/org/apache/camel/component/paho/PahoToDTwiceTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.paho;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Two toD to paho in the same exchange must publish to two topics: the topic 
header that the toD optimisation sets for
+ * the first send must not be used by the second send.
+ */
+class PahoToDTwiceTest {
+
+    @Test
+    void secondToDPublishesToItsOwnTopic() throws Exception {
+        MqttClient client = mock(MqttClient.class);
+        try (CamelContext context = new DefaultCamelContext()) {
+            PahoComponent paho = new PahoComponent();
+            paho.setClient(client);
+            context.addComponent("paho", paho);
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:start")
+                            .toD("paho:${header.first}")
+                            .toD("paho:${header.second}");
+                }
+            });
+            context.start();
+
+            
context.createProducerTemplate().sendBodyAndHeaders("direct:start", "Hello",
+                    Map.of("first", "status/1", "second", "audit/1"));
+
+            ArgumentCaptor<String> topics = 
ArgumentCaptor.forClass(String.class);
+            verify(client, times(2)).publish(topics.capture(), 
any(MqttMessage.class));
+            assertEquals(List.of("status/1", "audit/1"), 
topics.getAllValues());
+        }
+    }
+
+    @Test
+    void loopWithToDPublishesEachIterationToItsOwnTopic() throws Exception {
+        MqttClient client = mock(MqttClient.class);
+        try (CamelContext context = new DefaultCamelContext()) {
+            PahoComponent paho = new PahoComponent();
+            paho.setClient(client);
+            context.addComponent("paho", paho);
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:loop")
+                            .loop(3)
+                            
.toD("paho:devices/${exchangeProperty.CamelLoopIndex}/cmd")
+                            .end();
+                }
+            });
+            context.start();
+
+            context.createProducerTemplate().sendBody("direct:loop", "Hello");
+
+            ArgumentCaptor<String> topics = 
ArgumentCaptor.forClass(String.class);
+            verify(client, times(3)).publish(topics.capture(), 
any(MqttMessage.class));
+            assertEquals(List.of("devices/0/cmd", "devices/1/cmd", 
"devices/2/cmd"), topics.getAllValues());
+        }
+    }
+}

Reply via email to