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}