/** * * Copyright 2018-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smack.fsm; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import javax.net.ssl.SSLSession; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.FailedNonzaException; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XmppInputOutputFilter; import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.compress.packet.Compressed; import org.jivesoftware.smack.compress.packet.Failure; import org.jivesoftware.smack.compression.XmppCompressionFactory; import org.jivesoftware.smack.compression.XmppCompressionManager; import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.StreamError; import org.jivesoftware.smack.parsing.SmackParsingException; import org.jivesoftware.smack.sasl.SASLErrorException; import org.jivesoftware.smack.sasl.SASLMechanism; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; import org.jxmpp.jid.parts.Resourcepart; public abstract class AbstractXmppStateMachineConnection extends AbstractXMPPConnection { private final List connectionStateMachineListeners = new CopyOnWriteArrayList<>(); private boolean featuresReceived; protected boolean streamResumed; private GraphVertex currentStateVertex; private List walkFromDisconnectToAuthenticated; private final List inputOutputFilters = new CopyOnWriteArrayList<>(); private List previousInputOutputFilters; protected AbstractXmppStateMachineConnection(ConnectionConfiguration configuration, GraphVertex initialStateDescriptorVertex) { super(configuration); currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, this); } @Override protected void loginInternal(String username, String password, Resourcepart resource) throws XMPPException, SmackException, IOException, InterruptedException { WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(AuthenticatedAndResourceBoundStateDescriptor.class) .withLoginContext(username, password, resource) .build(); walkStateGraph(walkStateGraphContext); } protected static WalkStateGraphContextBuilder buildNewWalkTo(Class finalStateClass) { return new WalkStateGraphContextBuilder(finalStateClass); } protected static final class WalkStateGraphContext { private final Class finalStateClass; private final Class mandatoryIntermediateState; private final LoginContext loginContext; private final List walkedStateGraphPath = new ArrayList<>(); /** * A linked Map of failed States with their reason as value. */ private final Map failedStates = new LinkedHashMap<>(); private boolean mandatoryIntermediateStateHandled; private WalkStateGraphContext(Class finalStateClass, Class mandatoryIntermedidateState, LoginContext loginContext) { this.finalStateClass = Objects.requireNonNull(finalStateClass); this.mandatoryIntermediateState = mandatoryIntermedidateState; this.loginContext = loginContext; } public boolean isFinalStateAuthenticatedAndResourceBound() { return finalStateClass == AuthenticatedAndResourceBoundStateDescriptor.class; } } protected static final class WalkStateGraphContextBuilder { private final Class finalStateClass; private Class mandatoryIntermedidateState; private LoginContext loginContext; private WalkStateGraphContextBuilder(Class finalStateClass) { this.finalStateClass = finalStateClass; } public WalkStateGraphContextBuilder withMandatoryIntermediateState(Class mandatoryIntermedidateState) { this.mandatoryIntermedidateState = mandatoryIntermedidateState; return this; } public WalkStateGraphContextBuilder withLoginContext(String username, String password, Resourcepart resource) { LoginContext loginContext = new LoginContext(username, password, resource); return withLoginContext(loginContext); } public WalkStateGraphContextBuilder withLoginContext(LoginContext loginContext) { this.loginContext = loginContext; return this; } public WalkStateGraphContext build() { return new WalkStateGraphContext(finalStateClass, mandatoryIntermedidateState, loginContext); } } protected final void walkStateGraph(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, FailedNonzaException, IOException, SmackException, InterruptedException { // Save a copy of the current state GraphVertex previousStateVertex = currentStateVertex; try { walkStateGraphInternal(walkStateGraphContext); } catch (XMPPErrorException | SASLErrorException | FailedNonzaException | IOException | SmackException | InterruptedException e) { currentStateVertex = previousStateVertex; // Reset that state. State revertedState = currentStateVertex.getElement(); invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); revertedState.resetState(); throw e; } } private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { // Save a copy of the current state final GraphVertex initialStateVertex = currentStateVertex; final State initialState = initialStateVertex.getElement(); final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); walkStateGraphContext.walkedStateGraphPath.add(initialState); if (initialStateDescriptor.getClass() == walkStateGraphContext.finalStateClass) { // If this is used as final state, then it should be marked as such. assert (initialStateDescriptor.isFinalState()); // We reached the final state. invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); return; } List> outgoingStateEdges = currentStateVertex.getOutgoingEdges(); // See if we need to handle mandatory intermediate states. if (walkStateGraphContext.mandatoryIntermediateState != null && !walkStateGraphContext.mandatoryIntermediateStateHandled) { // Check if outgoingStateEdges contains the mandatory intermediate state. GraphVertex mandatoryIntermediateStateVertex = null; for (GraphVertex outgoingStateVertex : outgoingStateEdges) { if (outgoingStateVertex.getElement().getStateDescriptor().getClass() == walkStateGraphContext.mandatoryIntermediateState) { mandatoryIntermediateStateVertex = outgoingStateVertex; break; } } if (mandatoryIntermediateStateVertex != null) { walkStateGraphContext.mandatoryIntermediateStateHandled = true; TransitionReason reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); if (reason instanceof TransitionSuccessResult) { walkStateGraph(walkStateGraphContext); return; } // We could not enter a mandatory intermediate state. Throw here. throw new StateMachineException.SmackMandatoryStateFailedException( mandatoryIntermediateStateVertex.getElement(), reason); } } for (Iterator> it = outgoingStateEdges.iterator(); it.hasNext();) { GraphVertex successorStateVertex = it.next(); State successorState = successorStateVertex.getElement(); TransitionReason reason = attemptEnterState(successorStateVertex, walkStateGraphContext); if (reason instanceof TransitionSuccessResult) { break; } // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we // just record this value and go on from there. Note that reason may be null, which is returned by // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't // record it. if (reason != null) { walkStateGraphContext.failedStates.put(successorState, reason); } if (!it.hasNext()) { throw new StateMachineException.SmackStateGraphDeadEndException(walkStateGraphContext.walkedStateGraphPath, walkStateGraphContext.failedStates); } } // Walk the state graph by recursion. walkStateGraph(walkStateGraphContext); } private TransitionReason attemptEnterState(GraphVertex successorStateVertex, WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPErrorException, SASLErrorException, IOException, InterruptedException, FailedNonzaException { final State successorState = successorStateVertex.getElement(); final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); if (!successorStateDescriptor.isMultiVisitState() && walkStateGraphContext.walkedStateGraphPath.contains(successorState)) { // This can happen if a state leads back to the state where it originated from. See for example the // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. return null; } if (successorStateDescriptor.isNotImplemented()) { TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new TransitionImpossibleBecauseNotImplemented( successorStateDescriptor); invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, transtionImpossibleBecauseNotImplemented)); return transtionImpossibleBecauseNotImplemented; } final TransitionIntoResult transitionIntoResult; try { TransitionImpossibleReason transitionImpossibleReason = successorState.isTransitionToPossible(walkStateGraphContext); if (transitionImpossibleReason != null) { invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, transitionImpossibleReason)); return transitionImpossibleReason; } invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(successorState)); transitionIntoResult = successorState.transitionInto(walkStateGraphContext); } catch (SmackException | XMPPErrorException | SASLErrorException | IOException | InterruptedException | FailedNonzaException e) { // TODO Document why this is required given that there is another call site of resetState(). invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(successorState)); successorState.resetState(); throw e; } if (transitionIntoResult instanceof TransitionFailureResult) { TransitionFailureResult transitionFailureResult = (TransitionFailureResult) transitionIntoResult; invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionFailed(successorState, transitionFailureResult)); return transitionIntoResult; } // If transitionIntoResult is not an instance of TransitionFailureResult, then it has to be of type // TransitionSuccessResult. TransitionSuccessResult transitionSuccessResult = (TransitionSuccessResult) transitionIntoResult; currentStateVertex = successorStateVertex; invokeConnectionStateMachineListener(new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, transitionSuccessResult)); return transitionSuccessResult; } protected abstract SSLSession getSSLSession(); @Override protected void afterFeaturesReceived() { featuresReceived = true; synchronized (this) { notifyAll(); } } protected final void parseAndProcessElement(String element) throws XmlPullParserException, IOException, InterruptedException, StreamErrorException, SmackException, SmackParsingException { XmlPullParser parser = PacketParserUtils.getParserFor(element); // Skip the enclosing stream open what is guaranteed to be there. parser.next(); XmlPullParser.Event event = parser.getEventType(); outerloop: while (true) { switch (event) { case START_ELEMENT: final String name = parser.getName(); // Note that we don't handle "stream" here as it's done in the splitter. switch (name) { case Message.ELEMENT: case IQ.IQ_ELEMENT: case Presence.ELEMENT: try { parseAndProcessStanza(parser); } finally { // TODO: Here would be the following stream management code. // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); } break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser, null); saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); throw new StreamErrorException(streamError); case "features": parseFeatures(parser); afterFeaturesReceived(); break; // SASL related top level stream elements case Challenge.ELEMENT: // The server is challenging the SASL authentication made by the client String challengeData = parser.nextText(); getSASLAuthentication().challengeReceived(challengeData); break; case Success.ELEMENT: Success success = new Success(parser.nextText()); // The SASL authentication with the server was successful. The next step // will be to bind the resource getSASLAuthentication().authenticated(success); sendStreamOpen(); break; default: parseAndProcessNonza(parser); break; } break; case END_DOCUMENT: break outerloop; default: // fall out } event = parser.next(); } } protected synchronized void prepareToWaitForFeaturesReceived() { featuresReceived = false; } protected void waitForFeaturesReceived(String waitFor) throws InterruptedException, ConnectionUnexpectedTerminatedException, NoResponseException { long waitStartMs = System.currentTimeMillis(); long timeoutMs = getReplyTimeout(); synchronized (this) { while (!featuresReceived && currentConnectionException == null) { long remainingWaitMs = timeoutMs - (System.currentTimeMillis() - waitStartMs); if (remainingWaitMs <= 0) { throw NoResponseException.newWith(this, waitFor); } wait(remainingWaitMs); } if (currentConnectionException != null) { throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); } } } protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { prepareToWaitForFeaturesReceived(); sendStreamOpen(); waitForFeaturesReceived(waitFor); } protected final void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { inputOutputFilters.add(0, xmppInputOutputFilter); } protected final ListIterator getXmppInputOutputFilterBeginIterator() { return inputOutputFilters.listIterator(); } protected final ListIterator getXmppInputOutputFilterEndIterator() { return inputOutputFilters.listIterator(inputOutputFilters.size()); } protected final synchronized List getFilterStats() { Collection filters; if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) { filters = previousInputOutputFilters; } else { filters = inputOutputFilters; } List filterStats = new ArrayList<>(filters.size()); for (XmppInputOutputFilter xmppInputOutputFilter : filters) { Object stats = xmppInputOutputFilter.getStats(); if (stats != null) { filterStats.add(stats); } } return Collections.unmodifiableList(filterStats); } protected abstract class State { private final StateDescriptor stateDescriptor; protected State(StateDescriptor stateDescriptor) { this.stateDescriptor = stateDescriptor; } /** * Check if the state should be activated. * * @return null if the state should be activated. * @throws SmackException in case a Smack exception occurs. */ protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) throws SmackException { return null; } protected abstract TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException; StateDescriptor getStateDescriptor() { return stateDescriptor; } protected void resetState() { } @Override public String toString() { return "State " + stateDescriptor + ' ' + AbstractXmppStateMachineConnection.this; } protected final void ensureNotOnOurWayToAuthenticatedAndResourceBound(WalkStateGraphContext walkStateGraphContext) { if (walkStateGraphContext.isFinalStateAuthenticatedAndResourceBound()) { throw new IllegalStateException( "Smack should never attempt to reach the authenticated and resource bound state over " + this + ". This is probably a programming error within Smack, please report it to the develoeprs."); } } } abstract static class TransitionReason { public final String reason; private TransitionReason(String reason) { this.reason = reason; } @Override public final String toString() { return reason; } } protected static class TransitionImpossibleReason extends TransitionReason { public TransitionImpossibleReason(String reason) { super(reason); } } protected static class TransitionImpossibleBecauseNotImplemented extends TransitionImpossibleReason { public TransitionImpossibleBecauseNotImplemented(StateDescriptor stateDescriptor) { super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)"); } } protected abstract static class TransitionIntoResult extends TransitionReason { public TransitionIntoResult(String reason) { super(reason); } } public static class TransitionSuccessResult extends TransitionIntoResult { public static final TransitionSuccessResult EMPTY_INSTANCE = new TransitionSuccessResult(); private TransitionSuccessResult() { super(""); } public TransitionSuccessResult(String reason) { super(reason); } } public static final class TransitionFailureResult extends TransitionIntoResult { private TransitionFailureResult(String reason) { super(reason); } } protected final class NoOpState extends State { private NoOpState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { // Transition into a NoOpState is always possible. return null; } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { // Transition into a NoOpState always succeeds. return TransitionSuccessResult.EMPTY_INSTANCE; } } protected static class DisconnectedStateDescriptor extends StateDescriptor { protected DisconnectedStateDescriptor() { super(DisconnectedState.class, StateDescriptor.Property.finalState); } } private final class DisconnectedState extends State { private DisconnectedState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { if (inputOutputFilters.isEmpty()) { previousInputOutputFilters = null; } else { previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); previousInputOutputFilters.addAll(inputOutputFilters); inputOutputFilters.clear(); } ListIterator it = walkFromDisconnectToAuthenticated.listIterator( walkFromDisconnectToAuthenticated.size()); while (it.hasPrevious()) { State stateToReset = it.previous(); stateToReset.resetState(); } walkFromDisconnectToAuthenticated = null; return TransitionSuccessResult.EMPTY_INSTANCE; } } protected static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor { private ConnectedButUnauthenticatedStateDescriptor() { super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState); addSuccessor(SaslAuthenticationStateDescriptor.class); } } private final class ConnectedButUnauthenticatedState extends State { private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { assert (walkFromDisconnectToAuthenticated == null); if (getStateDescriptor().getClass() == walkStateGraphContext.finalStateClass) { // If this is the final state, then record the walk so far. walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath); } connected = true; return TransitionSuccessResult.EMPTY_INSTANCE; } @Override protected void resetState() { connected = false; } } protected static final class SaslAuthenticationStateDescriptor extends StateDescriptor { private SaslAuthenticationStateDescriptor() { super(SaslAuthenticationState.class, "RFC 6120 § 6"); addSuccessor(AuthenticatedButUnboundStateDescriptor.class); } } private final class SaslAuthenticationState extends State { private SaslAuthenticationState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException { prepareToWaitForFeaturesReceived(); LoginContext loginContext = walkStateGraphContext.loginContext; SASLMechanism usedSaslMechanism = saslAuthentication.authenticate(loginContext.username, loginContext.password, config.getAuthzid(), getSSLSession()); // authenticate() will only return if the SASL authentication was successful, but we also need to wait for the next round of stream features. waitForFeaturesReceived("server stream features after SASL authentication"); return new SaslAuthenticationSuccessResult(usedSaslMechanism); } } public static final class SaslAuthenticationSuccessResult extends TransitionSuccessResult { private final String saslMechanismName; private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) { super("SASL authentication successfull using " + usedSaslMechanism.getName()); this.saslMechanismName = usedSaslMechanism.getName(); } public String getSaslMechanismName() { return saslMechanismName; } } protected static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor { private AuthenticatedButUnboundStateDescriptor() { super(StateDescriptor.Property.multiVisitState); addSuccessor(ResourceBindingStateDescriptor.class); addSuccessor(CompressionStateDescriptor.class); } } protected static final class ResourceBindingStateDescriptor extends StateDescriptor { private ResourceBindingStateDescriptor() { super(ResourceBindingState.class, "RFC 6120 § 7"); addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); } } private final class ResourceBindingState extends State { private ResourceBindingState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException { // TODO: The reportSuccess() is just a quick fix until there is a variant of the // bindResourceAndEstablishSession() method which does not require this. lastFeaturesReceived.reportSuccess(); LoginContext loginContext = walkStateGraphContext.loginContext; Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); streamResumed = false; return new ResourceBoundResult(resource, loginContext.resource); } } public static final class ResourceBoundResult extends TransitionSuccessResult { private final Resourcepart resource; private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) { super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "'"); this.resource = boundResource; } public Resourcepart getResource() { return resource; } } protected static final class CompressionStateDescriptor extends StateDescriptor { private CompressionStateDescriptor() { super(CompressionState.class, 138); addSuccessor(AuthenticatedButUnboundStateDescriptor.class); declarePrecedenceOver(ResourceBindingStateDescriptor.class); } } private boolean compressionEnabled; private class CompressionState extends State { private XmppCompressionFactory selectedCompressionFactory; private XmppInputOutputFilter usedXmppInputOutputCompressionFitler; protected CompressionState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { if (!config.isCompressionEnabled()) { return new TransitionImpossibleReason("Stream compression disabled"); } Compress.Feature compressFeature = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); if (compressFeature == null) { return new TransitionImpossibleReason("Stream compression not supported"); } selectedCompressionFactory = XmppCompressionManager.getBestFactory(compressFeature); if (selectedCompressionFactory == null) { return new TransitionImpossibleReason("No matching compression factory"); } usedXmppInputOutputCompressionFitler = selectedCompressionFactory.fabricate(config); return null; } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException, ConnectionUnexpectedTerminatedException { final String compressionMethod = selectedCompressionFactory.getCompressionMethod(); sendAndWaitForResponse(new Compress(compressionMethod), Compressed.class, Failure.class); addXmppInputOutputFilter(usedXmppInputOutputCompressionFitler); newStreamOpenWaitForFeaturesSequence("server stream features after compression enabled"); compressionEnabled = true; return new CompressionTransitionSuccessResult(compressionMethod); } @Override protected void resetState() { selectedCompressionFactory = null; usedXmppInputOutputCompressionFitler = null; compressionEnabled = false; } } public static final class CompressionTransitionSuccessResult extends TransitionSuccessResult { private final String compressionMethod; private CompressionTransitionSuccessResult(String compressionMethod) { super(compressionMethod + " compression enabled"); this.compressionMethod = compressionMethod; } public String getCompressionMethod() { return compressionMethod; } } @Override public final boolean isUsingCompression() { return compressionEnabled; } protected static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor { private AuthenticatedAndResourceBoundStateDescriptor() { super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState); } } private final class AuthenticatedAndResourceBoundState extends State { private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor) { super(stateDescriptor); } @Override protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws NotConnectedException, InterruptedException { if (walkFromDisconnectToAuthenticated != null) { // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current // walk must not start from the 'Disconnected' state. assert (walkStateGraphContext.walkedStateGraphPath.get(0).stateDescriptor.getClass() != DisconnectedStateDescriptor.class); walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); } else { walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath.size() + 1); walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); } walkFromDisconnectToAuthenticated.add(this); afterSuccessfulLogin(streamResumed); return TransitionSuccessResult.EMPTY_INSTANCE; } @Override protected void resetState() { authenticated = false; } } public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { connectionStateMachineListeners.add(connectionStateMachineListener); } public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { return connectionStateMachineListeners.remove(connectionStateMachineListener); } protected void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { if (connectionStateMachineListeners.isEmpty()) { return; } ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) { connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this); } }); } }