001/**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.engine.node.hierarchyrouting;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import org.apache.commons.collections.CollectionUtils;
024import org.apache.commons.collections.Transformer;
025import org.apache.commons.lang.ObjectUtils;
026import org.apache.commons.lang.StringUtils;
027import org.apache.commons.lang.builder.ToStringBuilder;
028import org.apache.log4j.Logger;
029import org.kuali.rice.kew.engine.RouteContext;
030import org.kuali.rice.kew.engine.node.NodeState;
031import org.kuali.rice.kew.engine.node.RouteNode;
032import org.kuali.rice.kew.engine.node.RouteNodeConfigParam;
033import org.kuali.rice.kew.engine.node.RouteNodeInstance;
034import org.kuali.rice.kew.rule.NamedRuleSelector;
035import org.kuali.rice.kew.util.Utilities;
036import org.w3c.dom.Element;
037import org.w3c.dom.NodeList;
038
039
040/**
041 * A simple hierarchy provider that provides hierarchy based on doc content
042 * <pre>
043 * stop id="..." recipient="..." type="..."
044 *     stop ...
045 *     stop ...
046 *       stop ...
047 * </pre>
048 * @author Kuali Rice Team (rice.collab@kuali.org)
049 */
050public class SimpleHierarchyProvider implements HierarchyProvider {
051    private static final Logger LOG = Logger.getLogger(SimpleHierarchyProvider.class);
052
053    /**
054     * Simple implementation of Stop.  Contains stop recipient type,
055     * recipient string, and string id, and maintains pointers to
056     * parent and children stops.
057     */
058    static class SimpleStop implements Stop {
059        private static enum RecipientType { USER, WORKGROUP };
060        public SimpleStop parent;
061        public List<SimpleStop> children = new ArrayList<SimpleStop>();
062        public RecipientType type;
063        public String recipient;
064        public String id;
065
066        public String toString() {
067            return new ToStringBuilder(this).append("id", id)
068                                            .append("recipient", recipient)
069                                            .append("type", type)
070                                            .append("parent", parent == null ? null : parent.id)
071                                            .append("children", StringUtils.join(CollectionUtils.collect(children, new Transformer() {
072                                                        public Object transform(Object o) { return ((SimpleStop) o).id; }
073                                                    })
074                                                    , ','))
075                                            .toString();
076                                                    
077        }
078        
079        public boolean equals(Object o) {
080            if (!(o instanceof SimpleStop)) return false;
081            return id.equals(((SimpleStop) o).id);
082        }
083        
084        public int hashCode() {
085            return ObjectUtils.hashCode(id);
086        }
087    }
088
089    /**
090     * The root stop
091     */
092    private SimpleStop root;
093    /**
094     * Map of Stop id-to-Stop instance
095     */
096    private Map<String, SimpleStop> stops = new HashMap<String, SimpleStop>();
097
098    public void init(RouteNodeInstance nodeInstance, RouteContext context) {
099        init(context.getDocumentContent().getDocument().getDocumentElement());
100    }
101
102    /**
103     * This constructor can be used in tests
104     * @param element the root Element of the hierarchy XML
105     */
106    public void init(Element element) {
107        Element rootStop = findRootStop(element);
108        root = parseStops(rootStop, null);
109    }
110
111    /**
112     * @param e the element at which to start the search
113     * @return the first stop element encountered
114     * @throws RuntimeException if no stop elements were encountered 
115     */
116    protected Element findRootStop(Element e) {
117        if ("stop".equals(e.getNodeName())) return e;
118        NodeList nl = e.getElementsByTagName("stop");
119        if (nl == null || nl.getLength() == 0) {
120            throw new RuntimeException("No stops found");
121        }
122        return (Element) nl.item(0);
123    }
124
125    /**
126     * Parses a hierarchy of stop elements recursively, and populates the stops Map.
127     * @param e a stop element
128     * @param parent the parent of the current element (if any)
129     * @return the SimpleStop instance for the initial element
130     */
131    protected SimpleStop parseStops(Element e, SimpleStop parent) {
132        LOG.debug("parsing element: " + e + " parent: " + parent);
133        SimpleStop stop = parseStop(e);
134        LOG.debug("parsed stop: "+ stop);
135        stop.parent = parent;
136        if (parent != null) {
137            parent.children.add(stop);
138        }
139        stops.put(e.getAttribute("id"), stop);
140        NodeList nl = e.getChildNodes();
141        for (int i = 0; i < nl.getLength(); i++) {
142            org.w3c.dom.Node n = nl.item(i);
143            if (!(n instanceof Element)) continue;
144            parseStops((Element) n, stop);
145        }
146        return stop;
147    }
148
149    /**
150     * Parses stop info from a stop element
151     * @param e the stop element
152     * @return a SimpleStop initialized with attribute/property information
153     */
154    protected SimpleStop parseStop(Element e) {
155        SimpleStop ss = new SimpleStop();
156        String recipient = e.getAttribute("recipient");
157        String type = e.getAttribute("type");
158        String id = e.getAttribute("id");
159        if (id == null) {
160            throw new RuntimeException("malformed document content, missing id attribute: " + e);
161        }
162        /* make optional, since ruleselector/rules can govern this ? */
163        /*
164        if (recipient == null) {
165            throw new RuntimeException("malformed document content, missing recipient attribute: " + e);
166        }
167        if (type == null) {
168            throw new RuntimeException("malformed document content, missing type attribute: " + e);
169        }
170        */
171        ss.id = id;
172        ss.recipient = recipient;
173
174        if (!StringUtils.isEmpty(type)) {
175            SimpleStop.RecipientType rtype = SimpleStop.RecipientType.valueOf(type.toUpperCase());
176            ss.type = rtype;
177        }
178
179        return ss;
180    }
181
182    /* Returns the list of stops in the stops Map which have 0 children */
183    public List<Stop> getLeafStops(RouteContext context) {
184        List<Stop> leafStops = new ArrayList<Stop>();
185        for (SimpleStop stop: stops.values()) {
186            if (stop.children.size() == 0) {
187                leafStops.add(stop);
188            }
189        }
190        return leafStops;
191    }
192
193    /* Looks up a stop in the stops map by identifier */
194    public Stop getStopByIdentifier(String stopId) {
195        return stops.get(stopId);
196    }
197
198    /* Returns the identifier for the specified stop */
199    public String getStopIdentifier(Stop stop) {
200        return ((SimpleStop) stop).id;
201    }
202
203    public boolean hasStop(RouteNodeInstance nodeInstance) {
204        return nodeInstance.getNodeState("id") != null;
205    }
206    
207    public void setStop(RouteNodeInstance requestNodeInstance, Stop stop) {
208        SimpleStop ss = (SimpleStop) stop;
209        requestNodeInstance.addNodeState(new NodeState("id", getStopIdentifier(stop)));
210        //requestNodeInstance.addNodeState(new NodeState(KewApiConstants.RULE_SELECTOR_NODE_STATE_KEY, "named"));
211        //requestNodeInstance.addNodeState(new NodeState(KewApiConstants.RULE_NAME_NODE_STATE_KEY, "NodeInstanceRecipientRule"));
212        if (ss.recipient != null) {
213            requestNodeInstance.addNodeState(new NodeState("recipient", ((SimpleStop) stop).recipient));
214        }
215        if (ss.type != null) {
216            requestNodeInstance.addNodeState(new NodeState("type", ((SimpleStop) stop).type.name().toLowerCase()));
217        }
218    }
219    
220    public boolean equals(Stop a, Stop b) {
221        return ObjectUtils.equals(a, b);
222    }
223
224    public Stop getParent(Stop stop) {
225        return ((SimpleStop) stop).parent;
226    }
227
228    public boolean isRoot(Stop stop) {
229        return equals(stop, root);
230    }
231
232    public Stop getStop(RouteNodeInstance nodeInstance) {
233        NodeState state = nodeInstance.getNodeState("id");
234        if (state == null) {
235            //return null;
236            throw new RuntimeException();
237        } else {
238            LOG.warn("id Node state on nodeinstance " + nodeInstance + ": " + state);
239            return stops.get(state.getValue());
240        }
241    }
242
243    /* Propagates the rule selector and rule name from the hierarchy node to the request node */
244    public void configureRequestNode(RouteNodeInstance hierarchyNodeInstance, RouteNode node) {
245        Map<String, String> cfgMap = Utilities.getKeyValueCollectionAsMap(node.getConfigParams());
246        // propagate rule selector and name from hierarchy node
247        if (!cfgMap.containsKey(RouteNode.RULE_SELECTOR_CFG_KEY)) {
248            Map<String, String> hierarchyCfgMap = Utilities.getKeyValueCollectionAsMap(hierarchyNodeInstance.getRouteNode().getConfigParams());
249            node.getConfigParams().add(new RouteNodeConfigParam(node, RouteNode.RULE_SELECTOR_CFG_KEY, hierarchyCfgMap.get(RouteNode.RULE_SELECTOR_CFG_KEY)));
250        }
251        if (!cfgMap.containsKey(NamedRuleSelector.RULE_NAME_CFG_KEY)) {
252            Map<String, String> hierarchyCfgMap = Utilities.getKeyValueCollectionAsMap(hierarchyNodeInstance.getRouteNode().getConfigParams());
253            node.getConfigParams().add(new RouteNodeConfigParam(node, NamedRuleSelector.RULE_NAME_CFG_KEY, hierarchyCfgMap.get(NamedRuleSelector.RULE_NAME_CFG_KEY)));
254        }
255    }
256}