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 a4ff25a844c0 CAMEL-25841: camel-xmpp - do not hand out the endpoint of 
another participant or user (#28262)
a4ff25a844c0 is described below

commit a4ff25a844c00b69fb81f96997a761b198cfba4a
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 23 02:19:15 2026 +0530

    CAMEL-25841: camel-xmpp - do not hand out the endpoint of another 
participant or user (#28262)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/docs/xmpp-component.adoc  |   7 +
 components/camel-xmpp/pom.xml                      |   6 +
 .../camel-xmpp/src/main/docs/xmpp-component.adoc   |   7 +
 .../apache/camel/component/xmpp/XmppComponent.java | 166 +++++++++--
 .../apache/camel/component/xmpp/XmppConsumer.java  |  27 +-
 .../camel/component/xmpp/XmppDirectProducer.java   |  22 +-
 .../apache/camel/component/xmpp/XmppEndpoint.java  |  65 +++--
 .../component/xmpp/XmppGroupChatProducer.java      |  34 +--
 .../component/xmpp/XmppPrivateChatProducer.java    |  17 +-
 .../camel/component/xmpp/XmppPubSubProducer.java   |  20 +-
 .../camel/component/xmpp/FakeXmppServer.java       | 305 +++++++++++++++++++++
 .../xmpp/XmppComponentEndpointPerUriTest.java      |  71 +++++
 .../component/xmpp/XmppSharedConnectionTest.java   | 289 +++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 +
 14 files changed, 968 insertions(+), 82 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmpp-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmpp-component.adoc
index 98b432855851..7354f9322955 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmpp-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmpp-component.adoc
@@ -48,6 +48,13 @@ include::partial$component-endpoint-headers.adoc[]
 
 == Usage
 
+=== Connections
+
+The consumers and producers of one account (the same host, port, service name, 
user, password, resource and login
+settings) share one XMPP connection, also across endpoints with different 
participants or rooms. With the
+`connectionConfig` option, endpoints share a connection only when they refer 
to the same `ConnectionConfiguration`
+instance, for example the same bean in the registry, because that class has no 
`equals()`.
+
 === Headers and setting Subject or Language
 
 Camel sets the message IN headers as properties on the XMPP message. You
diff --git a/components/camel-xmpp/pom.xml b/components/camel-xmpp/pom.xml
index 130814b73dbd..928b74a4b185 100644
--- a/components/camel-xmpp/pom.xml
+++ b/components/camel-xmpp/pom.xml
@@ -49,6 +49,12 @@
             <artifactId>jxmpp-jid</artifactId>
             <version>${jxmpp-version}</version>
         </dependency>
+        <!-- smack-core needs it, but the resolver used on CI can leave it out 
(smack asks for jxmpp in a version range) -->
+        <dependency>
+            <groupId>org.jxmpp</groupId>
+            <artifactId>jxmpp-util-cache</artifactId>
+            <version>${jxmpp-version}</version>
+        </dependency>
         <dependency>
             <groupId>org.igniterealtime.smack</groupId>
             <artifactId>smack-core</artifactId>
diff --git a/components/camel-xmpp/src/main/docs/xmpp-component.adoc 
b/components/camel-xmpp/src/main/docs/xmpp-component.adoc
index 98b432855851..7354f9322955 100644
--- a/components/camel-xmpp/src/main/docs/xmpp-component.adoc
+++ b/components/camel-xmpp/src/main/docs/xmpp-component.adoc
@@ -48,6 +48,13 @@ include::partial$component-endpoint-headers.adoc[]
 
 == Usage
 
+=== Connections
+
+The consumers and producers of one account (the same host, port, service name, 
user, password, resource and login
+settings) share one XMPP connection, also across endpoints with different 
participants or rooms. With the
+`connectionConfig` option, endpoints share a connection only when they refer 
to the same `ConnectionConfiguration`
+instance, for example the same bean in the registry, because that class has no 
`equals()`.
+
 === Headers and setting Subject or Language
 
 Camel sets the message IN headers as properties on the XMPP message. You
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppComponent.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppComponent.java
index 96e6524b33ae..861a0d9a70ee 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppComponent.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppComponent.java
@@ -16,17 +16,25 @@
  */
 package org.apache.camel.component.xmpp;
 
+import java.io.IOException;
 import java.net.URI;
-import java.net.URISyntaxException;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.camel.Endpoint;
 import org.apache.camel.spi.annotations.Component;
 import org.apache.camel.support.DefaultComponent;
-import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.URISupport;
 import org.jivesoftware.smack.ReconnectionManager;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,20 +43,15 @@ public class XmppComponent extends DefaultComponent {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(XmppComponent.class);
 
-    // keep a cache of endpoints so they can be properly cleaned up
-    private final Map<String, XmppEndpoint> endpointCache = new HashMap<>();
+    // the connections of the endpoints, one per account: a server keeps one 
session per account and resource, and
+    // closes the older session (conflict) when another one logs in, so the 
endpoints of an account share its connection
+    private final Map<List<Object>, SharedConnection> connections = new 
HashMap<>();
 
     public XmppComponent() {
     }
 
     @Override
     protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) throws Exception {
-        String cacheKey = extractCacheKeyFromUri(uri);
-        if (endpointCache.containsKey(cacheKey)) {
-            LOG.debug("Using cached endpoint for URI {}", 
URISupport.sanitizeUri(uri));
-            return endpointCache.get(cacheKey);
-        }
-
         LOG.debug("Creating new endpoint for URI {}", 
URISupport.sanitizeUri(uri));
         XmppEndpoint endpoint = new XmppEndpoint(uri, this);
 
@@ -78,8 +81,6 @@ public class XmppComponent extends DefaultComponent {
             }
         }
 
-        endpointCache.put(cacheKey, endpoint);
-
         return endpoint;
     }
 
@@ -92,14 +93,145 @@ public class XmppComponent extends DefaultComponent {
 
     @Override
     protected void doStop() throws Exception {
-        ServiceHelper.stopService(endpointCache.values());
-        endpointCache.clear();
+        List<SharedConnection> remaining;
+        synchronized (connections) {
+            remaining = List.copyOf(connections.values());
+            connections.clear();
+        }
+        for (SharedConnection shared : remaining) {
+            shared.disconnect();
+        }
 
         super.doStop();
     }
 
-    private String extractCacheKeyFromUri(String uri) throws 
URISyntaxException {
-        URI u = new URI(uri);
-        return u.getScheme() + "://" + u.getHost() + u.getPort() + 
u.getQuery();
+    /**
+     * Gets the connection of the endpoint's account for the given consumer or 
producer, and creates it when the account
+     * has no connection yet. A connection that is no longer connected is 
connected and logged in again, not replaced:
+     * the consumers and producers that got it before keep using that object 
(and Smack's ReconnectionManager keeps
+     * reconnecting it), so a second connection of the account would log in 
the same JID and resource again and the
+     * server would close one of the two (conflict). The connection is shared 
with the other consumers and producers of
+     * the account, also of other endpoints; release it with {@link 
#releaseConnection(XmppEndpoint, Object)}.
+     */
+    XMPPTCPConnection acquireConnection(XmppEndpoint endpoint, Object user)
+            throws InterruptedException, IOException, SmackException, 
XMPPException {
+        List<Object> key = endpoint.getConnectionKey();
+        SharedConnection shared;
+        synchronized (connections) {
+            shared = connections.computeIfAbsent(key, k -> new 
SharedConnection());
+            shared.users.add(user);
+        }
+        boolean acquired = false;
+        shared.lock.lock();
+        try {
+            if (shared.connection == null) {
+                shared.connection = endpoint.createConnection();
+            } else {
+                connectAndLogin(endpoint, shared.connection);
+            }
+            acquired = true;
+            return shared.connection;
+        } finally {
+            shared.lock.unlock();
+            if (!acquired) {
+                releaseConnection(endpoint, user);
+            }
+        }
     }
+
+    /**
+     * Connects and logs in the shared connection of the endpoint's account 
again when it is not connected or not logged
+     * in. The consumers and producers of an account share one connection, so 
the reconnect is done under the account's
+     * lock.
+     */
+    void reconnect(XmppEndpoint endpoint, XMPPTCPConnection connection)
+            throws InterruptedException, IOException, SmackException, 
XMPPException {
+        SharedConnection shared;
+        synchronized (connections) {
+            shared = connections.get(endpoint.getConnectionKey());
+        }
+        Lock lock = shared != null ? shared.lock : null;
+        if (lock != null) {
+            lock.lock();
+        }
+        try {
+            connectAndLogin(endpoint, connection);
+        } finally {
+            if (lock != null) {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Connects the connection when it is not connected, and logs it in again 
when it is not logged in (a connection
+     * that is connected again is not logged in, and the server delivers 
nothing to it until it is). Smack's
+     * {@code connect()} and {@code login()} throw {@link 
SmackException.AlreadyConnectedException} and
+     * {@link SmackException.AlreadyLoggedInException} to a second caller 
(such as Smack's ReconnectionManager), which
+     * only means that it was done already. {@code login()} logs in with the 
user, password and resource the connection
+     * logged in with before.
+     */
+    private static void connectAndLogin(XmppEndpoint endpoint, 
XMPPTCPConnection connection)
+            throws InterruptedException, IOException, SmackException, 
XMPPException {
+        if (!connection.isConnected()) {
+            LOG.debug("Reconnecting to: {}", 
XmppEndpoint.getConnectionMessage(connection));
+            try {
+                connection.connect();
+            } catch (SmackException.AlreadyConnectedException e) {
+                LOG.debug("Already connected again: {}", 
XmppEndpoint.getConnectionMessage(connection));
+            }
+        }
+        if (!connection.isAuthenticated() && (endpoint.getUser() == null || 
endpoint.isLogin())) {
+            LOG.debug("Logging in again to: {}", 
XmppEndpoint.getConnectionMessage(connection));
+            try {
+                connection.login();
+            } catch (SmackException.AlreadyLoggedInException e) {
+                LOG.debug("Already logged in again: {}", 
XmppEndpoint.getConnectionMessage(connection));
+            }
+        }
+    }
+
+    /**
+     * Releases the connection that the given consumer or producer acquired, 
and disconnects it when no consumer or
+     * producer of the account uses it any more.
+     */
+    void releaseConnection(XmppEndpoint endpoint, Object user) {
+        List<Object> key = endpoint.getConnectionKey();
+        SharedConnection unused = null;
+        synchronized (connections) {
+            SharedConnection shared = connections.get(key);
+            if (shared != null && shared.users.remove(user) && 
shared.users.isEmpty()) {
+                connections.remove(key);
+                unused = shared;
+            }
+        }
+        if (unused != null) {
+            unused.disconnect();
+        }
+    }
+
+    /**
+     * The number of consumers and producers that use the connection of the 
endpoint's account. For tests.
+     */
+    int connectionUsers(XmppEndpoint endpoint) {
+        synchronized (connections) {
+            SharedConnection shared = 
connections.get(endpoint.getConnectionKey());
+            return shared != null ? shared.users.size() : 0;
+        }
+    }
+
+    private static final class SharedConnection {
+        private final Lock lock = new ReentrantLock();
+        private final Set<Object> users = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        private volatile XMPPTCPConnection connection;
+
+        void disconnect() {
+            XMPPTCPConnection current = connection;
+            if (current != null) {
+                // also when it is not connected: that stops Smack's 
ReconnectionManager from connecting it again
+                current.disconnect();
+            }
+        }
+    }
+
 }
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppConsumer.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppConsumer.java
index bd4b9906a159..7db77ce9963d 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppConsumer.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppConsumer.java
@@ -69,7 +69,7 @@ public class XmppConsumer extends DefaultConsumer implements 
IncomingChatMessage
     @Override
     protected void doStart() throws Exception {
         try {
-            connection = endpoint.createConnection();
+            connection = endpoint.acquireConnection(this);
         } catch (SmackException e) {
             if (endpoint.isTestConnectionOnStartup()) {
                 throw new RuntimeCamelException("Could not connect to XMPP 
server.", e);
@@ -184,11 +184,28 @@ public class XmppConsumer extends DefaultConsumer 
implements IncomingChatMessage
         if (muc != null) {
             LOG.info("Leaving room: {}", muc.getRoom());
             muc.removeMessageListener(this);
-            muc.leave();
-            muc = null;
+            try {
+                muc.leave();
+            } finally {
+                muc = null;
+                // also when leaving failed: the connection is down, or Smack 
no longer has us in the room
+                releaseConnection();
+            }
         }
-        if (connection != null && connection.isConnected()) {
-            connection.disconnect();
+        releaseConnection();
+    }
+
+    private void releaseConnection() {
+        if (connection != null) {
+            // the connection is shared with the other consumers and producers 
of the account: stop listening on it,
+            // and release it (it is closed when nobody uses it any more)
+            if (chatManager != null) {
+                chatManager.removeIncomingListener(this);
+                chatManager = null;
+            }
+            connection.removeSyncStanzaListener(this);
+            endpoint.releaseConnection(this);
+            connection = null;
         }
     }
 
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppDirectProducer.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppDirectProducer.java
index 20fad1159d3d..87cb1d8d37e0 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppDirectProducer.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppDirectProducer.java
@@ -42,17 +42,11 @@ public class XmppDirectProducer extends DefaultProducer {
     public void process(Exchange exchange) throws Exception {
         try {
             if (connection == null) {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             }
 
-            // make sure we are connected
-            if (!connection.isConnected()) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug("Reconnecting to: {}", 
XmppEndpoint.getConnectionMessage(connection));
-                }
-
-                connection.connect();
-            }
+            // make sure we are connected (the connection is shared by the 
consumers and producers of the account)
+            endpoint.reconnect(connection);
         } catch (XMPPException e) {
             throw new RuntimeExchangeException(
                     "Cannot connect to XMPP Server: "
@@ -89,4 +83,14 @@ public class XmppDirectProducer extends DefaultProducer {
                     exchange, e);
         }
     }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (connection != null) {
+            // the connection is shared with the other consumers and producers 
of the account
+            endpoint.releaseConnection(this);
+        }
+        connection = null;
+        super.doStop();
+    }
 }
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppEndpoint.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppEndpoint.java
index 12ed1e40ffa6..87c5409dea0d 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppEndpoint.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppEndpoint.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.xmpp;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
@@ -65,7 +66,6 @@ public class XmppEndpoint extends DefaultEndpoint implements 
HeaderFilterStrateg
 
     private static final Logger LOG = 
LoggerFactory.getLogger(XmppEndpoint.class);
 
-    private volatile XMPPTCPConnection connection;
     private XmppBinding binding;
 
     @UriPath
@@ -177,18 +177,51 @@ public class XmppEndpoint extends DefaultEndpoint 
implements HeaderFilterStrateg
         return "xmpp://" + host + ":" + port + "/" + getParticipant() + 
"?serviceName=" + serviceName;
     }
 
+    /**
+     * Gets the connection of this endpoint's account for the given consumer 
or producer. The endpoints of one account
+     * share its connection (a server keeps one session per account and 
resource); each endpoint keeps its own
+     * participant, room and other settings. Release it with {@link 
#releaseConnection(Object)} when the consumer or
+     * producer stops: the connection is closed when no consumer or producer 
of the account uses it any more.
+     */
+    public XMPPTCPConnection acquireConnection(Object user)
+            throws InterruptedException, IOException, SmackException, 
XMPPException {
+        return getComponent().acquireConnection(this, user);
+    }
+
+    /**
+     * Connects and logs in the given connection, acquired with {@link 
#acquireConnection(Object)}, again when it is not
+     * connected or not logged in; safe when other consumers or producers of 
the account do the same at the same time.
+     */
+    public void reconnect(XMPPTCPConnection connection)
+            throws InterruptedException, IOException, SmackException, 
XMPPException {
+        getComponent().reconnect(this, connection);
+    }
+
+    /**
+     * Releases the connection that the given consumer or producer acquired 
with {@link #acquireConnection(Object)}.
+     */
+    public void releaseConnection(Object user) {
+        getComponent().releaseConnection(this, user);
+    }
+
+    /**
+     * The settings that make up the connection of an account: the endpoints 
with the same settings share a connection.
+     * Smack's {@code ConnectionConfiguration} does not implement {@code 
equals()}, so endpoints that set the
+     * {@code connectionConfig} option share a connection only when they refer 
to the same instance (for example the
+     * same bean in the registry).
+     */
+    List<Object> getConnectionKey() {
+        return Arrays.asList(host, port == 0 ? 5222 : port, serviceName, user, 
password, resource, login, connectionConfig);
+    }
+
+    /**
+     * Creates a new connection, connected and logged in. The consumers and 
producers use the connection of the account
+     * instead, see {@link #acquireConnection(Object)}.
+     */
     public XMPPTCPConnection createConnection()
             throws InterruptedException, IOException, SmackException, 
XMPPException {
         lock.lock();
         try {
-            if (connection != null && connection.isConnected()) {
-                // use existing working connection
-                return connection;
-            }
-
-            // prepare for creating new connection
-            connection = null;
-
             LOG.trace("Creating new connection ...");
             XMPPTCPConnection newConnection = createConnectionInternal();
 
@@ -229,10 +262,9 @@ public class XmppEndpoint extends DefaultEndpoint 
implements HeaderFilterStrateg
                 // presence is not needed to be sent after login
             }
 
-            // okay new connection was created successfully so assign it as 
the connection
+            // okay new connection was created successfully
             LOG.debug("Created new connection successfully: {}", 
newConnection);
-            connection = newConnection;
-            return connection;
+            return newConnection;
         } finally {
             lock.unlock();
         }
@@ -540,12 +572,13 @@ public class XmppEndpoint extends DefaultEndpoint 
implements HeaderFilterStrateg
     // Implementation methods
     // 
-------------------------------------------------------------------------
 
+    @Override
+    public XmppComponent getComponent() {
+        return (XmppComponent) super.getComponent();
+    }
+
     @Override
     protected void doStop() throws Exception {
-        if (connection != null) {
-            connection.disconnect();
-        }
-        connection = null;
         binding = null;
     }
 
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppGroupChatProducer.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppGroupChatProducer.java
index c6c1dc7a16fc..2d6869a941d7 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppGroupChatProducer.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppGroupChatProducer.java
@@ -51,7 +51,7 @@ public class XmppGroupChatProducer extends DefaultProducer {
     public void process(Exchange exchange) {
         if (connection == null) {
             try {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
                 throw new RuntimeExchangeException("Interrupted while 
connecting to XMPP server.", exchange, e);
@@ -101,12 +101,8 @@ public class XmppGroupChatProducer extends DefaultProducer 
{
     private void reconnect() throws InterruptedException, IOException, 
SmackException, XMPPException {
         lock.lock();
         try {
-            if (!connection.isConnected()) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug("Reconnecting to: {}", 
XmppEndpoint.getConnectionMessage(connection));
-                }
-                connection.connect();
-            }
+            // the connection is shared by the consumers and producers of the 
account
+            endpoint.reconnect(connection);
         } finally {
             lock.unlock();
         }
@@ -116,7 +112,7 @@ public class XmppGroupChatProducer extends DefaultProducer {
     protected void doStart() throws Exception {
         if (connection == null) {
             try {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             } catch (SmackException e) {
                 if (endpoint.isTestConnectionOnStartup()) {
                     throw new RuntimeCamelException(
@@ -160,16 +156,20 @@ public class XmppGroupChatProducer extends 
DefaultProducer {
 
     @Override
     protected void doStop() throws Exception {
-        if (chat != null) {
-            LOG.info("Leaving room: {}", room);
-            chat.leave();
-        }
-        chat = null;
-
-        if (connection != null && connection.isConnected()) {
-            connection.disconnect();
+        try {
+            if (chat != null) {
+                LOG.info("Leaving room: {}", room);
+                chat.leave();
+            }
+        } finally {
+            chat = null;
+            // also when leaving failed: the connection is down, or Smack no 
longer has us in the room
+            if (connection != null) {
+                // the connection is shared with the other consumers and 
producers of the account
+                endpoint.releaseConnection(this);
+            }
+            connection = null;
         }
-        connection = null;
 
         super.doStop();
     }
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPrivateChatProducer.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPrivateChatProducer.java
index fee149fcd521..950d9242da6f 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPrivateChatProducer.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPrivateChatProducer.java
@@ -55,7 +55,7 @@ public class XmppPrivateChatProducer extends DefaultProducer {
         // make sure we are connected
         try {
             if (connection == null) {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             }
 
             if (!connection.isConnected()) {
@@ -113,12 +113,8 @@ public class XmppPrivateChatProducer extends 
DefaultProducer {
     private void reconnect() throws InterruptedException, IOException, 
SmackException, XMPPException {
         lock.lock();
         try {
-            if (!connection.isConnected()) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug("Reconnecting to: {}", 
XmppEndpoint.getConnectionMessage(connection));
-                }
-                connection.connect();
-            }
+            // the connection is shared by the consumers and producers of the 
account
+            endpoint.reconnect(connection);
         } finally {
             lock.unlock();
         }
@@ -128,7 +124,7 @@ public class XmppPrivateChatProducer extends 
DefaultProducer {
     protected void doStart() throws Exception {
         if (connection == null) {
             try {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             } catch (SmackException e) {
                 if (endpoint.isTestConnectionOnStartup()) {
                     throw new RuntimeCamelException(
@@ -144,8 +140,9 @@ public class XmppPrivateChatProducer extends 
DefaultProducer {
 
     @Override
     protected void doStop() throws Exception {
-        if (connection != null && connection.isConnected()) {
-            connection.disconnect();
+        if (connection != null) {
+            // the connection is shared with the other consumers and producers 
of the account
+            endpoint.releaseConnection(this);
         }
         connection = null;
         super.doStop();
diff --git 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPubSubProducer.java
 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPubSubProducer.java
index 778d2ec6ac25..e71848572dbe 100644
--- 
a/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPubSubProducer.java
+++ 
b/components/camel-xmpp/src/main/java/org/apache/camel/component/xmpp/XmppPubSubProducer.java
@@ -40,16 +40,11 @@ public class XmppPubSubProducer extends DefaultProducer {
     public void process(Exchange exchange) throws Exception {
         try {
             if (connection == null) {
-                connection = endpoint.createConnection();
+                connection = endpoint.acquireConnection(this);
             }
 
-            // make sure we are connected
-            if (!connection.isConnected()) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug("Reconnecting to: {}", 
XmppEndpoint.getConnectionMessage(connection));
-                }
-                connection.connect();
-            }
+            // make sure we are connected (the connection is shared by the 
consumers and producers of the account)
+            endpoint.reconnect(connection);
         } catch (XMPPException e) {
             throw new RuntimeExchangeException(
                     "Cannot connect to XMPP Server: "
@@ -76,4 +71,13 @@ public class XmppPubSubProducer extends DefaultProducer {
         }
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        if (connection != null) {
+            // the connection is shared with the other consumers and producers 
of the account
+            endpoint.releaseConnection(this);
+        }
+        connection = null;
+        super.doStop();
+    }
 }
diff --git 
a/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/FakeXmppServer.java
 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/FakeXmppServer.java
new file mode 100644
index 000000000000..d1591b45ccac
--- /dev/null
+++ 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/FakeXmppServer.java
@@ -0,0 +1,305 @@
+/*
+ * 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.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A small XMPP server for tests, on plain TCP, that does what Smack needs to 
connect, log in (SASL PLAIN, any password,
+ * or ANONYMOUS), bind a resource, send messages and enter a multi-user chat 
room. Like a real server it only delivers
+ * stanzas to sessions that logged in and bound a resource, and to room 
occupants that entered the room on their current
+ * session, and it closes the older session with a conflict stream error when 
a session binds the full JID (account and
+ * resource) of another session.
+ */
+final class FakeXmppServer implements AutoCloseable {
+
+    static final String DOMAIN = "example.com";
+
+    private static final Pattern STREAM = 
Pattern.compile("<stream:stream[^>]*>");
+    private static final Pattern AUTH = 
Pattern.compile("<auth[^>]*?(/>|>[^<]*</auth>)");
+    private static final Pattern IQ = 
Pattern.compile("<iq[^>]*?(/>|>.*?</iq>)", Pattern.DOTALL);
+    private static final Pattern PRESENCE = 
Pattern.compile("<presence[^>]*?(/>|>.*?</presence>)", Pattern.DOTALL);
+    private static final Pattern MESSAGE = 
Pattern.compile("<message[^>]*?(/>|>.*?</message>)", Pattern.DOTALL);
+    private static final Pattern ID = 
Pattern.compile("\\sid=['\"]([^'\"]+)['\"]");
+    private static final Pattern TO = 
Pattern.compile("\\sto=['\"]([^'\"]+)['\"]");
+    private static final Pattern MECHANISM = 
Pattern.compile("\\smechanism=['\"]([^'\"]+)['\"]");
+    private static final Pattern AUTH_DATA = 
Pattern.compile(">([^<]*)</auth>");
+    private static final Pattern RESOURCE = 
Pattern.compile("<resource>([^<]+)</resource>");
+    private static final Pattern BODY = 
Pattern.compile("<body[^>]*>(.*?)</body>", Pattern.DOTALL);
+
+    private final ServerSocket serverSocket;
+    private final List<Session> sessions = new CopyOnWriteArrayList<>();
+    private final List<String[]> received = new CopyOnWriteArrayList<>();
+    private final AtomicInteger counter = new AtomicInteger();
+    private final AtomicInteger binds = new AtomicInteger();
+    private final AtomicInteger conflicts = new AtomicInteger();
+    private volatile boolean running = true;
+
+    FakeXmppServer() throws IOException {
+        serverSocket = new ServerSocket(0, 50, 
InetAddress.getLoopbackAddress());
+        Thread acceptor = new Thread(this::accept, "fake-xmpp-acceptor");
+        acceptor.setDaemon(true);
+        acceptor.start();
+    }
+
+    int getPort() {
+        return serverSocket.getLocalPort();
+    }
+
+    /**
+     * The number of sessions that logged in and bound a resource.
+     */
+    long boundSessions() {
+        return sessions.stream().filter(s -> s.jid != null && 
!s.socket.isClosed()).count();
+    }
+
+    /**
+     * The number of times a session bound a resource since the server started.
+     */
+    int binds() {
+        return binds.get();
+    }
+
+    /**
+     * The number of sessions closed with a conflict stream error, as another 
session bound the same full JID.
+     */
+    int conflicts() {
+        return conflicts.get();
+    }
+
+    /**
+     * The number of sessions that are occupants of the given room.
+     */
+    long occupants(String room) {
+        return sessions.stream().filter(s -> room.equals(s.room) && 
!s.socket.isClosed()).count();
+    }
+
+    /**
+     * The bodies of the messages the clients sent to the given JID.
+     */
+    List<String> messagesTo(String jid) {
+        return received.stream().filter(m -> jid.equals(m[0])).map(m -> 
m[1]).toList();
+    }
+
+    /**
+     * Drops every connection, as a network failure or a server restart does.
+     */
+    void dropConnections() throws IOException {
+        for (Session session : sessions) {
+            session.socket.close();
+        }
+        sessions.clear();
+    }
+
+    /**
+     * Sends a chat message to every bound session.
+     */
+    void sendChat(String from, String body) throws IOException {
+        for (Session session : sessions) {
+            if (session.jid != null && !session.socket.isClosed()) {
+                session.write("<message from='" + from + "/phone' to='" + 
session.jid + "' type='chat' id='m"
+                              + counter.incrementAndGet() + "'><body>" + body 
+ "</body></message>");
+            }
+        }
+    }
+
+    /**
+     * Sends a groupchat message to every occupant of the room.
+     */
+    void sendGroupChat(String room, String nick, String body) throws 
IOException {
+        for (Session session : sessions) {
+            if (room.equals(session.room) && !session.socket.isClosed()) {
+                session.write("<message from='" + room + "/" + nick + "' to='" 
+ session.jid + "' type='groupchat' id='g"
+                              + counter.incrementAndGet() + "'><body>" + body 
+ "</body></message>");
+            }
+        }
+    }
+
+    private void accept() {
+        while (running) {
+            try {
+                Socket socket = serverSocket.accept();
+                Session session = new Session(socket);
+                sessions.add(session);
+                Thread reader = new Thread(session::read, "fake-xmpp-session");
+                reader.setDaemon(true);
+                reader.start();
+            } catch (IOException e) {
+                // closed
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        running = false;
+        serverSocket.close();
+        dropConnections();
+    }
+
+    private final class Session {
+        private final Socket socket;
+        private final StringBuilder buffer = new StringBuilder();
+        private volatile String user;
+        private volatile String jid;
+        private volatile String room;
+
+        Session(Socket socket) {
+            this.socket = socket;
+        }
+
+        synchronized void write(String xml) throws IOException {
+            OutputStream out = socket.getOutputStream();
+            out.write(xml.getBytes(StandardCharsets.UTF_8));
+            out.flush();
+        }
+
+        void read() {
+            byte[] bytes = new byte[8192];
+            try (InputStream in = socket.getInputStream()) {
+                int n;
+                while ((n = in.read(bytes)) != -1) {
+                    buffer.append(new String(bytes, 0, n, 
StandardCharsets.UTF_8));
+                    handle();
+                }
+            } catch (IOException e) {
+                // the connection was closed
+            }
+        }
+
+        private void handle() throws IOException {
+            boolean progress = true;
+            while (progress) {
+                progress = false;
+                for (Pattern pattern : List.of(STREAM, AUTH, IQ, PRESENCE, 
MESSAGE)) {
+                    Matcher m = pattern.matcher(buffer);
+                    if (m.find() && buffer.substring(0, 
m.start()).trim().replaceAll("<\\?xml[^>]*\\?>", "").isEmpty()) {
+                        String element = m.group();
+                        buffer.delete(0, m.end());
+                        onElement(pattern, element);
+                        progress = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        private void onElement(Pattern pattern, String element) throws 
IOException {
+            if (pattern == STREAM) {
+                String features = user != null
+                        ? "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
+                        : "<mechanisms 
xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism>"
+                          + "<mechanism>ANONYMOUS</mechanism></mechanisms>";
+                write("<?xml version='1.0'?><stream:stream 
xmlns='jabber:client' "
+                      + "xmlns:stream='http://etherx.jabber.org/streams' 
id='s" + counter.incrementAndGet()
+                      + "' from='" + DOMAIN + "' version='1.0' 
xml:lang='en'><stream:features>" + features
+                      + "</stream:features>");
+            } else if (pattern == AUTH) {
+                user = "PLAIN".equals(attribute(MECHANISM, element))
+                        ? new 
String(Base64.getDecoder().decode(content(AUTH_DATA, element)), 
StandardCharsets.UTF_8)
+                                .split("\0")[1]
+                        : "anon" + counter.incrementAndGet();
+                write("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
+            } else if (pattern == IQ) {
+                String id = attribute(ID, element);
+                if (user == null) {
+                    return;
+                }
+                if (element.contains("urn:ietf:params:xml:ns:xmpp-bind")) {
+                    String resource = content(RESOURCE, element);
+                    bind(user + "@" + DOMAIN + "/" + (resource != null ? 
resource : "camel"));
+                    write("<iq type='result' id='" + id + "'><bind 
xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>" + jid
+                          + "</jid></bind></iq>");
+                } else if (element.contains("jabber:iq:roster")) {
+                    write("<iq type='result' id='" + id + "' to='" + jid + 
"'><query xmlns='jabber:iq:roster'/></iq>");
+                } else if (element.contains("type='get'") || 
element.contains("type=\"get\"")
+                        || element.contains("type='set'") || 
element.contains("type=\"set\"")) {
+                    String to = attribute(TO, element);
+                    String from = to != null ? " from='" + to + "'" : "";
+                    if (to != null && to.startsWith("conference.") && 
element.contains("disco#info")) {
+                        // the multi-user chat service
+                        write("<iq type='result' id='" + id + "'" + from + " 
to='" + jid + "'>"
+                              + "<query 
xmlns='http://jabber.org/protocol/disco#info'>"
+                              + "<identity category='conference' type='text' 
name='Rooms'/>"
+                              + "<feature 
var='http://jabber.org/protocol/muc'/></query></iq>");
+                    } else {
+                        write("<iq type='result' id='" + id + "'" + from + " 
to='" + jid + "'/>");
+                    }
+                }
+            } else if (pattern == PRESENCE) {
+                String to = attribute(TO, element);
+                if (jid == null || to == null || 
!element.contains("http://jabber.org/protocol/muc";)) {
+                    return;
+                }
+                if (element.contains("type='unavailable'") || 
element.contains("type=\"unavailable\"")) {
+                    room = null;
+                    write("<presence from='" + to + "' to='" + jid + "' 
type='unavailable'>"
+                          + "<x 
xmlns='http://jabber.org/protocol/muc#user'><item affiliation='none' 
role='none'/>"
+                          + "<status code='110'/></x></presence>");
+                    return;
+                }
+                // entering a room: the room answers with the occupant's own 
presence (status 110)
+                room = to.substring(0, to.indexOf('/'));
+                write("<presence from='" + to + "' to='" + jid + "' id='" + 
attribute(ID, element) + "'>"
+                      + "<x xmlns='http://jabber.org/protocol/muc#user'><item 
affiliation='none' role='participant'/>"
+                      + "<status code='110'/></x></presence>");
+            } else if (pattern == MESSAGE && jid != null) {
+                String to = attribute(TO, element);
+                String body = content(BODY, element);
+                if (to != null && body != null) {
+                    received.add(new String[] { to, body });
+                }
+            }
+        }
+
+        private void bind(String fullJid) throws IOException {
+            // a server keeps one session per full JID: the older session is 
closed
+            for (Session other : sessions) {
+                if (other != this && fullJid.equals(other.jid) && 
!other.socket.isClosed()) {
+                    conflicts.incrementAndGet();
+                    other.write("<stream:error><conflict 
xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>"
+                                + "</stream:stream>");
+                    other.socket.close();
+                }
+            }
+            jid = fullJid;
+            binds.incrementAndGet();
+        }
+
+        private String attribute(Pattern pattern, String element) {
+            Matcher m = pattern.matcher(element.substring(0, 
element.indexOf('>') + 1));
+            return m.find() ? m.group(1) : null;
+        }
+
+        private String content(Pattern pattern, String element) {
+            Matcher m = pattern.matcher(element);
+            return m.find() ? m.group(1) : null;
+        }
+    }
+}
diff --git 
a/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppComponentEndpointPerUriTest.java
 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppComponentEndpointPerUriTest.java
new file mode 100644
index 000000000000..2618d9c1b862
--- /dev/null
+++ 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppComponentEndpointPerUriTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.xmpp;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Endpoints whose URIs differ only in the participant (the path) or in the 
user must be different endpoints: the
+ * component must not hand out the endpoint of another participant or user.
+ */
+class XmppComponentEndpointPerUriTest {
+
+    private final CamelContext context = new DefaultCamelContext();
+
+    @AfterEach
+    void stop() {
+        context.stop();
+    }
+
+    @Test
+    void endpointPerParticipant() {
+        XmppEndpoint bob = 
context.getEndpoint("xmpp://camel-user@localhost:123/bob@localhost?password=secret",
+                XmppEndpoint.class);
+        XmppEndpoint carol = 
context.getEndpoint("xmpp://camel-user@localhost:123/carol@localhost?password=secret",
+                XmppEndpoint.class);
+
+        assertEquals("bob@localhost", bob.getParticipant());
+        assertEquals("carol@localhost", carol.getParticipant());
+    }
+
+    @Test
+    void endpointPerUser() {
+        XmppEndpoint alice = 
context.getEndpoint("xmpp://alice@localhost:123/bob@localhost?password=secret",
+                XmppEndpoint.class);
+        XmppEndpoint dave = 
context.getEndpoint("xmpp://dave@localhost:123/bob@localhost?password=secret",
+                XmppEndpoint.class);
+
+        assertEquals("alice", alice.getUser());
+        assertEquals("dave", dave.getUser());
+    }
+
+    @Test
+    void sameUriSameEndpoint() {
+        XmppEndpoint one = 
context.getEndpoint("xmpp://camel-user@localhost:123/bob@localhost?password=secret",
+                XmppEndpoint.class);
+        XmppEndpoint two = 
context.getEndpoint("xmpp://camel-user@localhost:123/bob@localhost?password=secret",
+                XmppEndpoint.class);
+
+        assertSame(one, two);
+    }
+}
diff --git 
a/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppSharedConnectionTest.java
 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppSharedConnectionTest.java
new file mode 100644
index 000000000000..20d157b331b7
--- /dev/null
+++ 
b/components/camel-xmpp/src/test/java/org/apache/camel/component/xmpp/XmppSharedConnectionTest.java
@@ -0,0 +1,289 @@
+/*
+ * 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.xmpp;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.jivesoftware.smack.AbstractXMPPConnection;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Endpoints of one account that differ in the participant share the account's 
connection: a server keeps one session
+ * per account and resource and closes the older one when another logs in. 
Each endpoint still sends to its own
+ * participant.
+ */
+class XmppSharedConnectionTest extends CamelTestSupport {
+
+    private static final String ALICE = "alice@" + FakeXmppServer.DOMAIN;
+    private static final String CAROL = "carol@" + FakeXmppServer.DOMAIN;
+    private static final String ROOM = "dev@conference." + 
FakeXmppServer.DOMAIN;
+
+    private FakeXmppServer server;
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        server = new FakeXmppServer();
+        CamelContext context = super.createCamelContext();
+        // the test server has no TLS
+        context.getRegistry().bind("connectionConfig", 
XMPPTCPConnectionConfiguration.builder()
+                .setHostAddress(InetAddress.getLoopbackAddress())
+                .setPort(server.getPort())
+                .setXmppDomain(FakeXmppServer.DOMAIN)
+                .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)
+                .build());
+        return context;
+    }
+
+    @AfterEach
+    void closeServer() throws Exception {
+        // stop the routes before the server, so the consumers do not try to 
connect to a server that is closed
+        context.stop();
+        if (server != null) {
+            server.close();
+        }
+    }
+
+    @Test
+    void endpointsOfOneAccountShareItsConnection() {
+        template.sendBody("direct:alice", "hello alice");
+        template.sendBody("direct:carol", "hello carol");
+
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            assertEquals(List.of("hello alice"), server.messagesTo(ALICE));
+            assertEquals(List.of("hello carol"), server.messagesTo(CAROL));
+        });
+        assertEquals(0, server.conflicts(), "A session of the account was 
closed by another one");
+        assertEquals(1, server.binds(), "The account logged in more than 
once");
+        assertEquals(1, server.boundSessions());
+    }
+
+    @Test
+    void stoppingOneEndpointKeepsTheConnectionOfTheOther() throws Exception {
+        context.getRouteController().stopRoute("alice");
+        context.removeEndpoint(context.getEndpoint(uri(ALICE)));
+
+        template.sendBody("direct:carol", "hello carol");
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(List.of("hello carol"), 
server.messagesTo(CAROL)));
+        assertEquals(1, server.binds(), "The account logged in more than 
once");
+        assertEquals(1, server.boundSessions());
+
+        // the connection is closed when its last consumer or producer stops
+        context.getRouteController().stopRoute("carol");
+        assertEquals(1, server.boundSessions());
+        context.getRouteController().stopRoute("in");
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> 
assertEquals(0, server.boundSessions()));
+    }
+
+    @Test
+    void consumerRestartedOnASharedConnectionReceivesEachMessageOnce() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:in");
+        mock.expectedBodiesReceived("first", "second");
+
+        template.sendBody("direct:carol", "hello carol");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.boundSessions() == 1);
+        server.sendChat(CAROL, "first");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
mock.getReceivedCounter() == 1);
+
+        // the producer of the account keeps the connection while the consumer 
restarts
+        context.getRouteController().stopRoute("in");
+        context.getRouteController().startRoute("in");
+        server.sendChat(CAROL, "second");
+
+        // the restarted consumer must not receive a message twice
+        mock.setAssertPeriod(500);
+        mock.assertIsSatisfied();
+        assertEquals(1, server.binds(), "The account logged in more than 
once");
+    }
+
+    @Test
+    void producersOfTheAccountReconnectTheSharedConnectionConcurrently() 
throws Exception {
+        template.sendBody("direct:alice", "before alice");
+        template.sendBody("direct:carol", "before carol");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.messagesTo(CAROL).size() == 1);
+
+        // the server closes the session: every producer of the account finds 
the shared connection disconnected
+        server.dropConnections();
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.boundSessions() == 0);
+
+        int sends = 40;
+        ExecutorService pool = Executors.newFixedThreadPool(8);
+        try {
+            CountDownLatch start = new CountDownLatch(1);
+            List<Future<?>> results = new ArrayList<>();
+            for (int i = 0; i < sends; i++) {
+                String route = i % 2 == 0 ? "direct:alice" : "direct:carol";
+                String body = "after " + i;
+                results.add(pool.submit(() -> {
+                    start.await();
+                    template.sendBody(route, body);
+                    return null;
+                }));
+            }
+            start.countDown();
+            for (Future<?> result : results) {
+                // a producer that lost the race to connect() must not fail 
its exchange
+                result.get(20, TimeUnit.SECONDS);
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+
+        // the reconnect also logged the connection in again, so the server 
delivers what the producers send now
+        template.sendBody("direct:alice", "later alice");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.messagesTo(ALICE).contains("later alice"));
+        assertEquals(0, server.conflicts(), "A session of the account was 
closed by another one");
+    }
+
+    @Test
+    void connectionAcquiredDuringAnOutageIsTheConnectionOfTheAccount() throws 
Exception {
+        template.sendBody("direct:alice", "before");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.messagesTo(ALICE).size() == 1);
+
+        XmppEndpoint endpoint = context.getEndpoint(uri(ALICE), 
XmppEndpoint.class);
+        XmppComponent component = endpoint.getComponent();
+        Object first = new Object();
+        Object second = new Object();
+        XMPPTCPConnection connection = component.acquireConnection(endpoint, 
first);
+        try {
+            server.dropConnections();
+            await().atMost(10, TimeUnit.SECONDS).until(() -> 
!connection.isConnected());
+
+            // a consumer or producer of the account starts during the outage 
(a route that starts, the first message
+            // of a toD producer): it gets the connection the others hold, 
connected and logged in again
+            XMPPTCPConnection acquired = component.acquireConnection(endpoint, 
second);
+            assertSame(connection, acquired, "A second connection of the 
account was created");
+            assertTrue(acquired.isAuthenticated(), "The connection was not 
logged in again");
+        } finally {
+            component.releaseConnection(endpoint, second);
+            component.releaseConnection(endpoint, first);
+        }
+
+        // the producer that got the connection before the outage sends on it 
again
+        template.sendBody("direct:alice", "after");
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(List.of("before", "after"), 
server.messagesTo(ALICE)));
+        assertEquals(0, server.conflicts(), "A session of the account was 
closed by another one");
+        assertEquals(1, server.boundSessions());
+    }
+
+    @Test
+    void consumerThatCannotLeaveItsRoomReleasesTheConnection() throws 
Exception {
+        assertReleasedWhenLeavingFails("roomConsumer", roomUri("listener"));
+    }
+
+    @Test
+    void groupChatProducerThatCannotLeaveItsRoomReleasesTheConnection() throws 
Exception {
+        assertReleasedWhenLeavingFails("roomProducer", roomUri("speaker"));
+    }
+
+    private void assertReleasedWhenLeavingFails(String routeId, String uri) 
throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                if ("roomConsumer".equals(routeId)) {
+                    from(uri).routeId(routeId).to("mock:room");
+                } else {
+                    // the group chat producer enters the room when it starts
+                    from("direct:room").routeId(routeId).to(uri);
+                }
+            }
+        });
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
server.occupants(ROOM) == 1);
+        XmppEndpoint endpoint = context.getEndpoint(uri, XmppEndpoint.class);
+        XmppComponent component = endpoint.getComponent();
+        assertEquals(1, component.connectionUsers(endpoint));
+
+        // the connection is lost: leaving the room fails with 
NotConnectedException when the route stops
+        Object probe = new Object();
+        XMPPTCPConnection connection = component.acquireConnection(endpoint, 
probe);
+        component.releaseConnection(endpoint, probe);
+        server.dropConnections();
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
!connection.isConnected());
+
+        context.getRouteController().stopRoute(routeId);
+
+        // the connection is released all the same (it is closed, as nothing 
else of the account uses it)
+        assertEquals(0, component.connectionUsers(endpoint));
+        context.removeRoute(routeId);
+    }
+
+    private String roomUri(String user) {
+        return "xmpp://" + user + "@127.0.0.1:" + server.getPort() + "?room=" 
+ ROOM + "&nickname=" + user
+               + "&password=secret&connectionConfig=#connectionConfig";
+    }
+
+    @Test
+    void reconnectAcceptsAConnectionThatAnotherUserOfTheAccountConnected() 
throws Exception {
+        XmppEndpoint endpoint = context.getEndpoint(uri(ALICE), 
XmppEndpoint.class);
+        XMPPTCPConnectionConfiguration config
+                = 
context.getRegistry().lookupByNameAndType("connectionConfig", 
XMPPTCPConnectionConfiguration.class);
+        XMPPTCPConnection connection = new XMPPTCPConnection(config) {
+            @Override
+            public synchronized AbstractXMPPConnection connect() throws 
SmackException {
+                // what Smack 4.4.8 connect() throws to a caller when another 
thread connected the connection first
+                throw new SmackException.AlreadyConnectedException();
+            }
+
+            @Override
+            public synchronized void login() throws SmackException {
+                // what Smack 4.4.8 login() throws to a caller when another 
thread logged the connection in first
+                throw new SmackException.AlreadyLoggedInException();
+            }
+        };
+        assertDoesNotThrow(() -> endpoint.reconnect(connection));
+    }
+
+    private String uri(String participant) {
+        return "xmpp://[email protected]:" + server.getPort() + "/" + participant
+               + "?password=secret&connectionConfig=#connectionConfig";
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:alice").routeId("alice").to(uri(ALICE));
+                from("direct:carol").routeId("carol").to(uri(CAROL));
+                from(uri(CAROL)).routeId("in").to("mock:in");
+            }
+        };
+    }
+}
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 72132ccb57ed..984cd5ca20dd 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
@@ -1476,6 +1476,20 @@ subscription are not routed. `REPLAY_BLOCKS_OBSERVABLE`, 
`REPLAY_TRANSACTIONS_OB
 `ETH_LOG_OBSERVABLE` with `fromBlock` set to a block number are not subscribed 
again, as that would route the same
 blocks or logs again.
 
+=== camel-xmpp - one endpoint per participant and user, and one connection per 
account
+
+XMPP URIs that differ only in the participant or the user are now separate 
endpoints. Before, the component could
+return an endpoint that was created for another participant or user, and 
messages went to the wrong participant.
+
+All the consumers and producers of one account (the same host, port, service 
name, user, password, resource and login
+settings) now share one XMPP connection, also across endpoints. So an account 
has one session on the server instead
+of one per endpoint, and the server no longer closes one of them with a 
`conflict` error. The connection is closed when
+the last route that uses the account stops. With the `connectionConfig` 
option, endpoints share a connection only when
+they refer to the same `ConnectionConfiguration` instance (for example the 
same bean in the registry).
+
+`XmppEndpoint.createConnection()` now creates a new connection on every call, 
instead of returning the connection of
+the endpoint, and an endpoint no longer disconnects a connection when it stops.
+
 === camel-vertx-http - a header with several values is sent as several headers
 
 A message header whose value is a `java.util.Collection` or an object array 
(as the HTTP consumers map a request

Reply via email to