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