/**
*
* Copyright 2018 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.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.DisconnectedStateDescriptor;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.util.Consumer;
import org.jivesoftware.smack.util.MultiMap;
/**
* Smack's utility API for Finite State Machines (FSM).
*
*
* Thanks to Andreas Fried for the fun and successful bug hunting session.
*
*
* @author Florian Schmaus
*
*/
public class StateDescriptorGraph {
private static final Logger LOGGER = Logger.getLogger(StateDescriptorGraph.class.getName());
private static GraphVertex addNewStateDescriptorGraphVertex(
Class extends StateDescriptor> stateDescriptorClass,
Map, GraphVertex> graphVertexes)
throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
Constructor extends StateDescriptor> stateDescriptorConstructor = stateDescriptorClass.getDeclaredConstructor();
stateDescriptorConstructor.setAccessible(true);
StateDescriptor stateDescriptor = stateDescriptorConstructor.newInstance();
GraphVertex graphVertexStateDescriptor = new GraphVertex<>(stateDescriptor);
GraphVertex previous = graphVertexes.put(stateDescriptorClass, graphVertexStateDescriptor);
assert previous == null;
return graphVertexStateDescriptor;
}
private static final class HandleStateDescriptorGraphVertexContext {
private final Set> handledStateDescriptors = new HashSet<>();
Map, GraphVertex> graphVertexes;
MultiMap, Class extends StateDescriptor>> inferredForwardEdges;
private HandleStateDescriptorGraphVertexContext(
Map, GraphVertex> graphVertexes,
MultiMap, Class extends StateDescriptor>> inferredForwardEdges) {
this.graphVertexes = graphVertexes;
this.inferredForwardEdges = inferredForwardEdges;
}
private boolean recurseInto(Class extends StateDescriptor> stateDescriptorClass) {
boolean wasAdded = handledStateDescriptors.add(stateDescriptorClass);
boolean alreadyHandled = !wasAdded;
return alreadyHandled;
}
private GraphVertex getOrConstruct(Class extends StateDescriptor> stateDescriptorClass)
throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
GraphVertex graphVertexStateDescriptor = graphVertexes.get(stateDescriptorClass);
if (graphVertexStateDescriptor == null) {
graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(stateDescriptorClass, graphVertexes);
for (Class extends StateDescriptor> inferredSuccessor : inferredForwardEdges.getAll(
stateDescriptorClass)) {
graphVertexStateDescriptor.getElement().addSuccessor(inferredSuccessor);
}
}
return graphVertexStateDescriptor;
}
}
private static void handleStateDescriptorGraphVertex(GraphVertex node,
HandleStateDescriptorGraphVertexContext context)
throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
Class extends StateDescriptor> stateDescriptorClass = node.element.getClass();
boolean alreadyHandled = context.recurseInto(stateDescriptorClass);
if (alreadyHandled) {
return;
}
Set> successorClasses = node.element.getSuccessors();
int numSuccessors = successorClasses.size();
Map, GraphVertex> successorStateDescriptors = new HashMap<>(
numSuccessors);
for (Class extends StateDescriptor> successorClass : successorClasses) {
GraphVertex successorGraphNode = context.getOrConstruct(successorClass);
successorStateDescriptors.put(successorClass, successorGraphNode);
}
switch (numSuccessors) {
case 0:
throw new IllegalStateException("State " + stateDescriptorClass + " has no successor");
case 1:
GraphVertex soleSuccessorNode = successorStateDescriptors.values().iterator().next();
node.addOutgoingEdge(soleSuccessorNode);
handleStateDescriptorGraphVertex(soleSuccessorNode, context);
return;
}
// We hit a state with multiple successors, perform a topological sort on the successors first.
// Process the information regarding subordinates and superiors states.
// The preference graph is the graph where the precedence information of all successors is stored, which we will
// topologically sort to find out which successor we should try first. It is a further new graph we use solely in
// this step for every node. The graph is represented as map. There is no special marker for the initial node
// as it is not required for the topological sort performed later.
Map, GraphVertex>> preferenceGraph = new HashMap<>(numSuccessors);
// Iterate over all successor states of the current state.
for (GraphVertex successorStateDescriptorGraphNode : successorStateDescriptors.values()) {
StateDescriptor successorStateDescriptor = successorStateDescriptorGraphNode.element;
Class extends StateDescriptor> successorStateDescriptorClass = successorStateDescriptor.getClass();
for (Class extends StateDescriptor> subordinateClass : successorStateDescriptor.getSubordinates()) {
if (!successorClasses.contains(subordinateClass)) {
LOGGER.severe(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set");
continue;
}
GraphVertex> superiorClassNode = lookupAndCreateIfRequired(
preferenceGraph, successorStateDescriptorClass);
GraphVertex> subordinateClassNode = lookupAndCreateIfRequired(
preferenceGraph, subordinateClass);
superiorClassNode.addOutgoingEdge(subordinateClassNode);
}
for (Class extends StateDescriptor> superiorClass : successorStateDescriptor.getSuperiors()) {
if (!successorClasses.contains(superiorClass)) {
LOGGER.severe(successorStateDescriptor + " points to a superior '" + superiorClass
+ "' which is not part of the successor set");
continue;
}
GraphVertex> subordinateClassNode = lookupAndCreateIfRequired(
preferenceGraph, successorStateDescriptorClass);
GraphVertex> superiorClassNode = lookupAndCreateIfRequired(
preferenceGraph, superiorClass);
superiorClassNode.addOutgoingEdge(subordinateClassNode);
}
}
// Perform a topological sort which returns the state descriptor classes sorted by their priority. Highest
// priority state descriptors first.
List>> sortedSuccessors = topologicalSort(preferenceGraph.values());
// Handle the successor nodes which have not preference information available. Simply append them to the end of
// the sorted successor list.
outerloop: for (Class extends StateDescriptor> successorStateDescriptor : successorClasses) {
for (GraphVertex> sortedSuccessor : sortedSuccessors) {
if (sortedSuccessor.getElement() == successorStateDescriptor) {
continue outerloop;
}
}
sortedSuccessors.add(new GraphVertex<>(successorStateDescriptor));
}
for (GraphVertex> successor : sortedSuccessors) {
GraphVertex successorVertex = successorStateDescriptors.get(successor.element);
node.addOutgoingEdge(successorVertex);
// Recurse further.
handleStateDescriptorGraphVertex(successorVertex, context);
}
}
public static GraphVertex constructStateDescriptorGraph(Set> backwardEdgeStateDescriptors)
throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
Map, GraphVertex> graphVertexes = new HashMap<>();
final Class extends StateDescriptor> initialStatedescriptorClass = DisconnectedStateDescriptor.class;
GraphVertex initialNode = addNewStateDescriptorGraphVertex(initialStatedescriptorClass, graphVertexes);
MultiMap, Class extends StateDescriptor>> inferredForwardEdges = new MultiMap<>();
for (Class extends StateDescriptor> backwardsEdge : backwardEdgeStateDescriptors) {
GraphVertex graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(backwardsEdge, graphVertexes);
for (Class extends StateDescriptor> predecessor : graphVertexStateDescriptor.getElement().getPredeccessors()) {
inferredForwardEdges.put(predecessor, backwardsEdge);
}
}
// Ensure that the intial node has their successors inferred.
for (Class extends StateDescriptor> inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) {
initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor);
}
HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges);
handleStateDescriptorGraphVertex(initialNode, context);
return initialNode;
}
private static GraphVertex convertToStateGraph(GraphVertex stateDescriptorVertex,
ModularXmppClientToServerConnectionInternal connectionInternal, Map> handledStateDescriptors) {
StateDescriptor stateDescriptor = stateDescriptorVertex.getElement();
GraphVertex stateVertex = handledStateDescriptors.get(stateDescriptor);
if (stateVertex != null) {
return stateVertex;
}
State state = stateDescriptor.constructState(connectionInternal);
stateVertex = new GraphVertex<>(state);
handledStateDescriptors.put(stateDescriptor, stateVertex);
for (GraphVertex successorStateDescriptorVertex : stateDescriptorVertex.getOutgoingEdges()) {
GraphVertex successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connectionInternal, handledStateDescriptors);
// It is important that we keep the order of the edges. This should do it.
stateVertex.addOutgoingEdge(successorStateVertex);
}
return stateVertex;
}
public static GraphVertex convertToStateGraph(GraphVertex initialStateDescriptor,
ModularXmppClientToServerConnectionInternal connectionInternal) {
Map> handledStateDescriptors = new HashMap<>();
GraphVertex initialState = convertToStateGraph(initialStateDescriptor, connectionInternal,
handledStateDescriptors);
return initialState;
}
// Graph API after here.
// This API could possibly factored out into an extra package/class, but then we will probably need a builder for
// the graph vertex in order to keep it immutable.
public static final class GraphVertex {
private final E element;
private final List> outgoingEdges = new ArrayList<>();
private VertexColor color = VertexColor.white;
private GraphVertex(E element) {
this.element = element;
}
private void addOutgoingEdge(GraphVertex vertex) {
assert vertex != null;
if (outgoingEdges.contains(vertex)) {
throw new IllegalArgumentException("This " + this + " already has an outgoing edge to " + vertex);
}
outgoingEdges.add(vertex);
}
public E getElement() {
return element;
}
public List> getOutgoingEdges() {
return Collections.unmodifiableList(outgoingEdges);
}
private enum VertexColor {
white,
grey,
black,
}
@Override
public String toString() {
return toString(true);
}
public String toString(boolean includeOutgoingEdges) {
StringBuilder sb = new StringBuilder();
sb.append("GraphVertex " + element + " [color=" + color
+ ", identityHashCode=" + System.identityHashCode(this)
+ ", outgoingEdgeCount=" + outgoingEdges.size());
if (includeOutgoingEdges) {
sb.append(", outgoingEdges={");
for (Iterator> it = outgoingEdges.iterator(); it.hasNext();) {
GraphVertex outgoingEdgeVertex = it.next();
sb.append(outgoingEdgeVertex.toString(false));
if (it.hasNext()) {
sb.append(", ");
}
}
sb.append('}');
}
sb.append(']');
return sb.toString();
}
}
private static GraphVertex> lookupAndCreateIfRequired(
Map, GraphVertex>> map,
Class extends StateDescriptor> clazz) {
GraphVertex> vertex = map.get(clazz);
if (vertex == null) {
vertex = new GraphVertex<>(clazz);
map.put(clazz, vertex);
}
return vertex;
}
private static List> topologicalSort(Collection> vertexes) {
List> res = new ArrayList<>();
dfs(vertexes, vertex -> res.add(0, vertex), null);
return res;
}
private static void dfsVisit(GraphVertex vertex, Consumer> dfsFinishedVertex,
DfsEdgeFound dfsEdgeFound) {
vertex.color = GraphVertex.VertexColor.grey;
final int totalEdgeCount = vertex.getOutgoingEdges().size();
int edgeCount = 0;
for (GraphVertex successorVertex : vertex.getOutgoingEdges()) {
edgeCount++;
if (dfsEdgeFound != null) {
dfsEdgeFound.onEdgeFound(vertex, successorVertex, edgeCount, totalEdgeCount);
}
if (successorVertex.color == GraphVertex.VertexColor.white) {
dfsVisit(successorVertex, dfsFinishedVertex, dfsEdgeFound);
}
}
vertex.color = GraphVertex.VertexColor.black;
if (dfsFinishedVertex != null) {
dfsFinishedVertex.accept(vertex);
}
}
private static void dfs(Collection> vertexes, Consumer> dfsFinishedVertex,
DfsEdgeFound dfsEdgeFound) {
for (GraphVertex vertex : vertexes) {
if (vertex.color == GraphVertex.VertexColor.white) {
dfsVisit(vertex, dfsFinishedVertex, dfsEdgeFound);
}
}
}
public static void stateDescriptorGraphToDot(Collection> vertexes,
PrintWriter dotOut, boolean breakStateName) {
dotOut.append("digraph {\n");
dfs(vertexes,
finishedVertex -> {
boolean isMultiVisitState = finishedVertex.element.isMultiVisitState();
boolean isFinalState = finishedVertex.element.isFinalState();
boolean isNotImplemented = finishedVertex.element.isNotImplemented();
String style = null;
if (isMultiVisitState) {
style = "bold";
} else if (isFinalState) {
style = "filled";
} else if (isNotImplemented) {
style = "dashed";
}
if (style == null) {
return;
}
dotOut.append('"')
.append(finishedVertex.element.getFullStateName(breakStateName))
.append("\" [ ")
.append("style=")
.append(style)
.append(" ]\n");
},
(from, to, edgeId, totalEdgeCount) -> {
dotOut.append(" \"")
.append(from.element.getFullStateName(breakStateName))
.append("\" -> \"")
.append(to.element.getFullStateName(breakStateName))
.append('"');
if (totalEdgeCount > 1) {
// Note that 'dot' requires *double* quotes to enclose the value.
dotOut.append(" [xlabel=\"")
.append(Integer.toString(edgeId))
.append("\"]");
}
dotOut.append(";\n");
});
dotOut.append("}\n");
}
private interface DfsEdgeFound {
void onEdgeFound(GraphVertex from, GraphVertex to, int edgeId, int totalEdgeCount);
}
}