001/**
002 *
003 * Copyright 2018 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.fsm;
018
019import java.io.PrintWriter;
020import java.lang.reflect.Constructor;
021import java.lang.reflect.InvocationTargetException;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.logging.Logger;
032
033import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.DisconnectedStateDescriptor;
034import org.jivesoftware.smack.util.MultiMap;
035
036public class StateDescriptorGraph {
037
038    private static final Logger LOGGER = Logger.getLogger(StateDescriptorGraph.class.getName());
039
040    private static GraphVertex<StateDescriptor> addNewStateDescriptorGraphVertex(
041                    Class<? extends StateDescriptor> stateDescriptorClass,
042                    Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes)
043                    throws InstantiationException, IllegalAccessException, IllegalArgumentException,
044                    InvocationTargetException, NoSuchMethodException, SecurityException {
045        Constructor<? extends StateDescriptor> stateDescriptorConstructor = stateDescriptorClass.getDeclaredConstructor();
046        stateDescriptorConstructor.setAccessible(true);
047        StateDescriptor stateDescriptor = stateDescriptorConstructor.newInstance();
048        GraphVertex<StateDescriptor> graphVertexStateDescriptor = new GraphVertex<>(stateDescriptor);
049
050        GraphVertex<StateDescriptor> previous = graphVertexes.put(stateDescriptorClass, graphVertexStateDescriptor);
051        assert previous == null;
052
053        return graphVertexStateDescriptor;
054    }
055
056    private static final class HandleStateDescriptorGraphVertexContext {
057        private final Set<Class<? extends StateDescriptor>> handledStateDescriptors = new HashSet<>();
058        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes;
059        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges;
060
061        private HandleStateDescriptorGraphVertexContext(
062                        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes,
063                        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges) {
064            this.graphVertexes = graphVertexes;
065            this.inferredForwardEdges = inferredForwardEdges;
066        }
067
068        private boolean recurseInto(Class<? extends StateDescriptor> stateDescriptorClass) {
069            boolean wasAdded = handledStateDescriptors.add(stateDescriptorClass);
070            boolean alreadyHandled = !wasAdded;
071            return alreadyHandled;
072        }
073
074        private GraphVertex<StateDescriptor> getOrConstruct(Class<? extends StateDescriptor> stateDescriptorClass)
075                        throws InstantiationException, IllegalAccessException, IllegalArgumentException,
076                        InvocationTargetException, NoSuchMethodException, SecurityException {
077            GraphVertex<StateDescriptor> graphVertexStateDescriptor = graphVertexes.get(stateDescriptorClass);
078
079            if (graphVertexStateDescriptor == null) {
080                graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(stateDescriptorClass, graphVertexes);
081
082                for (Class<? extends StateDescriptor> inferredSuccessor : inferredForwardEdges.getAll(
083                                stateDescriptorClass)) {
084                    graphVertexStateDescriptor.getElement().addSuccessor(inferredSuccessor);
085                }
086            }
087
088            return graphVertexStateDescriptor;
089        }
090    }
091
092    private static void handleStateDescriptorGraphVertex(GraphVertex<StateDescriptor> node,
093                    HandleStateDescriptorGraphVertexContext context)
094                    throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
095        Class<? extends StateDescriptor> stateDescriptorClass = node.element.getClass();
096        boolean alreadyHandled = context.recurseInto(stateDescriptorClass);
097        if (alreadyHandled) {
098            return;
099        }
100
101        Set<Class<? extends StateDescriptor>> successorClasses = node.element.getSuccessors();
102        int numSuccessors = successorClasses.size();
103
104        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> successorStateDescriptors = new HashMap<>(
105                        numSuccessors);
106        for (Class<? extends StateDescriptor> successorClass : successorClasses) {
107            GraphVertex<StateDescriptor> successorGraphNode = context.getOrConstruct(successorClass);
108            successorStateDescriptors.put(successorClass, successorGraphNode);
109        }
110
111        switch (numSuccessors) {
112        case 0:
113            throw new IllegalStateException("State " + stateDescriptorClass + " has no successor");
114        case 1:
115            GraphVertex<StateDescriptor> soleSuccessorNode = successorStateDescriptors.values().iterator().next();
116            node.addOutgoingEdge(soleSuccessorNode);
117            handleStateDescriptorGraphVertex(soleSuccessorNode, context);
118            return;
119        }
120
121        // We hit a state with multiple successors, perform a topological sort on the successors first.
122        // Process the information regarding subordinates and superiors states.
123        // TODO: Find a better name for preference graph. It is the graph where the precedence information of all
124        // successors is stored, which we will topologically sort to find out which successor we should try first.
125        Map<Class<? extends StateDescriptor>, GraphVertex<Class<? extends StateDescriptor>>> preferenceGraph = new HashMap<>(numSuccessors);
126        for (GraphVertex<StateDescriptor> successorStateDescriptorGraphNode : successorStateDescriptors.values()) {
127            StateDescriptor successorStateDescriptor = successorStateDescriptorGraphNode.element;
128            Class<? extends StateDescriptor> successorStateDescriptorClass = successorStateDescriptor.getClass();
129            for (Class<? extends StateDescriptor> subordinateClass : successorStateDescriptor.getSubordinates()) {
130                if (!successorClasses.contains(subordinateClass)) {
131                    // TODO: Make this an exception.
132                    LOGGER.warning(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set");
133                    continue;
134                }
135
136                GraphVertex<Class<? extends StateDescriptor>> superiorClassNode = lookupAndCreateIfRequired(
137                                preferenceGraph, successorStateDescriptorClass);
138                GraphVertex<Class<? extends StateDescriptor>> subordinateClassNode = lookupAndCreateIfRequired(
139                                preferenceGraph, subordinateClass);
140
141                superiorClassNode.addOutgoingEdge(subordinateClassNode);
142            }
143            for (Class<? extends StateDescriptor> superiorClass : successorStateDescriptor.getSuperiors()) {
144                if (!successorClasses.contains(superiorClass)) {
145                    // TODO: Make this an exception.
146                    LOGGER.warning(successorStateDescriptor + " points to a superior '" + superiorClass
147                                    + "' which is not part of the successor set");
148                    continue;
149                }
150
151                GraphVertex<Class<? extends StateDescriptor>> subordinateClassNode = lookupAndCreateIfRequired(
152                                preferenceGraph, successorStateDescriptorClass);
153                GraphVertex<Class<? extends StateDescriptor>> superiorClassNode = lookupAndCreateIfRequired(
154                                preferenceGraph, superiorClass);
155
156                superiorClassNode.addOutgoingEdge(subordinateClassNode);
157            }
158        }
159
160        // Perform a topological sort which returns the state descriptor classes in their priority.
161        List<GraphVertex<Class<? extends StateDescriptor>>> sortedSuccessors = topologicalSort(preferenceGraph.values());
162
163        outerloop: for (Class<? extends StateDescriptor> successorStateDescriptor : successorClasses) {
164            for (GraphVertex<Class<? extends StateDescriptor>> sortedSuccessor : sortedSuccessors) {
165                if (sortedSuccessor.getElement() == successorStateDescriptor) {
166                    continue outerloop;
167                }
168            }
169            sortedSuccessors.add(new GraphVertex<>(successorStateDescriptor));
170        }
171
172        for (GraphVertex<Class<? extends StateDescriptor>> successor : sortedSuccessors) {
173            GraphVertex<StateDescriptor> successorVertex = successorStateDescriptors.get(successor.element);
174            assert successorVertex != null;
175            node.outgoingEdges.add(successorVertex);
176            handleStateDescriptorGraphVertex(successorVertex, context);
177        }
178    }
179
180    public static GraphVertex<StateDescriptor> constructStateDescriptorGraph(Set<Class<? extends StateDescriptor>> backwardEdgeStateDescriptors)
181                    throws InstantiationException, IllegalAccessException, IllegalArgumentException,
182                    InvocationTargetException, NoSuchMethodException, SecurityException {
183        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes = new HashMap<>();
184
185        final Class<? extends StateDescriptor> initialStatedescriptorClass = DisconnectedStateDescriptor.class;
186        GraphVertex<StateDescriptor> initialNode = addNewStateDescriptorGraphVertex(initialStatedescriptorClass, graphVertexes);
187
188        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges = new MultiMap<>();
189        for (Class<? extends StateDescriptor> backwardsEdge : backwardEdgeStateDescriptors) {
190            GraphVertex<StateDescriptor> graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(backwardsEdge, graphVertexes);
191
192            for (Class<? extends StateDescriptor> predecessor : graphVertexStateDescriptor.getElement().getPredeccessors()) {
193                inferredForwardEdges.put(predecessor, backwardsEdge);
194            }
195        }
196        // Ensure that the intial node has their successors inferred.
197        for (Class<? extends StateDescriptor> inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) {
198            initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor);
199        }
200
201        HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges);
202        handleStateDescriptorGraphVertex(initialNode, context);
203
204        return initialNode;
205    }
206
207    private static GraphVertex<AbstractXmppStateMachineConnection.State> convertToStateGraph(GraphVertex<StateDescriptor> stateDescriptorVertex,
208                    AbstractXmppStateMachineConnection connection, Map<StateDescriptor, GraphVertex<AbstractXmppStateMachineConnection.State>> handledStateDescriptors) {
209        StateDescriptor stateDescriptor = stateDescriptorVertex.getElement();
210        GraphVertex<AbstractXmppStateMachineConnection.State> stateVertex = handledStateDescriptors.get(stateDescriptor);
211        if (stateVertex != null) {
212            return stateVertex;
213        }
214
215        AbstractXmppStateMachineConnection.State state = stateDescriptor.constructState(connection);
216        stateVertex = new GraphVertex<>(state);
217        handledStateDescriptors.put(stateDescriptor, stateVertex);
218        for (GraphVertex<StateDescriptor> successorStateDescriptorVertex : stateDescriptorVertex.getOutgoingEdges()) {
219            GraphVertex<AbstractXmppStateMachineConnection.State> successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connection, handledStateDescriptors);
220            // It is important that we keep the order of the edges. This should do it.
221            stateVertex.addOutgoingEdge(successorStateVertex);
222        }
223
224        return stateVertex;
225    }
226
227    static GraphVertex<AbstractXmppStateMachineConnection.State> convertToStateGraph(GraphVertex<StateDescriptor> initialStateDescriptor,
228                    AbstractXmppStateMachineConnection connection) {
229        Map<StateDescriptor, GraphVertex<AbstractXmppStateMachineConnection.State>> handledStateDescriptors = new HashMap<>();
230        GraphVertex<AbstractXmppStateMachineConnection.State> initialState = convertToStateGraph(initialStateDescriptor, connection,
231                        handledStateDescriptors);
232        return initialState;
233    }
234
235    // TODO: Graph API after here
236    // This API could possibly factored out into an extra package/class, but then we will probably need a builder for
237    // the graph vertex in order to keep it immutable.
238    public static final class GraphVertex<E> {
239        private final E element;
240        private final List<GraphVertex<E>> outgoingEdges = new ArrayList<>();
241
242        private VertexColor color = VertexColor.white;
243
244        private GraphVertex(E element) {
245            this.element = element;
246        }
247
248        private void addOutgoingEdge(GraphVertex<E> vertex) {
249            if (outgoingEdges.contains(vertex)) {
250                throw new IllegalArgumentException("This " + this + " already has an outgoing edge to " + vertex);
251            }
252            outgoingEdges.add(vertex);
253        }
254
255        public E getElement() {
256            return element;
257        }
258
259        public List<GraphVertex<E>> getOutgoingEdges() {
260            return Collections.unmodifiableList(outgoingEdges);
261        }
262
263        private enum VertexColor {
264            white,
265            grey,
266            black,
267        }
268
269        @Override
270        public String toString() {
271            return toString(true);
272        }
273
274        public String toString(boolean includeOutgoingEdges) {
275            StringBuilder sb = new StringBuilder();
276            sb.append("GraphVertex " + element + " [color=" + color
277                            + ", identityHashCode=" + System.identityHashCode(this)
278                            + ", outgoingEdgeCount=" + outgoingEdges.size());
279
280            if (includeOutgoingEdges) {
281                sb.append(", outgoingEdges={");
282
283                for (Iterator<GraphVertex<E>> it = outgoingEdges.iterator(); it.hasNext();) {
284                    GraphVertex<E> outgoingEdgeVertex = it.next();
285                    sb.append(outgoingEdgeVertex.toString(false));
286                    if (it.hasNext()) {
287                        sb.append(", ");
288                    }
289                }
290                sb.append('}');
291            }
292
293            sb.append(']');
294            return sb.toString();
295        }
296    }
297
298    private static GraphVertex<Class<? extends StateDescriptor>> lookupAndCreateIfRequired(
299                    Map<Class<? extends StateDescriptor>, GraphVertex<Class<? extends StateDescriptor>>> map,
300                    Class<? extends StateDescriptor> clazz) {
301        GraphVertex<Class<? extends StateDescriptor>> vertex = map.get(clazz);
302        if (vertex == null) {
303            vertex = new GraphVertex<>(clazz);
304            map.put(clazz, vertex);
305        }
306        return vertex;
307    }
308
309    private static <E> List<GraphVertex<E>> topologicalSort(Collection<GraphVertex<E>> vertexes) {
310        List<GraphVertex<E>> res = new ArrayList<>();
311        dfs(vertexes, (vertex) -> res.add(0, vertex), null);
312        return res;
313    }
314
315    private static <E> void dfsVisit(GraphVertex<E> vertex, DfsFinishedVertex<E> dfsFinishedVertex, DfsEdgeFound<E> dfsEdgeFound) {
316        vertex.color = GraphVertex.VertexColor.grey;
317
318        int edgeCount = 0;
319
320        for (GraphVertex<E> successorVertex : vertex.getOutgoingEdges()) {
321            edgeCount++;
322            if (dfsEdgeFound != null) {
323                dfsEdgeFound.onEdgeFound(vertex, successorVertex, edgeCount);
324            }
325            if (successorVertex.color == GraphVertex.VertexColor.white) {
326                dfsVisit(successorVertex, dfsFinishedVertex, dfsEdgeFound);
327            }
328        }
329
330        vertex.color = GraphVertex.VertexColor.black;
331        if (dfsFinishedVertex != null) {
332            dfsFinishedVertex.onVertexFinished(vertex);
333        }
334    }
335
336    private static <E> void dfs(Collection<GraphVertex<E>> vertexes, DfsFinishedVertex<E> dfsFinishedVertex, DfsEdgeFound<E> dfsEdgeFound) {
337        for (GraphVertex<E> vertex : vertexes) {
338            if (vertex.color == GraphVertex.VertexColor.white) {
339                dfsVisit(vertex, dfsFinishedVertex, dfsEdgeFound);
340            }
341        }
342    }
343
344    public static <E> void stateDescriptorGraphToDot(Collection<GraphVertex<StateDescriptor>> vertexes,
345                    PrintWriter dotOut, boolean breakStateName) {
346        dotOut.append("digraph {\n");
347        dfs(vertexes, null, (from, to, edgeId) -> dotOut.append("  \"")
348                        .append(from.element.getFullStateName(breakStateName))
349                        .append("\" -> \"")
350                        .append(to.element.getFullStateName(breakStateName))
351                        // Note that 'dot' requires *double* quotes to enclose the value.
352                        .append("\" [xlabel=\"")
353                        .append(Integer.toString(edgeId))
354                        .append("\"];\n"));
355        dotOut.append("}\n");
356    }
357
358    // TODO: Replace with java.util.function.Consumer<GraphVertex<E>> once Smack minimum Android SDK level is 24 or higher.
359    private interface DfsFinishedVertex<E> {
360        void onVertexFinished(GraphVertex<E> vertex);
361    }
362
363    // TODO: Replace with java.util.function.Consumer<GraphVertex<E>> once Smack minimum Android SDK level is 24 or higher.
364    private interface DfsEdgeFound<E> {
365        void onEdgeFound(GraphVertex<E> from, GraphVertex<E> to, int edgeId);
366    }
367}