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