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 dfa135ebf0f1 CAMEL-25308: camel-smpp - build received exchanges 
through the consumer, not the endpoint (#27329)
dfa135ebf0f1 is described below

commit dfa135ebf0f178dbb8851014352b61fd90f642ae
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Oct 5 19:04:36 2026 +0200

    CAMEL-25308: camel-smpp - build received exchanges through the consumer, 
not the endpoint (#27329)
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../smpp/MessageReceiverListenerImpl.java          | 133 ++++++++++++----
 .../apache/camel/component/smpp/SmppEndpoint.java  |  43 ++++--
 .../smpp/MessageReceiverListenerImplTest.java      | 169 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  16 ++
 4 files changed, 316 insertions(+), 45 deletions(-)

diff --git 
a/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/MessageReceiverListenerImpl.java
 
b/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/MessageReceiverListenerImpl.java
index 484a503f1c39..82cbae15aa34 100644
--- 
a/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/MessageReceiverListenerImpl.java
+++ 
b/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/MessageReceiverListenerImpl.java
@@ -86,16 +86,20 @@ public class MessageReceiverListenerImpl implements 
MessageReceiverListener {
 
         Exchange exchange = 
createOnAcceptAlertNotificationExchange(alertNotification);
         try {
-            processor.process(exchange);
-        } catch (Exception e) {
-            exchange.setException(e);
-        }
+            try {
+                processor.process(exchange);
+            } catch (Exception e) {
+                exchange.setException(e);
+            }
 
-        if (exchange.getException() != null) {
-            exceptionHandler.handleException("Cannot process exchange. This 
exception will be ignored.", exchange,
-                    exchange.getException());
+            if (exchange.getException() != null) {
+                exceptionHandler.handleException("Cannot process exchange. 
This exception will be ignored.", exchange,
+                        exchange.getException());
+            }
+        } finally {
+            // in a finally so that a custom exception handler that throws 
cannot cost the release
+            consumer.releaseExchange(exchange, false);
         }
-        consumer.releaseExchange(exchange, false);
     }
 
     @Override
@@ -110,24 +114,28 @@ public class MessageReceiverListenerImpl implements 
MessageReceiverListener {
 
         Exchange exchange;
         try {
-            exchange = endpoint.createOnAcceptDeliverSmExchange(deliverSm);
+            exchange = createOnAcceptDeliverSmExchange(deliverSm);
         } catch (Exception e) {
             exceptionHandler.handleException("Cannot create exchange. This 
exception will be ignored.", e);
             return;
         }
 
         try {
-            processor.process(exchange);
-        } catch (Exception e) {
-            exchange.setException(e);
-        }
+            try {
+                processor.process(exchange);
+            } catch (Exception e) {
+                exchange.setException(e);
+            }
 
-        if (exchange.getException() != null) {
-            ProcessRequestException pre = 
exchange.getException(ProcessRequestException.class);
-            if (pre == null) {
-                pre = new 
ProcessRequestException(exchange.getException().getMessage(), 255, 
exchange.getException());
+            if (exchange.getException() != null) {
+                ProcessRequestException pre = 
exchange.getException(ProcessRequestException.class);
+                if (pre == null) {
+                    pre = new 
ProcessRequestException(exchange.getException().getMessage(), 255, 
exchange.getException());
+                }
+                throw pre;
             }
-            throw pre;
+        } finally {
+            consumer.releaseExchange(exchange, false);
         }
     }
 
@@ -142,28 +150,82 @@ public class MessageReceiverListenerImpl implements 
MessageReceiverListener {
         LOG.debug("Received a dataSm {}", dataSm);
 
         MessageId newMessageId = messageIDGenerator.newMessageId();
-        Exchange exchange = endpoint.createOnAcceptDataSm(dataSm, 
newMessageId.getValue());
+        Exchange exchange = createOnAcceptDataSmExchange(dataSm, 
newMessageId.getValue());
         try {
-            processor.process(exchange);
-        } catch (Exception e) {
-            exchange.setException(e);
-        }
+            try {
+                processor.process(exchange);
+            } catch (Exception e) {
+                exchange.setException(e);
+            }
 
-        if (exchange.getException() != null) {
-            ProcessRequestException pre = 
exchange.getException(ProcessRequestException.class);
-            if (pre == null) {
-                pre = new 
ProcessRequestException(exchange.getException().getMessage(), 255, 
exchange.getException());
+            if (exchange.getException() != null) {
+                ProcessRequestException pre = 
exchange.getException(ProcessRequestException.class);
+                if (pre == null) {
+                    pre = new 
ProcessRequestException(exchange.getException().getMessage(), 255, 
exchange.getException());
+                }
+                throw pre;
             }
-            throw pre;
-        }
 
-        return new DataSmResult(newMessageId, dataSm.getOptionalParameters());
+            return new DataSmResult(newMessageId, 
dataSm.getOptionalParameters());
+        } finally {
+            consumer.releaseExchange(exchange, false);
+        }
     }
 
     public void setMessageIDGenerator(MessageIDGenerator messageIDGenerator) {
         this.messageIDGenerator = messageIDGenerator;
     }
 
+    /**
+     * Creates the exchange for a received {@code deliver_sm}.
+     * <p/>
+     * Built through {@link Consumer#createExchange(boolean)} rather than on 
the endpoint, as
+     * {@link org.apache.camel.Endpoint#createExchange()} instructs: an 
exchange a consumer received a message for has
+     * to come from the configured {@code ExchangeFactory}, or pooling never 
applies to it and its {@code fromRouteId}
+     * is left unset.
+     *
+     * @param  deliverSm the received message from the SMSC
+     * @return           a new exchange
+     */
+    private Exchange createOnAcceptDeliverSmExchange(DeliverSm deliverSm) 
throws Exception {
+        Exchange exchange = consumer.createExchange(false);
+        try {
+            // the pattern has to be set explicitly: in transceiver mode the 
consumer belongs to the receiver route,
+            // not to this SMPP endpoint, so the factory would otherwise stamp 
that endpoint's pattern on the exchange
+            exchange.setPattern(endpoint.getExchangePattern());
+            exchange.setProperty(Exchange.BINDING, endpoint.getBinding());
+            
exchange.setIn(endpoint.getBinding().createSmppMessage(endpoint.getCamelContext(),
 deliverSm));
+            return exchange;
+        } catch (Exception e) {
+            // the exchange is already out of the ExchangeFactory, and the 
caller's variable is still unassigned,
+            // so its finally cannot release it - a failed decode would 
otherwise cost a pooled exchange
+            consumer.releaseExchange(exchange, false);
+            throw e;
+        }
+    }
+
+    /**
+     * Creates the exchange for a received {@code data_sm}, through the 
consumer for the same reason as
+     * {@link #createOnAcceptDeliverSmExchange(DeliverSm)}.
+     *
+     * @param  dataSm        the received message from the SMSC
+     * @param  smppMessageId the smpp message id which will be used in the 
response
+     * @return               a new exchange
+     */
+    private Exchange createOnAcceptDataSmExchange(DataSm dataSm, String 
smppMessageId) {
+        Exchange exchange = consumer.createExchange(false);
+        try {
+            // see createOnAcceptDeliverSmExchange: the pattern cannot be left 
to the factory in transceiver mode
+            exchange.setPattern(endpoint.getExchangePattern());
+            exchange.setProperty(Exchange.BINDING, endpoint.getBinding());
+            
exchange.setIn(endpoint.getBinding().createSmppMessage(endpoint.getCamelContext(),
 dataSm, smppMessageId));
+            return exchange;
+        } catch (Exception e) {
+            consumer.releaseExchange(exchange, false);
+            throw e;
+        }
+    }
+
     /**
      * Create a new exchange for communicating with this endpoint from a SMSC 
with the specified {@link ExchangePattern}
      * such as whether its going to be an {@link ExchangePattern#InOnly} or 
{@link ExchangePattern#InOut} exchange
@@ -173,9 +235,14 @@ public class MessageReceiverListenerImpl implements 
MessageReceiverListener {
      */
     public Exchange createOnAcceptAlertNotificationExchange(AlertNotification 
alertNotification) {
         Exchange exchange = consumer.createExchange(false);
-        exchange.setProperty(Exchange.BINDING, endpoint.getBinding());
-        
exchange.setIn(endpoint.getBinding().createSmppMessage(endpoint.getCamelContext(),
 alertNotification));
-        return exchange;
+        try {
+            exchange.setProperty(Exchange.BINDING, endpoint.getBinding());
+            
exchange.setIn(endpoint.getBinding().createSmppMessage(endpoint.getCamelContext(),
 alertNotification));
+            return exchange;
+        } catch (Exception e) {
+            consumer.releaseExchange(exchange, false);
+            throw e;
+        }
     }
 
 }
diff --git 
a/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/SmppEndpoint.java
 
b/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/SmppEndpoint.java
index 62dc5fcfeb6f..84771fb69c4a 100644
--- 
a/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/SmppEndpoint.java
+++ 
b/components/camel-smpp/src/main/java/org/apache/camel/component/smpp/SmppEndpoint.java
@@ -81,9 +81,13 @@ public class SmppEndpoint extends DefaultEndpoint implements 
EndpointServiceLoca
     /**
      * Create a new exchange for communicating with this endpoint from a SMSC
      *
-     * @param  deliverSm the received message from the SMSC
-     * @return           a new exchange
+     * @param      deliverSm the received message from the SMSC
+     * @return               a new exchange
+     * @deprecated           an exchange a consumer received a message for has 
to come from
+     *                       {@link 
org.apache.camel.Consumer#createExchange(boolean)}, so that the configured
+     *                       {@code ExchangeFactory} sees it; {@code 
MessageReceiverListenerImpl} does that itself now
      */
+    @Deprecated
     public Exchange createOnAcceptDeliverSmExchange(DeliverSm deliverSm) 
throws Exception {
         return createOnAcceptDeliverSmExchange(getExchangePattern(), 
deliverSm);
     }
@@ -92,10 +96,15 @@ public class SmppEndpoint extends DefaultEndpoint 
implements EndpointServiceLoca
      * Create a new exchange for communicating with this endpoint from a SMSC 
with the specified {@link ExchangePattern}
      * such as whether its going to be an {@link ExchangePattern#InOnly} or 
{@link ExchangePattern#InOut} exchange
      *
-     * @param  exchangePattern the message exchange pattern for the exchange
-     * @param  deliverSm       the received message from the SMSC
-     * @return                 a new exchange
+     * @param      exchangePattern the message exchange pattern for the 
exchange
+     * @param      deliverSm       the received message from the SMSC
+     * @return                     a new exchange
+     * @deprecated                 an exchange a consumer received a message 
for has to come from
+     *                             {@link 
org.apache.camel.Consumer#createExchange(boolean)}, so that the configured
+     *                             {@code ExchangeFactory} sees it; {@code 
MessageReceiverListenerImpl} does that itself
+     *                             now
      */
+    @Deprecated
     public Exchange createOnAcceptDeliverSmExchange(
             ExchangePattern exchangePattern,
             DeliverSm deliverSm)
@@ -109,10 +118,15 @@ public class SmppEndpoint extends DefaultEndpoint 
implements EndpointServiceLoca
     /**
      * Create a new exchange for communicating with this endpoint from a SMSC
      *
-     * @param  dataSm        the received message from the SMSC
-     * @param  smppMessageId the smpp message id which will be used in the 
response
-     * @return               a new exchange
+     * @param      dataSm        the received message from the SMSC
+     * @param      smppMessageId the smpp message id which will be used in the 
response
+     * @return                   a new exchange
+     * @deprecated               an exchange a consumer received a message for 
has to come from
+     *                           {@link 
org.apache.camel.Consumer#createExchange(boolean)}, so that the configured
+     *                           {@code ExchangeFactory} sees it; {@code 
MessageReceiverListenerImpl} does that itself
+     *                           now
      */
+    @Deprecated
     public Exchange createOnAcceptDataSm(DataSm dataSm, String smppMessageId) {
         return createOnAcceptDataSm(getExchangePattern(), dataSm, 
smppMessageId);
     }
@@ -121,11 +135,16 @@ public class SmppEndpoint extends DefaultEndpoint 
implements EndpointServiceLoca
      * Create a new exchange for communicating with this endpoint from a SMSC 
with the specified {@link ExchangePattern}
      * such as whether its going to be an {@link ExchangePattern#InOnly} or 
{@link ExchangePattern#InOut} exchange
      *
-     * @param  exchangePattern the message exchange pattern for the exchange
-     * @param  dataSm          the received message from the SMSC
-     * @param  smppMessageId   the smpp message id which will be used in the 
response
-     * @return                 a new exchange
+     * @param      exchangePattern the message exchange pattern for the 
exchange
+     * @param      dataSm          the received message from the SMSC
+     * @param      smppMessageId   the smpp message id which will be used in 
the response
+     * @return                     a new exchange
+     * @deprecated                 an exchange a consumer received a message 
for has to come from
+     *                             {@link 
org.apache.camel.Consumer#createExchange(boolean)}, so that the configured
+     *                             {@code ExchangeFactory} sees it; {@code 
MessageReceiverListenerImpl} does that itself
+     *                             now
      */
+    @Deprecated
     public Exchange createOnAcceptDataSm(ExchangePattern exchangePattern, 
DataSm dataSm, String smppMessageId) {
         Exchange exchange = createExchange(exchangePattern);
         exchange.setProperty(Exchange.BINDING, getBinding());
diff --git 
a/components/camel-smpp/src/test/java/org/apache/camel/component/smpp/MessageReceiverListenerImplTest.java
 
b/components/camel-smpp/src/test/java/org/apache/camel/component/smpp/MessageReceiverListenerImplTest.java
new file mode 100644
index 000000000000..0de1acecc7f7
--- /dev/null
+++ 
b/components/camel-smpp/src/test/java/org/apache/camel/component/smpp/MessageReceiverListenerImplTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.smpp;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.Processor;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.ExceptionHandler;
+import org.apache.camel.support.DefaultExchange;
+import org.jsmpp.bean.DataSm;
+import org.jsmpp.bean.DeliverSm;
+import org.jsmpp.extra.ProcessRequestException;
+import org.jsmpp.session.Session;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * An exchange a consumer received a message for has to be built through 
{@link org.apache.camel.Consumer} rather than
+ * on the endpoint, so that the configured {@code ExchangeFactory} sees it, 
and it has to be released afterwards.
+ */
+class MessageReceiverListenerImplTest {
+
+    private SmppConsumer consumer;
+    private SmppEndpoint endpoint;
+    private CamelContext camelContext;
+    private Exchange exchange;
+
+    @BeforeEach
+    void setUp() {
+        camelContext = new DefaultCamelContext();
+        exchange = new DefaultExchange(camelContext);
+        consumer = mock(SmppConsumer.class);
+        endpoint = mock(SmppEndpoint.class);
+
+        when(consumer.createExchange(false)).thenReturn(exchange);
+        when(endpoint.getCamelContext()).thenReturn(camelContext);
+        when(endpoint.getBinding()).thenReturn(new SmppBinding());
+        when(endpoint.getExchangePattern()).thenReturn(ExchangePattern.InOnly);
+    }
+
+    private MessageReceiverListenerImpl listener(Processor processor) {
+        return new MessageReceiverListenerImpl(consumer, endpoint, processor, 
mock(ExceptionHandler.class));
+    }
+
+    private static DeliverSm deliverSm() {
+        DeliverSm deliverSm = new DeliverSm();
+        deliverSm.setShortMessage("Hello SMPP world!".getBytes());
+        return deliverSm;
+    }
+
+    @Test
+    void deliverSmTakesItsExchangeFromTheConsumerAndReleasesIt() throws 
Exception {
+        listener(received -> assertEquals("Hello SMPP world!", 
received.getIn().getBody(String.class)))
+                .onAcceptDeliverSm(deliverSm());
+
+        verify(consumer).createExchange(false);
+        verify(consumer).releaseExchange(exchange, false);
+    }
+
+    /**
+     * The failure path throws a {@link ProcessRequestException} so the SMSC 
gets a NACK, and must still release.
+     */
+    @Test
+    void deliverSmReleasesItsExchangeWhenTheRouteFailed() {
+        MessageReceiverListenerImpl listener
+                = listener(received -> received.setException(new 
IllegalStateException("the route blew up")));
+
+        ProcessRequestException thrown
+                = assertThrows(ProcessRequestException.class, () -> 
listener.onAcceptDeliverSm(deliverSm()));
+        assertEquals("the route blew up", thrown.getMessage());
+
+        verify(consumer).releaseExchange(exchange, false);
+    }
+
+    @Test
+    void dataSmTakesItsExchangeFromTheConsumerAndReleasesIt() throws Exception 
{
+        DataSm dataSm = new DataSm();
+
+        listener(received -> {
+        }).onAcceptDataSm(dataSm, mock(Session.class));
+
+        verify(consumer).createExchange(false);
+        verify(consumer).releaseExchange(exchange, false);
+    }
+
+    /**
+     * The exchange is taken from the factory before the message is decoded, 
and the caller's own {@code finally} cannot
+     * release it because its variable is still unassigned when creation 
throws. A malformed delivery receipt would
+     * otherwise cost a pooled exchange on every occurrence.
+     */
+    @Test
+    void deliverSmReleasesItsExchangeWhenTheMessageCannotBeDecoded() throws 
Exception {
+        SmppBinding failing = mock(SmppBinding.class);
+        when(failing.createSmppMessage(any(CamelContext.class), 
any(DeliverSm.class)))
+                .thenThrow(new IOException("malformed delivery receipt"));
+        when(endpoint.getBinding()).thenReturn(failing);
+
+        ExceptionHandler handler = mock(ExceptionHandler.class);
+        new MessageReceiverListenerImpl(consumer, endpoint, received -> {
+        }, handler).onAcceptDeliverSm(deliverSm());
+
+        verify(consumer).releaseExchange(exchange, false);
+        verify(handler).handleException(anyString(), any(Exception.class));
+    }
+
+    /**
+     * In transceiver mode - {@code SmppProducer} with {@code 
messageReceiverRouteId} - the consumer belongs to the
+     * receiver route rather than to the SMPP endpoint, so a received message 
now reports that route's endpoint as
+     * {@code fromEndpoint}, the way alert notifications already did. The SMPP 
endpoint's exchange pattern still has to
+     * win over the receiver endpoint's, which is why the pattern is set 
explicitly.
+     */
+    @Test
+    void transceiverModeUsesTheReceiverRouteConsumerAndKeepsTheSmppPattern() 
throws Exception {
+        AtomicReference<Exchange> received = new AtomicReference<>();
+        DefaultCamelContext trxContext = new DefaultCamelContext();
+        trxContext.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:messageReceiver").routeId("messageReceiver").process(received::set);
+            }
+        });
+
+        when(endpoint.getCamelContext()).thenReturn(trxContext);
+        when(endpoint.getExchangePattern()).thenReturn(ExchangePattern.InOut);
+
+        // constructed before the context starts, so its StartupListener 
resolves the receiver route's consumer
+        MessageReceiverListenerImpl listener = new 
MessageReceiverListenerImpl(endpoint, "messageReceiver");
+        trxContext.start();
+        try {
+            listener.onAcceptDeliverSm(deliverSm());
+
+            assertNotNull(received.get(), "the receiver route should have been 
given the message");
+            assertEquals("direct://messageReceiver", 
received.get().getFromEndpoint().getEndpointUri());
+            assertEquals(ExchangePattern.InOut, received.get().getPattern(),
+                    "the SMPP endpoint's pattern must win over the receiver 
endpoint's");
+        } finally {
+            trxContext.stop();
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 8313a4b93338..46a3d152e3c9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -591,6 +591,22 @@ falls back to its logging handler for such an exchange 
rather than bridging it a
 the consumer itself no longer stays silent, and that with 
`consumerProcessedStrategy=delete` the loss is now visible:
 the document is removed during the poll, before the route runs.
 
+=== camel-smpp - received exchanges now come from the consumer
+
+Exchanges for received `deliver_sm` and `data_sm` messages are now created 
through
+`Consumer#createExchange(boolean)` instead of on the endpoint, so that the 
configured `ExchangeFactory` sees them.
+Alert notifications already worked this way. Two consequences:
+
+* `Exchange#getFromRouteId()` is now set on received messages, where it was 
previously `null`, and exchange pooling
+  applies to them.
+* In transceiver mode - an `smpp` producer with `messageReceiverRouteId` - the 
consumer belongs to the receiver
+  route rather than to the SMPP endpoint, so `Exchange#getFromEndpoint()` now 
reports that route's endpoint (for
+  example `direct://messageReceiver`) instead of the SMPP endpoint. This 
matches what alert notifications already
+  did. The exchange pattern is unchanged: the SMPP endpoint's pattern is still 
applied explicitly.
+
+`SmppEndpoint#createOnAcceptDeliverSmExchange` and 
`SmppEndpoint#createOnAcceptDataSm` are deprecated. They are no
+longer used by the component; build such an exchange through the consumer 
instead.
+
 === camel-console
 
 The `context` developer console no longer counts routes created by Kamelets in 
its `routesTotal` and `routesStarted`

Reply via email to