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