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