001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.platform.routing.core.impl;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Deque;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.lang.builder.ToStringBuilder;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentModelList;
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.PropertyException;
039import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants;
040import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService;
041import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException;
042import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.State;
043import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.Transition;
044import org.nuxeo.runtime.api.Framework;
045
046/**
047 * @since 5.6
048 */
049public class GraphRouteImpl extends DocumentRouteImpl implements GraphRoute {
050
051    private static final long serialVersionUID = 1L;
052
053    /** To be used through getter. */
054    protected List<GraphNode> nodes;
055
056    /** To be used through getter. */
057    protected Map<String, GraphNode> nodesById;
058
059    public GraphRouteImpl(DocumentModel doc) {
060        super(doc, new GraphRunner());
061    }
062
063    @Override
064    public String toString() {
065        return new ToStringBuilder(this).append(getName()).toString();
066    }
067
068    @Override
069    public Collection<GraphNode> getNodes() {
070        if (nodes == null) {
071            compute();
072        }
073        return nodes;
074    }
075
076    protected void compute() {
077        String startNodeId = computeNodes();
078        computeTransitions();
079        computeLoopTransitions(startNodeId);
080    }
081
082    protected String computeNodes() {
083        CoreSession session = document.getCoreSession();
084        DocumentModelList children = session.getChildren(document.getRef());
085        nodes = new ArrayList<GraphNode>(children.size());
086        nodesById = new HashMap<String, GraphNode>();
087        String startNodeId = null;
088        for (DocumentModel doc : children) {
089            // TODO use adapters
090            if (doc.getType().equals("RouteNode")) {
091                GraphNode node = new GraphNodeImpl(doc, this);
092                String id = node.getId();
093                if (nodesById.put(id, node) != null) {
094                    throw new DocumentRouteException("Duplicate nodes with id: " + id);
095                }
096                nodes.add(node);
097                if (node.isStart()) {
098                    if (startNodeId != null) {
099                        throw new DocumentRouteException("Duplicate start nodes: " + startNodeId + " and " + id);
100                    }
101                    startNodeId = id;
102                }
103            }
104        }
105        return startNodeId;
106    }
107
108    /**
109     * Deduce input transitions from output transitions.
110     */
111    protected void computeTransitions() throws DocumentRouteException {
112        for (GraphNode node : nodes) {
113            List<Transition> tt = node.getOutputTransitions();
114            for (Transition t : tt) {
115                GraphNode target = getNode(t.target);
116                target.initAddInputTransition(t);
117            }
118        }
119    }
120
121    /**
122     * Finds which transitions are re-looping (feedback arc set).
123     */
124    protected void computeLoopTransitions(String startNodeId) throws DocumentRouteException {
125        if (startNodeId == null) {
126            // incomplete graph
127            return;
128        }
129        /*
130         * Depth-first search. In the todo stack, each element records a list of the siblings left to visit at that
131         * depth. After visiting the last sibling, we go back to the parent and at this point mark it as visited in
132         * post-traversal order.
133         */
134        List<String> postOrder = new LinkedList<String>();
135        Deque<Deque<String>> stack = new LinkedList<Deque<String>>();
136        Deque<String> first = new LinkedList<String>();
137        first.add(startNodeId);
138        stack.push(first);
139        Set<String> done = new HashSet<String>();
140        for (;;) {
141            // find next sibling
142            String nodeId = stack.peek().peek();
143            if (nodeId == null) {
144                // last sibling done
145                // go back up one level and mark post-traversal order
146                stack.pop(); // pop empty children
147                if (stack.isEmpty()) {
148                    // we are done
149                    break;
150                }
151                nodeId = stack.peek().pop(); // pop parent
152                postOrder.add(nodeId); // mark post-traversal order
153            } else if (done.add(nodeId)) {
154                // traverse the next sibling
155                Deque<String> children = new LinkedList<String>();
156                for (Transition t : getNode(nodeId).getOutputTransitions()) {
157                    children.add(t.target);
158                }
159                // add children to stack and recurse
160                stack.push(children);
161            } else {
162                // already traversed
163                stack.peek().pop(); // skip it
164            }
165        }
166
167        // reverse the post-order to find the topological ordering
168        Collections.reverse(postOrder);
169        Map<String, Integer> ordering = new HashMap<String, Integer>();
170        int i = 1;
171        for (String nodeId : postOrder) {
172            ordering.put(nodeId, Integer.valueOf(i++));
173        }
174
175        // walk the graph and all transitions again
176        // and mark as looping the transitions pointing to a node
177        // with a smaller order that the source
178        done.clear();
179        Deque<String> todo = new LinkedList<String>();
180        todo.add(startNodeId);
181        while (!todo.isEmpty()) {
182            String nodeId = todo.pop();
183            if (done.add(nodeId)) {
184                int source = ordering.get(nodeId).intValue();
185                for (Transition t : getNode(nodeId).getOutputTransitions()) {
186                    todo.push(t.target);
187                    // compare orders to detected feeback arcs
188                    int target = ordering.get(t.target).intValue();
189                    if (target <= source) {
190                        t.loop = true;
191                    }
192                }
193            }
194        }
195    }
196
197    @Override
198    public GraphNode getStartNode() throws DocumentRouteException {
199        for (GraphNode node : getNodes()) {
200            if (node.isStart()) {
201                return node;
202            }
203        }
204        throw new DocumentRouteException("No start node for graph: " + getName());
205    }
206
207    @Override
208    public GraphNode getNode(String id) {
209        getNodes(); // compute
210        GraphNode node = nodesById.get(id);
211        if (node != null) {
212            return node;
213        }
214        throw new IllegalArgumentException("No node with id: " + id + " in graph: " + this);
215    }
216
217    @Override
218    public Map<String, Serializable> getVariables() {
219        return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET);
220    }
221
222    @Override
223    public Map<String, Serializable> getJsonVariables() {
224        return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET, true);
225    }
226
227    @Override
228    public void setVariables(Map<String, Serializable> map) {
229        GraphVariablesUtil.setVariables(document, PROP_VARIABLES_FACET, map);
230    }
231
232    @Override
233    public void setJSONVariables(Map<String, String> map) {
234        GraphVariablesUtil.setJSONVariables(document, PROP_VARIABLES_FACET, map);
235    }
236
237    @Override
238    public DocumentModelList getAttachedDocumentModels() {
239        @SuppressWarnings("unchecked")
240        List<String> ids = (List<String>) document.getPropertyValue(
241                DocumentRoutingConstants.ATTACHED_DOCUMENTS_PROPERTY_NAME);
242        ArrayList<DocumentRef> docRefs = new ArrayList<DocumentRef>();
243        for (String id : ids) {
244            docRefs.add(new IdRef(id));
245        }
246        return document.getCoreSession().getDocuments(docRefs.toArray(new DocumentRef[0]));
247    }
248
249    @Override
250    public String getAvailabilityFilter() {
251        try {
252            return (String) document.getPropertyValue(PROP_AVAILABILITY_FILTER);
253        } catch (PropertyException e) {
254            return null;
255        }
256    }
257
258    @Override
259    public boolean hasParentRoute() {
260        String parentRouteInstanceId = (String) document.getPropertyValue(PROP_PARENT_ROUTE);
261        return !StringUtils.isEmpty(parentRouteInstanceId);
262    }
263
264    @Override
265    public void resumeParentRoute(CoreSession session) {
266        DocumentRoutingService routing = Framework.getLocalService(DocumentRoutingService.class);
267        String parentRouteInstanceId = (String) document.getPropertyValue(PROP_PARENT_ROUTE);
268        String parentRouteNodeId = (String) document.getPropertyValue(PROP_PARENT_NODE);
269        routing.resumeInstance(parentRouteInstanceId, parentRouteNodeId, null, null, session);
270    }
271
272    @Override
273    public List<GraphNode> getSuspendedNodes() {
274        List<GraphNode> result = new ArrayList<GraphNode>();
275        for (GraphNode node : getNodes()) {
276            if (State.SUSPENDED.equals(node.getState())) {
277                result.add(node);
278            }
279        }
280        return result;
281    }
282
283}