001/**
002 *
003 * Copyright 2018-2022 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.c2s;
018
019import java.io.IOException;
020import java.security.cert.CertificateException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.ListIterator;
028import java.util.Map;
029import java.util.concurrent.CopyOnWriteArrayList;
030import java.util.concurrent.TimeUnit;
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034import javax.net.ssl.SSLSession;
035
036import org.jivesoftware.smack.AbstractXMPPConnection;
037import org.jivesoftware.smack.SmackException;
038import org.jivesoftware.smack.SmackException.NoResponseException;
039import org.jivesoftware.smack.SmackException.NotConnectedException;
040import org.jivesoftware.smack.SmackException.OutgoingQueueFullException;
041import org.jivesoftware.smack.SmackFuture;
042import org.jivesoftware.smack.XMPPException;
043import org.jivesoftware.smack.XMPPException.FailedNonzaException;
044import org.jivesoftware.smack.XMPPException.StreamErrorException;
045import org.jivesoftware.smack.XMPPException.XMPPErrorException;
046import org.jivesoftware.smack.XmppInputOutputFilter;
047import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsFailed;
048import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsResult;
049import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsSuccess;
050import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
051import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext;
052import org.jivesoftware.smack.fsm.ConnectionStateEvent;
053import org.jivesoftware.smack.fsm.ConnectionStateMachineListener;
054import org.jivesoftware.smack.fsm.LoginContext;
055import org.jivesoftware.smack.fsm.NoOpState;
056import org.jivesoftware.smack.fsm.State;
057import org.jivesoftware.smack.fsm.StateDescriptor;
058import org.jivesoftware.smack.fsm.StateDescriptorGraph;
059import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex;
060import org.jivesoftware.smack.fsm.StateMachineException;
061import org.jivesoftware.smack.fsm.StateTransitionResult;
062import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult;
063import org.jivesoftware.smack.internal.AbstractStats;
064import org.jivesoftware.smack.internal.SmackTlsContext;
065import org.jivesoftware.smack.packet.AbstractStreamClose;
066import org.jivesoftware.smack.packet.AbstractStreamOpen;
067import org.jivesoftware.smack.packet.IQ;
068import org.jivesoftware.smack.packet.Message;
069import org.jivesoftware.smack.packet.Nonza;
070import org.jivesoftware.smack.packet.Presence;
071import org.jivesoftware.smack.packet.StreamError;
072import org.jivesoftware.smack.packet.TopLevelStreamElement;
073import org.jivesoftware.smack.packet.XmlEnvironment;
074import org.jivesoftware.smack.parsing.SmackParsingException;
075import org.jivesoftware.smack.sasl.SASLErrorException;
076import org.jivesoftware.smack.sasl.SASLMechanism;
077import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown;
078import org.jivesoftware.smack.util.ExtendedAppendable;
079import org.jivesoftware.smack.util.PacketParserUtils;
080import org.jivesoftware.smack.util.StringUtils;
081import org.jivesoftware.smack.util.Supplier;
082import org.jivesoftware.smack.xml.XmlPullParser;
083import org.jivesoftware.smack.xml.XmlPullParserException;
084
085import org.jxmpp.jid.DomainBareJid;
086import org.jxmpp.jid.parts.Resourcepart;
087import org.jxmpp.util.XmppStringUtils;
088
089/**
090 * The superclass of Smack's Modular Connection Architecture.
091 * <p>
092 * <b>Note:</b> Everything related to the modular connection architecture is currently considered experimental and
093 * should not be used in production. Use the mature {@code XMPPTCPConnection} if you do not feel adventurous.
094 * </p>
095 * <p>
096 * Smack's modular connection architecture allows to extend a XMPP c2s (client-to-server) connection with additional
097 * functionality by adding modules. Those modules extend the Finite State Machine (FSM) within the connection with new
098 * states. Connection modules can either be
099 * <ul>
100 * <li>Transports</li>
101 * <li>Extensions</li>
102 * </ul>
103 * <p>
104 * Transports bind the XMPP XML stream to an underlying transport like TCP, WebSockets, BOSH, and allow for the
105 * different particularities of transports like DirectTLS
106 * (<a href="https://xmpp.org/extensions/xep-0368.html">XEP-0368</a>). This eventually means that a single transport
107 * module can implement multiple transport mechanisms. For example the TCP transport module implements the RFC6120 TCP
108 * and the XEP-0368 direct TLS TCP transport bindings.
109 * </p>
110 * <p>
111 * Extensions allow for a richer functionality of the connection. Those include
112 * <ul>
113 * <li>Compression</li>
114 *   <li><ul>
115 *   <li>zlib ([XEP-0138](https://xmpp.org/extensions/xep-0138.html))</li>
116 *   <li>[Efficient XML Interchange (EXI)](https://www.w3.org/TR/exi/)</li>
117 *   </ul></li>
118 * <li>Instant Stream Resumption ([XEP-0397](https://xmpp.org/extensions/xep-0397.html)</li>
119 * <li>Bind2</li>
120 * <li>Stream Management</li>
121 * </ul>
122 * Note that not all extensions work with every transport. For example compression only works with TCP-based transport
123 * bindings.
124 * <p>
125 * Connection modules are plugged into the the modular connection via their constructor. and they usually declare
126 * backwards edges to some common, generic connection state of the FSM.
127 * </p>
128 * <p>
129 * Modules and states always have an accompanying *descriptor* type. `ModuleDescriptor` and `StateDescriptor` exist
130 * without an connection instance. They describe the module and state metadata, while their modules and states are
131 * Instantiated once a modular connection is instantiated.
132 * </p>
133 */
134public final class ModularXmppClientToServerConnection extends AbstractXMPPConnection {
135
136    private static final Logger LOGGER = Logger.getLogger(
137                    ModularXmppClientToServerConnectionConfiguration.class.getName());
138
139    private final ArrayBlockingQueueWithShutdown<TopLevelStreamElement> outgoingElementsQueue = new ArrayBlockingQueueWithShutdown<>(
140                    100, true);
141
142    private XmppClientToServerTransport activeTransport;
143
144    private final List<ConnectionStateMachineListener> connectionStateMachineListeners = new CopyOnWriteArrayList<>();
145
146    private boolean featuresReceived;
147
148    private boolean streamResumed;
149
150    private GraphVertex<State> currentStateVertex;
151
152    private List<State> walkFromDisconnectToAuthenticated;
153
154    private final ModularXmppClientToServerConnectionConfiguration configuration;
155
156    private final ModularXmppClientToServerConnectionInternal connectionInternal;
157
158    private final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor>> connectionModules = new HashMap<>();
159
160    private final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport> transports = new HashMap<>();
161    /**
162     * This is one of those cases where the field is modified by one thread and read by another. We currently use
163     * CopyOnWriteArrayList but should potentially use a VarHandle once Smack supports them.
164     */
165    private final List<XmppInputOutputFilter> inputOutputFilters = new CopyOnWriteArrayList<>();
166
167    private List<XmppInputOutputFilter> previousInputOutputFilters;
168
169    public ModularXmppClientToServerConnection(ModularXmppClientToServerConnectionConfiguration configuration) {
170        super(configuration);
171
172        this.configuration = configuration;
173
174        // Construct the internal connection API.
175        connectionInternal = new ModularXmppClientToServerConnectionInternal(this, getReactor(), debugger,
176                        outgoingElementsQueue) {
177
178            @Override
179            public void parseAndProcessElement(String wrappedCompleteElement) {
180                ModularXmppClientToServerConnection.this.parseAndProcessElement(wrappedCompleteElement);
181            }
182
183            @Override
184            public void notifyConnectionError(Exception e) {
185                ModularXmppClientToServerConnection.this.notifyConnectionError(e);
186            }
187
188            @Override
189            public String onStreamOpen(XmlPullParser parser) {
190                return ModularXmppClientToServerConnection.this.onStreamOpen(parser);
191            }
192
193            @Override
194            public void onStreamClosed() {
195                ModularXmppClientToServerConnection.this.closingStreamReceived = true;
196                notifyWaitingThreads();
197            }
198
199            @Override
200            public void fireFirstLevelElementSendListeners(TopLevelStreamElement element) {
201                ModularXmppClientToServerConnection.this.firePacketSendingListeners(element);
202            }
203
204            @Override
205            public void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) {
206                ModularXmppClientToServerConnection.this.invokeConnectionStateMachineListener(connectionStateEvent);
207            }
208
209            @Override
210            public XmlEnvironment getOutgoingStreamXmlEnvironment() {
211                return outgoingStreamXmlEnvironment;
212            }
213
214            @Override
215            public void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) {
216                inputOutputFilters.add(0, xmppInputOutputFilter);
217            }
218
219            @Override
220            public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterBeginIterator() {
221                return inputOutputFilters.listIterator();
222            }
223
224            @Override
225            public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterEndIterator() {
226                return inputOutputFilters.listIterator(inputOutputFilters.size());
227            }
228
229            @Override
230            public void waitForFeaturesReceived(String waitFor)
231                            throws InterruptedException, SmackException, XMPPException {
232                ModularXmppClientToServerConnection.this.waitForFeaturesReceived(waitFor);
233            }
234
235            @Override
236            public void newStreamOpenWaitForFeaturesSequence(String waitFor)
237                            throws InterruptedException, SmackException, XMPPException {
238                ModularXmppClientToServerConnection.this.newStreamOpenWaitForFeaturesSequence(waitFor);
239            }
240
241            @Override
242            public SmackTlsContext getSmackTlsContext() {
243                return ModularXmppClientToServerConnection.this.getSmackTlsContext();
244            }
245
246            @Override
247            public <SN extends Nonza, FN extends Nonza> SN sendAndWaitForResponse(Nonza nonza,
248                            Class<SN> successNonzaClass, Class<FN> failedNonzaClass) throws NoResponseException,
249                            NotConnectedException, FailedNonzaException, InterruptedException {
250                return ModularXmppClientToServerConnection.this.sendAndWaitForResponse(nonza, successNonzaClass,
251                                failedNonzaClass);
252            }
253
254            @Override
255            public void asyncGo(Runnable runnable) {
256                AbstractXMPPConnection.asyncGo(runnable);
257            }
258
259            @Override
260            public void waitForConditionOrThrowConnectionException(Supplier<Boolean> condition, String waitFor)
261                            throws InterruptedException, SmackException, XMPPException {
262                ModularXmppClientToServerConnection.this.waitForConditionOrThrowConnectionException(condition, waitFor);
263            }
264
265            @Override
266            public void notifyWaitingThreads() {
267                ModularXmppClientToServerConnection.this.notifyWaitingThreads();
268            }
269
270            @Override
271            public void setCompressionEnabled(boolean compressionEnabled) {
272                ModularXmppClientToServerConnection.this.compressionEnabled = compressionEnabled;
273            }
274
275            @Override
276            public void setTransport(XmppClientToServerTransport xmppTransport) {
277                ModularXmppClientToServerConnection.this.activeTransport = xmppTransport;
278                ModularXmppClientToServerConnection.this.connected = true;
279            }
280
281        };
282
283        // Construct the modules from the module descriptor. We do this before constructing the state graph, as the
284        // modules are sometimes used to construct the states.
285        for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : configuration.moduleDescriptors) {
286            Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> moduleDescriptorClass = moduleDescriptor.getClass();
287            ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor> connectionModule = moduleDescriptor.constructXmppConnectionModule(
288                            connectionInternal);
289            connectionModules.put(moduleDescriptorClass, connectionModule);
290
291            XmppClientToServerTransport transport = connectionModule.getTransport();
292            // Not every module may provide a transport.
293            if (transport != null) {
294                transports.put(moduleDescriptorClass, transport);
295            }
296        }
297
298        GraphVertex<StateDescriptor> initialStateDescriptorVertex = configuration.initialStateDescriptorVertex;
299        // Convert the graph of state descriptors to a graph of states, bound to this very connection.
300        currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, connectionInternal);
301    }
302
303    @SuppressWarnings("unchecked")
304    public <CM extends ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor>> CM getConnectionModuleFor(
305                    Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> descriptorClass) {
306        return (CM) connectionModules.get(descriptorClass);
307    }
308
309    @Override
310    protected void loginInternal(String username, String password, Resourcepart resource)
311                    throws XMPPException, SmackException, IOException, InterruptedException {
312        WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(
313                        AuthenticatedAndResourceBoundStateDescriptor.class).withLoginContext(username, password,
314                                        resource).build();
315        walkStateGraph(walkStateGraphContext);
316    }
317
318    private WalkStateGraphContext.Builder buildNewWalkTo(Class<? extends StateDescriptor> finalStateClass) {
319        return WalkStateGraphContext.builder(currentStateVertex.getElement().getStateDescriptor().getClass(),
320                        finalStateClass);
321    }
322
323    /**
324     * Unwind the state. This will revert the effects of the state by calling {@link State#resetState()} prior issuing a
325     * connection state event of {@link ConnectionStateEvent#StateRevertBackwardsWalk}.
326     *
327     * @param revertedState the state which is going to get reverted.
328     */
329    private void unwindState(State revertedState) {
330        invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState));
331        revertedState.resetState();
332    }
333
334    private void walkStateGraph(WalkStateGraphContext walkStateGraphContext)
335                    throws XMPPException, IOException, SmackException, InterruptedException {
336        // Save a copy of the current state
337        GraphVertex<State> previousStateVertex = currentStateVertex;
338        try {
339            walkStateGraphInternal(walkStateGraphContext);
340        } catch (IOException | SmackException | InterruptedException | XMPPException e) {
341            currentStateVertex = previousStateVertex;
342            // Unwind the state.
343            State revertedState = currentStateVertex.getElement();
344            unwindState(revertedState);
345            throw e;
346        }
347    }
348
349    private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext)
350                    throws IOException, SmackException, InterruptedException, XMPPException {
351        // Save a copy of the current state
352        final GraphVertex<State> initialStateVertex = currentStateVertex;
353        final State initialState = initialStateVertex.getElement();
354        final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor();
355
356        walkStateGraphContext.recordWalkTo(initialState);
357
358        // Check if this is the walk's final state.
359        if (walkStateGraphContext.isWalksFinalState(initialStateDescriptor)) {
360            // If this is used as final state, then it should be marked as such.
361            assert initialStateDescriptor.isFinalState();
362
363            // We reached the final state.
364            invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState));
365            return;
366        }
367
368        List<GraphVertex<State>> outgoingStateEdges = initialStateVertex.getOutgoingEdges();
369
370        // See if we need to handle mandatory intermediate states.
371        GraphVertex<State> mandatoryIntermediateStateVertex = walkStateGraphContext.maybeReturnMandatoryImmediateState(
372                        outgoingStateEdges);
373        if (mandatoryIntermediateStateVertex != null) {
374            StateTransitionResult reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext);
375
376            if (reason instanceof StateTransitionResult.Success) {
377                walkStateGraph(walkStateGraphContext);
378                return;
379            }
380
381            // We could not enter a mandatory intermediate state. Throw here.
382            throw new StateMachineException.SmackMandatoryStateFailedException(
383                            mandatoryIntermediateStateVertex.getElement(), reason);
384        }
385
386        for (Iterator<GraphVertex<State>> it = outgoingStateEdges.iterator(); it.hasNext();) {
387            GraphVertex<State> successorStateVertex = it.next();
388            State successorState = successorStateVertex.getElement();
389
390            // Ignore successorStateVertex if the only way to the final state is via the initial state. This happens
391            // typically if we are in the ConnectedButUnauthenticated state on the way to ResourceboundAndAuthenticated,
392            // where we do not want to walk via InstantShutdown/Shtudown in a cycle over the initial state towards this
393            // state.
394            if (walkStateGraphContext.wouldCauseCycle(successorStateVertex)) {
395                // Ignore this successor.
396                invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionIgnoredDueCycle(
397                                initialStateVertex, successorStateVertex));
398            } else {
399                StateTransitionResult result = attemptEnterState(successorStateVertex, walkStateGraphContext);
400
401                if (result instanceof StateTransitionResult.Success) {
402                    break;
403                }
404
405                // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then
406                // we
407                // just record this value and go on from there. Note that reason may be null, which is returned by
408                // attemptEnterState in case the state was already successfully handled. If this is the case, then we
409                // don't
410                // record it.
411                if (result != null) {
412                    walkStateGraphContext.recordFailedState(successorState, result);
413                }
414            }
415
416            if (!it.hasNext()) {
417                throw StateMachineException.SmackStateGraphDeadEndException.from(walkStateGraphContext,
418                                initialStateVertex);
419            }
420        }
421
422        // Walk the state graph by recursion.
423        walkStateGraph(walkStateGraphContext);
424    }
425
426    /**
427     * Attempt to enter a state. Note that this method may return <code>null</code> if this state can be safely ignored.
428     *
429     * @param successorStateVertex the successor state vertex.
430     * @param walkStateGraphContext the "walk state graph" context.
431     * @return A state transition result or <code>null</code> if this state can be ignored.
432     * @throws SmackException if Smack detected an exceptional situation.
433     * @throws XMPPException if an XMPP protocol error was received.
434     * @throws IOException if an I/O error occurred.
435     * @throws InterruptedException if the calling thread was interrupted.
436     */
437    private StateTransitionResult attemptEnterState(GraphVertex<State> successorStateVertex,
438                    WalkStateGraphContext walkStateGraphContext)
439                    throws SmackException, XMPPException, IOException, InterruptedException {
440        final GraphVertex<State> initialStateVertex = currentStateVertex;
441        final State initialState = initialStateVertex.getElement();
442        final State successorState = successorStateVertex.getElement();
443        final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor();
444
445        if (!successorStateDescriptor.isMultiVisitState()
446                        && walkStateGraphContext.stateAlreadyVisited(successorState)) {
447            // This can happen if a state leads back to the state where it originated from. See for example the
448            // 'Compression' state. We return 'null' here to signal that the state can safely be ignored.
449            return null;
450        }
451
452        if (successorStateDescriptor.isNotImplemented()) {
453            StateTransitionResult.TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new StateTransitionResult.TransitionImpossibleBecauseNotImplemented(
454                            successorStateDescriptor);
455            invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState,
456                            successorState, transtionImpossibleBecauseNotImplemented));
457            return transtionImpossibleBecauseNotImplemented;
458        }
459
460        final StateTransitionResult.AttemptResult transitionAttemptResult;
461        try {
462            StateTransitionResult.TransitionImpossible transitionImpossible = successorState.isTransitionToPossible(
463                            walkStateGraphContext);
464            if (transitionImpossible != null) {
465                invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState,
466                                successorState, transitionImpossible));
467                return transitionImpossible;
468            }
469
470            invokeConnectionStateMachineListener(
471                            new ConnectionStateEvent.AboutToTransitionInto(initialState, successorState));
472            transitionAttemptResult = successorState.transitionInto(walkStateGraphContext);
473        } catch (SmackException | IOException | InterruptedException | XMPPException e) {
474            // Unwind the state here too, since this state will not be unwound by walkStateGraph(), as it will not
475            // become a predecessor state in the walk.
476            unwindState(successorState);
477            throw e;
478        }
479        if (transitionAttemptResult instanceof StateTransitionResult.Failure) {
480            StateTransitionResult.Failure transitionFailureResult = (StateTransitionResult.Failure) transitionAttemptResult;
481            invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionFailed(initialState, successorState,
482                            transitionFailureResult));
483            return transitionAttemptResult;
484        }
485
486        // If transitionAttemptResult is not an instance of TransitionFailureResult, then it has to be of type
487        // TransitionSuccessResult.
488        StateTransitionResult.Success transitionSuccessResult = (StateTransitionResult.Success) transitionAttemptResult;
489
490        currentStateVertex = successorStateVertex;
491        invokeConnectionStateMachineListener(
492                        new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, transitionSuccessResult));
493
494        return transitionSuccessResult;
495    }
496
497    @Override
498    protected void sendInternal(TopLevelStreamElement element) throws NotConnectedException, InterruptedException {
499        final XmppClientToServerTransport transport = activeTransport;
500        if (transport == null) {
501            throw new NotConnectedException();
502        }
503
504        outgoingElementsQueue.put(element);
505        transport.notifyAboutNewOutgoingElements();
506    }
507
508    @Override
509    protected void sendNonBlockingInternal(TopLevelStreamElement element) throws NotConnectedException, OutgoingQueueFullException {
510        final XmppClientToServerTransport transport = activeTransport;
511        if (transport == null) {
512            throw new NotConnectedException();
513        }
514
515        boolean enqueued = outgoingElementsQueue.offer(element);
516        if (!enqueued) {
517            throw new OutgoingQueueFullException();
518        }
519
520        transport.notifyAboutNewOutgoingElements();
521    }
522
523    @Override
524    protected void shutdown() {
525        shutdown(false);
526    }
527
528    @Override
529    public synchronized void instantShutdown() {
530        shutdown(true);
531    }
532
533    @Override
534    public ModularXmppClientToServerConnectionConfiguration getConfiguration() {
535        return configuration;
536    }
537
538    private void shutdown(boolean instant) {
539        Class<? extends StateDescriptor> mandatoryIntermediateState;
540        if (instant) {
541            mandatoryIntermediateState = InstantShutdownStateDescriptor.class;
542        } else {
543            mandatoryIntermediateState = ShutdownStateDescriptor.class;
544        }
545
546        WalkStateGraphContext context = buildNewWalkTo(
547                        DisconnectedStateDescriptor.class).withMandatoryIntermediateState(
548                                        mandatoryIntermediateState).build();
549
550        try {
551            walkStateGraph(context);
552        } catch (IOException | SmackException | InterruptedException | XMPPException e) {
553            throw new IllegalStateException("A walk to disconnected state should never throw", e);
554        }
555    }
556
557    private SSLSession getSSLSession() {
558        final XmppClientToServerTransport transport = activeTransport;
559        if (transport == null) {
560            return null;
561        }
562        return transport.getSslSession();
563    }
564
565    @Override
566    protected void afterFeaturesReceived() {
567        featuresReceived = true;
568        notifyWaitingThreads();
569    }
570
571    private void parseAndProcessElement(String element) {
572        try {
573            XmlPullParser parser = PacketParserUtils.getParserFor(element);
574
575            // Skip the enclosing stream open what is guaranteed to be there.
576            parser.next();
577
578            XmlPullParser.Event event = parser.getEventType();
579            outerloop: while (true) {
580                switch (event) {
581                case START_ELEMENT:
582                    final String name = parser.getName();
583                    // Note that we don't handle "stream" here as it's done in the splitter.
584                    switch (name) {
585                    case Message.ELEMENT:
586                    case IQ.IQ_ELEMENT:
587                    case Presence.ELEMENT:
588                        try {
589                            parseAndProcessStanza(parser);
590                        } finally {
591                            // TODO: Here would be the following stream management code.
592                            // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount);
593                        }
594                        break;
595                    case "error":
596                        StreamError streamError = PacketParserUtils.parseStreamError(parser, null);
597                        StreamErrorException streamErrorException = new StreamErrorException(streamError);
598                        currentXmppException = streamErrorException;
599                        notifyWaitingThreads();
600                        throw streamErrorException;
601                    case "features":
602                        parseFeatures(parser);
603                        afterFeaturesReceived();
604                        break;
605                    default:
606                        parseAndProcessNonza(parser);
607                        break;
608                    }
609                    break;
610                case END_DOCUMENT:
611                    break outerloop;
612                default: // fall out
613                }
614                event = parser.next();
615            }
616        } catch (XmlPullParserException | IOException | InterruptedException | StreamErrorException
617                        | SmackParsingException e) {
618            notifyConnectionError(e);
619        }
620    }
621
622    private synchronized void prepareToWaitForFeaturesReceived() {
623        featuresReceived = false;
624    }
625
626    private void waitForFeaturesReceived(String waitFor) throws InterruptedException, SmackException, XMPPException {
627        waitForConditionOrThrowConnectionException(() -> featuresReceived, waitFor);
628    }
629
630    @Override
631    protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
632        StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory();
633        return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang);
634    }
635
636    private void newStreamOpenWaitForFeaturesSequence(String waitFor)
637                    throws InterruptedException, SmackException, XMPPException {
638        prepareToWaitForFeaturesReceived();
639
640        // Create StreamOpen from StreamOpenAndCloseFactory via underlying transport.
641        StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory();
642        CharSequence from = null;
643        CharSequence localpart = connectionInternal.connection.getConfiguration().getUsername();
644        DomainBareJid xmppServiceDomain = getXMPPServiceDomain();
645        if (localpart != null) {
646            from = XmppStringUtils.completeJidFrom(localpart, xmppServiceDomain);
647        }
648        AbstractStreamOpen streamOpen = streamOpenAndCloseFactory.createStreamOpen(xmppServiceDomain, from,
649                        getStreamId(), getConfiguration().getXmlLang());
650        sendStreamOpen(streamOpen);
651
652        waitForFeaturesReceived(waitFor);
653    }
654
655    private void sendStreamOpen(AbstractStreamOpen streamOpen) throws NotConnectedException, InterruptedException {
656        sendNonza(streamOpen);
657        updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen);
658    }
659
660    public static class DisconnectedStateDescriptor extends StateDescriptor {
661        protected DisconnectedStateDescriptor() {
662            super(DisconnectedState.class, StateDescriptor.Property.finalState);
663            addSuccessor(LookupRemoteConnectionEndpointsStateDescriptor.class);
664        }
665    }
666
667    private final class DisconnectedState extends State {
668
669        private DisconnectedState(StateDescriptor stateDescriptor,
670                        ModularXmppClientToServerConnectionInternal connectionInternal) {
671            super(stateDescriptor, connectionInternal);
672        }
673
674        @Override
675        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) {
676            synchronized (ModularXmppClientToServerConnection.this) {
677                if (inputOutputFilters.isEmpty()) {
678                    previousInputOutputFilters = null;
679                } else {
680                    previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size());
681                    previousInputOutputFilters.addAll(inputOutputFilters);
682                    inputOutputFilters.clear();
683                }
684            }
685
686            // Reset all states we have visited when transitioning from disconnected to authenticated. This assumes that
687            // every state after authenticated does not need to be reset.
688            ListIterator<State> it = walkFromDisconnectToAuthenticated.listIterator(
689                            walkFromDisconnectToAuthenticated.size());
690            while (it.hasPrevious()) {
691                State stateToReset = it.previous();
692                stateToReset.resetState();
693            }
694            walkFromDisconnectToAuthenticated = null;
695
696            return StateTransitionResult.Success.EMPTY_INSTANCE;
697        }
698    }
699
700    public static final class LookupRemoteConnectionEndpointsStateDescriptor extends StateDescriptor {
701        private LookupRemoteConnectionEndpointsStateDescriptor() {
702            super(LookupRemoteConnectionEndpointsState.class);
703        }
704    }
705
706    private final class LookupRemoteConnectionEndpointsState extends State {
707        boolean outgoingElementsQueueWasShutdown;
708
709        private LookupRemoteConnectionEndpointsState(StateDescriptor stateDescriptor,
710                        ModularXmppClientToServerConnectionInternal connectionInternal) {
711            super(stateDescriptor, connectionInternal);
712        }
713
714        @Override
715        public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException,
716                        SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException {
717            // There is a challenge here: We are going to trigger the discovery of endpoints which will run
718            // asynchronously. After a timeout, all discovered endpoints are collected. To prevent stale results from
719            // previous discover runs, the results are communicated via SmackFuture, so that we always handle the most
720            // up-to-date results.
721
722            Map<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> lookupFutures = new HashMap<>(
723                            transports.size());
724
725            final int numberOfFutures;
726            {
727                List<SmackFuture<?, ?>> allFutures = new ArrayList<>();
728                for (XmppClientToServerTransport transport : transports.values()) {
729                    // First we clear the transport of any potentially previously discovered connection endpoints.
730                    transport.resetDiscoveredConnectionEndpoints();
731
732                    // Ask the transport to start the discovery of remote connection endpoints asynchronously.
733                    List<SmackFuture<LookupConnectionEndpointsResult, Exception>> transportFutures = transport.lookupConnectionEndpoints();
734
735                    lookupFutures.put(transport, transportFutures);
736                    allFutures.addAll(transportFutures);
737                }
738
739                numberOfFutures = allFutures.size();
740
741                // Wait until all features are ready or if the timeout occurs. Note that we do not inspect and react the
742                // return value of SmackFuture.await() as we want to collect the LookupConnectionEndpointsFailed later.
743                SmackFuture.await(allFutures, getReplyTimeout(), TimeUnit.MILLISECONDS);
744            }
745
746            // Note that we do not pass the lookupFailures in case there is at least one successful transport. The
747            // lookup failures are also recording in LookupConnectionEndpointsSuccess, e.g. as part of
748            // RemoteXmppTcpConnectionEndpoints.Result.
749            List<LookupConnectionEndpointsFailed> lookupFailures = new ArrayList<>(numberOfFutures);
750
751            boolean atLeastOneConnectionEndpointDiscovered = false;
752            for (Map.Entry<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> entry : lookupFutures.entrySet()) {
753                XmppClientToServerTransport transport = entry.getKey();
754
755                for (SmackFuture<LookupConnectionEndpointsResult, Exception> future : entry.getValue()) {
756                    LookupConnectionEndpointsResult result = future.getIfAvailable();
757
758                    if (result == null) {
759                        continue;
760                    }
761
762                    if (result instanceof LookupConnectionEndpointsFailed) {
763                        LookupConnectionEndpointsFailed lookupFailure = (LookupConnectionEndpointsFailed) result;
764                        lookupFailures.add(lookupFailure);
765                        continue;
766                    }
767
768                    LookupConnectionEndpointsSuccess successResult = (LookupConnectionEndpointsSuccess) result;
769
770                    // Arm the transport with the success result, so that its information can be used by the transport
771                    // to establish the connection.
772                    transport.loadConnectionEndpoints(successResult);
773
774                    // Mark that the connection attempt can continue.
775                    atLeastOneConnectionEndpointDiscovered = true;
776                }
777            }
778
779            if (!atLeastOneConnectionEndpointDiscovered) {
780                throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures);
781            }
782
783            if (!lookupFailures.isEmpty()) {
784                // TODO: Put those non-fatal lookup failures into a sink of the connection so that the user is able to
785                // be aware of them.
786            }
787
788            // Even though the outgoing elements queue is unrelated to the lookup remote connection endpoints state, we
789            // do start the queue at this point. The transports will need it available, and we use the state's reset()
790            // function to close the queue again on failure.
791            outgoingElementsQueueWasShutdown = outgoingElementsQueue.start();
792
793            return StateTransitionResult.Success.EMPTY_INSTANCE;
794        }
795
796        @Override
797        public void resetState() {
798            for (XmppClientToServerTransport transport : transports.values()) {
799                transport.resetDiscoveredConnectionEndpoints();
800            }
801
802            if (outgoingElementsQueueWasShutdown) {
803                // Reset the outgoing elements queue in this state, since we also start it in this state.
804                outgoingElementsQueue.shutdown();
805            }
806        }
807    }
808
809    public static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor {
810        private ConnectedButUnauthenticatedStateDescriptor() {
811            super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState);
812            addSuccessor(SaslAuthenticationStateDescriptor.class);
813            addSuccessor(InstantShutdownStateDescriptor.class);
814            addSuccessor(ShutdownStateDescriptor.class);
815        }
816    }
817
818    private final class ConnectedButUnauthenticatedState extends State {
819        private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor,
820                        ModularXmppClientToServerConnectionInternal connectionInternal) {
821            super(stateDescriptor, connectionInternal);
822        }
823
824        @Override
825        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) {
826            assert walkFromDisconnectToAuthenticated == null;
827
828            if (walkStateGraphContext.isWalksFinalState(getStateDescriptor())) {
829                // If this is the final state, then record the walk so far.
830                walkFromDisconnectToAuthenticated = walkStateGraphContext.getWalk();
831            }
832
833            connected = true;
834            return StateTransitionResult.Success.EMPTY_INSTANCE;
835        }
836
837        @Override
838        public void resetState() {
839            connected = false;
840        }
841    }
842
843    public static final class SaslAuthenticationStateDescriptor extends StateDescriptor {
844        private SaslAuthenticationStateDescriptor() {
845            super(SaslAuthenticationState.class, "RFC 6120 § 6");
846            addSuccessor(AuthenticatedButUnboundStateDescriptor.class);
847        }
848    }
849
850    private final class SaslAuthenticationState extends State {
851        private SaslAuthenticationState(StateDescriptor stateDescriptor,
852                        ModularXmppClientToServerConnectionInternal connectionInternal) {
853            super(stateDescriptor, connectionInternal);
854        }
855
856        @Override
857        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
858                        throws IOException, SmackException, InterruptedException, XMPPException {
859            prepareToWaitForFeaturesReceived();
860
861            LoginContext loginContext = walkStateGraphContext.getLoginContext();
862            SASLMechanism usedSaslMechanism = authenticate(loginContext.username, loginContext.password,
863                            config.getAuthzid(), getSSLSession());
864            // authenticate() will only return if the SASL authentication was successful, but we also need to wait for
865            // the next round of stream features.
866
867            waitForFeaturesReceived("server stream features after SASL authentication");
868
869            return new SaslAuthenticationSuccessResult(usedSaslMechanism);
870        }
871    }
872
873    public static final class SaslAuthenticationSuccessResult extends StateTransitionResult.Success {
874        private final String saslMechanismName;
875
876        private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) {
877            super("SASL authentication successfull using " + usedSaslMechanism.getName());
878            this.saslMechanismName = usedSaslMechanism.getName();
879        }
880
881        public String getSaslMechanismName() {
882            return saslMechanismName;
883        }
884    }
885
886    public static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor {
887        private AuthenticatedButUnboundStateDescriptor() {
888            super(StateDescriptor.Property.multiVisitState);
889            addSuccessor(ResourceBindingStateDescriptor.class);
890        }
891    }
892
893    public static final class ResourceBindingStateDescriptor extends StateDescriptor {
894        private ResourceBindingStateDescriptor() {
895            super(ResourceBindingState.class, "RFC 6120 § 7");
896            addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class);
897        }
898    }
899
900    private final class ResourceBindingState extends State {
901        private ResourceBindingState(StateDescriptor stateDescriptor,
902                        ModularXmppClientToServerConnectionInternal connectionInternal) {
903            super(stateDescriptor, connectionInternal);
904        }
905
906        @Override
907        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
908                        throws IOException, SmackException, InterruptedException, XMPPException {
909            // Calling bindResourceAndEstablishSession() below requires the lastFeaturesReceived sync point to be
910            // signaled.
911            // Since we entered this state, the FSM has decided that the last features have been received, hence signal
912            // the sync point.
913            lastFeaturesReceived = true;
914            notifyWaitingThreads();
915
916            LoginContext loginContext = walkStateGraphContext.getLoginContext();
917            Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource);
918
919            // TODO: This should be a field in the Stream Management (SM) module. Here should be hook which the SM
920            // module can use to set the field instead.
921            streamResumed = false;
922
923            return new ResourceBoundResult(resource, loginContext.resource);
924        }
925    }
926
927    public static final class ResourceBoundResult extends StateTransitionResult.Success {
928        private final Resourcepart resource;
929
930        private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) {
931            super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "')");
932            this.resource = boundResource;
933        }
934
935        public Resourcepart getResource() {
936            return resource;
937        }
938    }
939
940    private boolean compressionEnabled;
941
942    @Override
943    public boolean isUsingCompression() {
944        return compressionEnabled;
945    }
946
947    public static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor {
948        private AuthenticatedAndResourceBoundStateDescriptor() {
949            super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState);
950            addSuccessor(InstantShutdownStateDescriptor.class);
951            addSuccessor(ShutdownStateDescriptor.class);
952        }
953    }
954
955    private final class AuthenticatedAndResourceBoundState extends State {
956        private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor,
957                        ModularXmppClientToServerConnectionInternal connectionInternal) {
958            super(stateDescriptor, connectionInternal);
959        }
960
961        @Override
962        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
963                        throws NotConnectedException, InterruptedException {
964            if (walkFromDisconnectToAuthenticated != null) {
965                // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current
966                // walk must not start from the 'Disconnected' state.
967                assert walkStateGraphContext.getWalk().get(
968                                0).getStateDescriptor().getClass() != DisconnectedStateDescriptor.class;
969                // Append the current walk to the previous one.
970                walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated);
971            } else {
972                walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.getWalkLength() + 1);
973                walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated);
974            }
975            walkFromDisconnectToAuthenticated.add(this);
976
977            afterSuccessfulLogin(streamResumed);
978
979            return StateTransitionResult.Success.EMPTY_INSTANCE;
980        }
981
982        @Override
983        public void resetState() {
984            authenticated = false;
985        }
986    }
987
988    static final class ShutdownStateDescriptor extends StateDescriptor {
989        private ShutdownStateDescriptor() {
990            super(ShutdownState.class);
991            addSuccessor(CloseConnectionStateDescriptor.class);
992        }
993    }
994
995    private final class ShutdownState extends State {
996        private ShutdownState(StateDescriptor stateDescriptor,
997                        ModularXmppClientToServerConnectionInternal connectionInternal) {
998            super(stateDescriptor, connectionInternal);
999        }
1000
1001        @Override
1002        public StateTransitionResult.TransitionImpossible isTransitionToPossible(
1003                        WalkStateGraphContext walkStateGraphContext) {
1004            ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext);
1005            return null;
1006        }
1007
1008        @Override
1009        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) {
1010            closingStreamReceived = false;
1011
1012            StreamOpenAndCloseFactory openAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory();
1013            AbstractStreamClose closeStreamElement = openAndCloseFactory.createStreamClose();
1014            boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(closeStreamElement);
1015
1016            if (streamCloseIssued) {
1017                activeTransport.notifyAboutNewOutgoingElements();
1018
1019                boolean successfullyReceivedStreamClose = waitForClosingStreamTagFromServer();
1020
1021                if (successfullyReceivedStreamClose) {
1022                    for (Iterator<XmppInputOutputFilter> it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) {
1023                        XmppInputOutputFilter filter = it.next();
1024                        filter.closeInputOutput();
1025                    }
1026
1027                    // Closing the filters may produced new outgoing data. Notify the transport about it.
1028                    activeTransport.afterFiltersClosed();
1029
1030                    for (Iterator<XmppInputOutputFilter> it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) {
1031                        XmppInputOutputFilter filter = it.next();
1032                        try {
1033                            filter.waitUntilInputOutputClosed();
1034                        } catch (IOException | CertificateException | InterruptedException | SmackException
1035                                        | XMPPException e) {
1036                            LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e);
1037                        }
1038                    }
1039
1040                    // For correctness we set authenticated to false here, even though we will later again set it to
1041                    // false in the disconnected state.
1042                    authenticated = false;
1043                }
1044            }
1045
1046            return StateTransitionResult.Success.EMPTY_INSTANCE;
1047        }
1048    }
1049
1050    static final class InstantShutdownStateDescriptor extends StateDescriptor {
1051        private InstantShutdownStateDescriptor() {
1052            super(InstantShutdownState.class);
1053            addSuccessor(CloseConnectionStateDescriptor.class);
1054        }
1055    }
1056
1057    private static final class InstantShutdownState extends NoOpState {
1058        private InstantShutdownState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor,
1059                        ModularXmppClientToServerConnectionInternal connectionInternal) {
1060            super(connection, stateDescriptor, connectionInternal);
1061        }
1062
1063        @Override
1064        public StateTransitionResult.TransitionImpossible isTransitionToPossible(
1065                        WalkStateGraphContext walkStateGraphContext) {
1066            ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext);
1067            return null;
1068        }
1069    }
1070
1071    private static final class CloseConnectionStateDescriptor extends StateDescriptor {
1072        private CloseConnectionStateDescriptor() {
1073            super(CloseConnectionState.class);
1074            addSuccessor(DisconnectedStateDescriptor.class);
1075        }
1076    }
1077
1078    private final class CloseConnectionState extends State {
1079        private CloseConnectionState(StateDescriptor stateDescriptor,
1080                        ModularXmppClientToServerConnectionInternal connectionInternal) {
1081            super(stateDescriptor, connectionInternal);
1082        }
1083
1084        @Override
1085        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) {
1086            activeTransport.disconnect();
1087            activeTransport = null;
1088
1089            authenticated = connected = false;
1090
1091            return StateTransitionResult.Success.EMPTY_INSTANCE;
1092        }
1093    }
1094
1095    public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) {
1096        connectionStateMachineListeners.add(connectionStateMachineListener);
1097    }
1098
1099    public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) {
1100        return connectionStateMachineListeners.remove(connectionStateMachineListener);
1101    }
1102
1103    private void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) {
1104        if (connectionStateMachineListeners.isEmpty()) {
1105            return;
1106        }
1107
1108        ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> {
1109            for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) {
1110                connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this);
1111            }
1112        });
1113    }
1114
1115    @Override
1116    public boolean isSecureConnection() {
1117        final XmppClientToServerTransport transport = activeTransport;
1118        if (transport == null) {
1119            return false;
1120        }
1121        return transport.isTransportSecured();
1122    }
1123
1124    @Override
1125    protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException {
1126        WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(
1127                        ConnectedButUnauthenticatedStateDescriptor.class).build();
1128        walkStateGraph(walkStateGraphContext);
1129    }
1130
1131    private Map<String, Object> getFilterStats() {
1132        Collection<XmppInputOutputFilter> filters;
1133        synchronized (this) {
1134            if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) {
1135                filters = previousInputOutputFilters;
1136            } else {
1137                filters = inputOutputFilters;
1138            }
1139        }
1140
1141        Map<String, Object> filterStats = new HashMap<>(filters.size());
1142        for (XmppInputOutputFilter xmppInputOutputFilter : filters) {
1143            Object stats = xmppInputOutputFilter.getStats();
1144            String filterName = xmppInputOutputFilter.getFilterName();
1145
1146            filterStats.put(filterName, stats);
1147        }
1148
1149        return filterStats;
1150    }
1151
1152    public Stats getStats() {
1153        Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats = new HashMap<>(
1154                        transports.size());
1155        for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport> entry : transports.entrySet()) {
1156            XmppClientToServerTransport.Stats transportStats = entry.getValue().getStats();
1157
1158            transportsStats.put(entry.getKey(), transportStats);
1159        }
1160
1161        Map<String, Object> filterStats = getFilterStats();
1162
1163        return new Stats(transportsStats, filterStats);
1164    }
1165
1166    public static final class Stats extends AbstractStats {
1167        public final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats;
1168        public final Map<String, Object> filtersStats;
1169
1170        private Stats(Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats,
1171                        Map<String, Object> filtersStats) {
1172            this.transportsStats = Collections.unmodifiableMap(transportsStats);
1173            this.filtersStats = Collections.unmodifiableMap(filtersStats);
1174        }
1175
1176        @Override
1177        public void appendStatsTo(ExtendedAppendable appendable) throws IOException {
1178            StringUtils.appendHeading(appendable, "Connection stats", '#').append('\n');
1179
1180            for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> entry : transportsStats.entrySet()) {
1181                Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> transportClass = entry.getKey();
1182                XmppClientToServerTransport.Stats stats = entry.getValue();
1183
1184                StringUtils.appendHeading(appendable, transportClass.getName());
1185                if (stats != null) {
1186                    appendable.append(stats.toString());
1187                } else {
1188                    appendable.append("No stats available.");
1189                }
1190                appendable.append('\n');
1191            }
1192
1193            for (Map.Entry<String, Object> entry : filtersStats.entrySet()) {
1194                String filterName = entry.getKey();
1195                Object filterStats = entry.getValue();
1196
1197                StringUtils.appendHeading(appendable, filterName);
1198                appendable.append(filterStats.toString()).append('\n');
1199            }
1200        }
1201
1202    }
1203}