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