001    /**
002     * Copyright 2005-2011 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     */
016    package org.kuali.rice.kew.engine.node.hierarchyrouting;
017    
018    import java.util.ArrayList;
019    import java.util.HashMap;
020    import java.util.List;
021    import java.util.Map;
022    
023    import org.apache.commons.collections.CollectionUtils;
024    import org.apache.commons.collections.Transformer;
025    import org.apache.commons.lang.ObjectUtils;
026    import org.apache.commons.lang.StringUtils;
027    import org.apache.commons.lang.builder.ToStringBuilder;
028    import org.apache.log4j.Logger;
029    import org.kuali.rice.kew.engine.RouteContext;
030    import org.kuali.rice.kew.engine.node.NodeState;
031    import org.kuali.rice.kew.engine.node.RouteNode;
032    import org.kuali.rice.kew.engine.node.RouteNodeConfigParam;
033    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
034    import org.kuali.rice.kew.rule.NamedRuleSelector;
035    import org.kuali.rice.kew.util.Utilities;
036    import org.w3c.dom.Element;
037    import 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     */
050    public 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    }