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", "TypeParameterUnusedInFormals"})
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/Shutdown 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    @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
1052                                        | XMPPException e) {
1053                            LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e);
1054                        }
1055                    }
1056
1057                    // For correctness we set authenticated to false here, even though we will later again set it to
1058                    // false in the disconnected state.
1059                    authenticated = false;
1060                }
1061            }
1062
1063            return StateTransitionResult.Success.EMPTY_INSTANCE;
1064        }
1065    }
1066
1067    static final class InstantShutdownStateDescriptor extends StateDescriptor {
1068        private InstantShutdownStateDescriptor() {
1069            super(InstantShutdownState.class);
1070            addSuccessor(CloseConnectionStateDescriptor.class);
1071        }
1072    }
1073
1074    private static final class InstantShutdownState extends NoOpState {
1075        // Invoked via reflection.
1076        @SuppressWarnings("UnusedMethod")
1077        private InstantShutdownState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor,
1078                        ModularXmppClientToServerConnectionInternal connectionInternal) {
1079            super(connection, stateDescriptor, connectionInternal);
1080        }
1081
1082        @Override
1083        public StateTransitionResult.TransitionImpossible isTransitionToPossible(
1084                        WalkStateGraphContext walkStateGraphContext) {
1085            ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext);
1086            return null;
1087        }
1088    }
1089
1090    private static final class CloseConnectionStateDescriptor extends StateDescriptor {
1091        private CloseConnectionStateDescriptor() {
1092            super(CloseConnectionState.class);
1093            addSuccessor(DisconnectedStateDescriptor.class);
1094        }
1095    }
1096
1097    private final class CloseConnectionState extends State {
1098        // Invoked via reflection.
1099        @SuppressWarnings("UnusedMethod")
1100        private CloseConnectionState(StateDescriptor stateDescriptor,
1101                        ModularXmppClientToServerConnectionInternal connectionInternal) {
1102            super(stateDescriptor, connectionInternal);
1103        }
1104
1105        @Override
1106        public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) {
1107            activeTransport.disconnect();
1108            activeTransport = null;
1109
1110            authenticated = connected = false;
1111
1112            return StateTransitionResult.Success.EMPTY_INSTANCE;
1113        }
1114    }
1115
1116    public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) {
1117        connectionStateMachineListeners.add(connectionStateMachineListener);
1118    }
1119
1120    public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) {
1121        return connectionStateMachineListeners.remove(connectionStateMachineListener);
1122    }
1123
1124    private void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) {
1125        if (connectionStateMachineListeners.isEmpty()) {
1126            return;
1127        }
1128
1129        ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> {
1130            for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) {
1131                connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this);
1132            }
1133        });
1134    }
1135
1136    @Override
1137    public boolean isSecureConnection() {
1138        final XmppClientToServerTransport transport = activeTransport;
1139        if (transport == null) {
1140            return false;
1141        }
1142        return transport.isTransportSecured();
1143    }
1144
1145    @Override
1146    protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException {
1147        WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(
1148                        ConnectedButUnauthenticatedStateDescriptor.class).build();
1149        walkStateGraph(walkStateGraphContext);
1150    }
1151
1152    @Override
1153    public InetAddress getLocalAddress() {
1154        return null;
1155    }
1156
1157    private Map<String, Object> getFilterStats() {
1158        Collection<XmppInputOutputFilter> filters;
1159        synchronized (this) {
1160            if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) {
1161                filters = previousInputOutputFilters;
1162            } else {
1163                filters = inputOutputFilters;
1164            }
1165        }
1166
1167        Map<String, Object> filterStats = new HashMap<>(filters.size());
1168        for (XmppInputOutputFilter xmppInputOutputFilter : filters) {
1169            Object stats = xmppInputOutputFilter.getStats();
1170            String filterName = xmppInputOutputFilter.getFilterName();
1171
1172            filterStats.put(filterName, stats);
1173        }
1174
1175        return filterStats;
1176    }
1177
1178    public Stats getStats() {
1179        Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats = new HashMap<>(
1180                        transports.size());
1181        for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport> entry : transports.entrySet()) {
1182            XmppClientToServerTransport.Stats transportStats = entry.getValue().getStats();
1183
1184            transportsStats.put(entry.getKey(), transportStats);
1185        }
1186
1187        Map<String, Object> filterStats = getFilterStats();
1188
1189        return new Stats(transportsStats, filterStats);
1190    }
1191
1192    public static final class Stats extends AbstractStats {
1193        public final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats;
1194        public final Map<String, Object> filtersStats;
1195
1196        private Stats(Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats,
1197                        Map<String, Object> filtersStats) {
1198            this.transportsStats = Collections.unmodifiableMap(transportsStats);
1199            this.filtersStats = Collections.unmodifiableMap(filtersStats);
1200        }
1201
1202        @Override
1203        public void appendStatsTo(ExtendedAppendable appendable) throws IOException {
1204            StringUtils.appendHeading(appendable, "Connection stats", '#').append('\n');
1205
1206            for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> entry : transportsStats.entrySet()) {
1207                Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> transportClass = entry.getKey();
1208                XmppClientToServerTransport.Stats stats = entry.getValue();
1209
1210                StringUtils.appendHeading(appendable, transportClass.getName());
1211                if (stats != null) {
1212                    appendable.append(stats.toString());
1213                } else {
1214                    appendable.append("No stats available.");
1215                }
1216                appendable.append('\n');
1217            }
1218
1219            for (Map.Entry<String, Object> entry : filtersStats.entrySet()) {
1220                String filterName = entry.getKey();
1221                Object filterStats = entry.getValue();
1222
1223                StringUtils.appendHeading(appendable, filterName);
1224                appendable.append(filterStats.toString()).append('\n');
1225            }
1226        }
1227
1228    }
1229}