001    /**
002     * Copyright 2005-2012 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.rule.xmlrouting;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.uif.RemotableAttributeErrorContract;
020    import org.kuali.rice.core.api.uif.RemotableAttributeError;
021    import org.kuali.rice.core.api.util.ConcreteKeyValue;
022    import org.kuali.rice.core.api.util.KeyValue;
023    import org.kuali.rice.core.api.util.xml.XmlJotter;
024    import org.kuali.rice.kew.api.KewApiConstants;
025    import org.kuali.rice.kew.api.WorkflowRuntimeException;
026    import org.kuali.rice.kew.api.extension.ExtensionDefinition;
027    import org.kuali.rice.kew.api.rule.RuleExtension;
028    import org.kuali.rice.kew.attribute.XMLAttributeUtils;
029    import org.kuali.rice.kew.exception.WorkflowServiceError;
030    import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
031    import org.kuali.rice.kew.routeheader.DocumentContent;
032    import org.kuali.rice.kew.rule.RuleExtensionBo;
033    import org.kuali.rice.kew.rule.RuleExtensionValue;
034    import org.kuali.rice.kew.rule.WorkflowAttributeXmlValidator;
035    import org.kuali.rice.kns.web.ui.Field;
036    import org.kuali.rice.kns.web.ui.Row;
037    import org.w3c.dom.Element;
038    import org.w3c.dom.NamedNodeMap;
039    import org.w3c.dom.Node;
040    import org.w3c.dom.NodeList;
041    import org.xml.sax.InputSource;
042    
043    import javax.xml.parsers.DocumentBuilderFactory;
044    import javax.xml.xpath.XPath;
045    import javax.xml.xpath.XPathConstants;
046    import javax.xml.xpath.XPathExpressionException;
047    import java.io.BufferedReader;
048    import java.io.StringReader;
049    import java.util.ArrayList;
050    import java.util.HashMap;
051    import java.util.Iterator;
052    import java.util.List;
053    import java.util.Map;
054    import java.util.regex.Pattern;
055    
056    
057    /**
058     * A generic WorkflowAttribute implementation that can be defined completely by XML.
059     * <ol>
060     *   <li>This attribute implementation takes "properties" defined on the the {@link org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition}
061     *       and maps them to the param map of {@link GenericXMLRuleAttribute}, which relate directly to a set of fields defined by the
062     *       XML <code>&lt;routingConfig&gt;</code> configuration.</li>
063     *   <li>Application of the properties defined on the WorkflowAttributeDefinition
064     *       to the actual attribute is performed in  {@link org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver#invokeProperties(Object, java.util.Collection)}</li>
065     *   <li>These params are then used to perform one of either EITHER:
066     *     <ul>
067     *       <li>Replace parameters of the syntax <code>%<i>field name</i>%</code> in the doc content if doc content is
068     *           defined in the <code>&lt;xmlDocumentContent&gt;</code></li>
069     *       <li>Generate a generic doc content, containing the parameter key/value pairs, e.g.:
070     *           <blockquote>
071     *           <code><pre>
072     *             &lt;xmlrouting&gt;
073     *               &lt;field name="color"&gt;&lt;value&gt;red&lt;/value&gt;&lt;/field&gt;
074     *               &lt;field name="shape"&gt;&lt;value&gt;circle&lt;/value&gt;&lt;/field&gt;
075     *             &lt;/xmlrouting&gt;
076     *           </pre></code>
077     *           </blockquote>
078     *       </li>
079     *     </ul>
080     *     Currently, only parameters that match fields configured in the routingConfig are honored (the others are ignored)
081     *     (NOTE: to make this even more reusable we might want to consider generating content for all parameters, even those that
082     *      do not have corresponding fields)
083     *   </li>
084     *   <li>The routingConfig element defines a set of <code>fieldDef</code>s, each of which may have an <code>xpathexpression</code> for field evaluation.
085     *       This <code>xpathexpression</code> is used to determine whether the attribute's {@link #isMatch(DocumentContent, List)} will
086     *       succeed.  Each fieldDef may also have a <code>validation</code> element which supplies a regular expression against which
087     *       to validate the field value (given by the param map)</li>
088     * </ol>
089     *
090     * @author Kuali Rice Team (rice.collab@kuali.org)
091     */
092    public class StandardGenericXMLRuleAttribute implements GenericXMLRuleAttribute, WorkflowAttributeXmlValidator {
093        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLRuleAttribute.class);
094    
095        private static final String FIELD_DEF_E = "fieldDef";
096    
097        private boolean evaluateForMissingExtensions = false;
098    
099        private NodeList getFields(XPath xpath, Element root, String[] types) throws XPathExpressionException {
100            final String OR = " or ";
101            StringBuffer findField = new StringBuffer("//routingConfig/" + FIELD_DEF_E);
102            if (types != null && types.length > 0) {
103                findField.append("[");
104                for (String type : types) {
105                    findField.append("@workflowType='" + type + "'" + OR);
106                    // missing workflowType is equivalent ("defaults") to ALL
107                    if ("ALL".equals(type)) {
108                        findField.append("not(@workflowType)" + OR);
109                    }
110                }
111                if (types.length > 0) {
112                    // remove trailing " or "
113                    findField.setLength(findField.length() - OR.length());
114                }
115                findField.append("]");
116            }
117    
118            try {
119                return (NodeList) xpath.evaluate(findField.toString(), root, XPathConstants.NODESET);
120            } catch (XPathExpressionException e) {
121                LOG.error("Error evaluating expression: '" + findField + "'");
122                throw e;
123            }
124        }
125    
126        private List<Row> getRows(Element root, String[] types) {
127            List<Row> rows = new ArrayList<Row>();
128            XPath xpath = XPathHelper.newXPath();
129            NodeList fieldNodeList;
130            try {
131                fieldNodeList = getFields(xpath, root, types);
132            } catch (XPathExpressionException e) {
133                LOG.error("Error evaluating fields expression");
134                return rows;
135            }
136            if (fieldNodeList != null) {
137                for (int i = 0; i < fieldNodeList.getLength(); i++) {
138                    Node field = fieldNodeList.item(i);
139                    NamedNodeMap fieldAttributes = field.getAttributes();
140    
141                    List<Field> fields = new ArrayList<Field>();
142                    Field myField = new Field(fieldAttributes.getNamedItem("title").getNodeValue(), "", "", false, fieldAttributes.getNamedItem("name").getNodeValue(), "", false, false, null, "");
143                    String quickfinderService = null;
144                    for (int j = 0; j < field.getChildNodes().getLength(); j++) {
145                        Node childNode = field.getChildNodes().item(j);
146                        if ("value".equals(childNode.getNodeName())) {
147                            myField.setPropertyValue(childNode.getFirstChild().getNodeValue());
148                        } else if ("display".equals(childNode.getNodeName())) {
149                            List<KeyValue> options = new ArrayList<KeyValue>();
150                            List<String> selectedOptions = new ArrayList<String>();
151                            for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
152                                Node displayChildNode = childNode.getChildNodes().item(k);
153                                if ("type".equals(displayChildNode.getNodeName())) {
154                                    myField.setFieldType(convertTypeToFieldType(displayChildNode.getFirstChild().getNodeValue()));
155                                } else if ("meta".equals(displayChildNode.getNodeName())) {
156                                    // i don't think the rule creation support things in this node.
157                                    // i don't think the flex Routing report supports things in this node.
158                                } else if ("values".equals(displayChildNode.getNodeName())) {
159                                    NamedNodeMap valuesAttributes = displayChildNode.getAttributes();
160                                    String optionValue = "";
161                                    // if element is empty then child will be null
162                                    Node firstChild = displayChildNode.getFirstChild();
163                                    if (firstChild != null) {
164                                            optionValue = firstChild.getNodeValue();
165                                    }
166                                    if (valuesAttributes.getNamedItem("selected") != null) {
167                                        selectedOptions.add(optionValue);
168                                    }
169                                    String title = "";
170                                    Node titleAttribute = valuesAttributes.getNamedItem("title");
171                                    if (titleAttribute != null) {
172                                            title = titleAttribute.getNodeValue();
173                                    }
174                                    options.add(new ConcreteKeyValue(optionValue, title));
175                                }
176                            }
177                            if (!options.isEmpty()) {
178                                myField.setFieldValidValues(options);
179                                if (!selectedOptions.isEmpty()) {
180                                    //if (Field.MULTI_VALUE_FIELD_TYPES.contains(myField.getFieldType())) {
181                                    //    String[] newSelectedOptions = new String[selectedOptions.size()];
182                                    //    int k = 0;
183                                    //    for (Iterator iter = selectedOptions.iterator(); iter.hasNext();) {
184                                    //        String option = (String) iter.next();
185                                    //        newSelectedOptions[k] = option;
186                                    //        k++;
187                                    //    }
188                                    //    myField.setPropertyValues(newSelectedOptions);
189                                    //} else {
190                                    //
191                                        myField.setPropertyValue((String)selectedOptions.get(0));
192                                    //}
193                                }
194                            }
195                        } else if ("lookup".equals(childNode.getNodeName())) {
196                                                    XMLAttributeUtils.establishFieldLookup(myField, childNode);
197                                            } 
198                    }
199                    fields.add(myField);
200                    rows.add(new Row(fields));
201                }
202            }
203            return rows;
204        }
205    
206        private static String convertTypeToFieldType(String type) {
207            if ("text".equals(type)) {
208                return Field.TEXT;
209            } else if ("select".equals(type)) {
210                return Field.DROPDOWN;
211            } else if ("radio".equals(type)) {
212                return Field.RADIO;
213            } else if ("quickfinder".equals(type)) {
214                return Field.QUICKFINDER;
215            }
216            return null;
217        }
218    
219        // thin interface for generating the appropriate error type
220        private static interface ErrorGenerator<T> {
221            T generateInvalidFieldError(Node field, String fieldName, String message);
222            T generateMissingFieldError(Node field, String fieldName, String message);
223        }
224    
225        private ExtensionDefinition extensionDefinition;
226        private Map paramMap = new HashMap();
227        private List ruleRows = new ArrayList();
228        private List routingDataRows = new ArrayList();
229        private boolean required;
230    
231        public StandardGenericXMLRuleAttribute() {
232        }
233    
234        public void setExtensionDefinition(ExtensionDefinition extensionDefinition) {
235            this.extensionDefinition = extensionDefinition;
236        }
237    
238    //    public boolean isMatch(DocumentContent docContent, List ruleExtensions) {
239    //        XPath xpath = XPathHelper.newXPath(docContent.getDocument());
240    //        WorkflowFunctionResolver resolver = XPathHelper.extractFunctionResolver(xpath);
241    //        for (Iterator iter = ruleExtensions.iterator(); iter.hasNext();) {
242    //            RuleExtension extension = (RuleExtension) iter.next();
243    //            if (extension.getRuleTemplateAttribute().getRuleAttribute().getName().equals(ruleAttribute.getName())) {
244    //                List extensions = new ArrayList();
245    //                extensions.add(extension);
246    //                resolver.setRuleExtensions(extensions);
247    //                //xpath.setXPathFunctionResolver(resolver);
248    //                for (Iterator iterator = extension.getExtensionValues().iterator(); iterator.hasNext();) {
249    //                    RuleExtensionValue value = (RuleExtensionValue) iterator.next();
250    //                    String findXpathExpression = "//routingConfig/" + FIELD_DEF_E + "[@name='" + value.getKey() + "']/fieldEvaluation/xpathexpression";
251    //                    String xpathExpression = null;
252    //                    try {
253    //                        xpathExpression = (String) xpath.evaluate(findXpathExpression, getConfigXML(), XPathConstants.STRING);
254    //                        LOG.debug("routingConfig XPath expression: " + xpathExpression);
255    //                        if (!org.apache.commons.lang.StringUtils.isEmpty(xpathExpression)) {
256    //                            LOG.debug("DocContent: " + docContent.getDocContent());
257    //                            Boolean match = (Boolean) xpath.evaluate(xpathExpression, docContent.getDocument(), XPathConstants.BOOLEAN);
258    //                            LOG.debug("routingConfig match? " + match);
259    //                            if (match != null && !match.booleanValue()) {
260    //                                return false;
261    //                            }
262    //                        }
263    //                    } catch (XPathExpressionException e) {
264    //                        LOG.error("error in isMatch ", e);
265    //                        throw new RuntimeException("Error trying to find xml content with xpath expressions: " + findXpathExpression + " or " + xpathExpression, e);
266    //                    }
267    //                }
268    //                resolver.setRuleExtensions(null);
269    //            }
270    //        }
271    //        String findXpathExpression = "//routingConfig/globalEvaluations/xpathexpression";
272    //        String xpathExpression = "";
273    //        try {
274    //            NodeList xpathExpressions = (NodeList) xpath.evaluate(findXpathExpression, getConfigXML(), XPathConstants.NODESET);
275    //            for (int i = 0; i < xpathExpressions.getLength(); i++) {
276    //                Node xpathNode = xpathExpressions.item(i);
277    //                xpathExpression = xpathNode.getFirstChild().getNodeValue();
278    //                LOG.debug("global XPath expression: " + xpathExpression);
279    //                if (!org.apache.commons.lang.StringUtils.isEmpty(xpathExpression)) {
280    //                    LOG.debug("DocContent: " + docContent.getDocContent());
281    //                    Boolean match = (Boolean) xpath.evaluate(xpathExpression, docContent.getDocument(), XPathConstants.BOOLEAN);
282    //                    LOG.debug("Global match? " + match);
283    //                    if (match != null && !match.booleanValue()) {
284    //                        return false;
285    //                    }
286    //                }
287    //            }
288    //        } catch (XPathExpressionException e) {
289    //            LOG.error("error in isMatch ", e);
290    //            throw new RuntimeException("Error trying to find xml content with xpath expressions: " + findXpathExpression, e);
291    //        }
292    //        return true;
293    //    }
294    
295        public boolean isMatch(DocumentContent docContent, List<RuleExtension> ruleExtensions) {
296            XPath xpath = XPathHelper.newXPath(docContent.getDocument());
297            WorkflowFunctionResolver resolver = XPathHelper.extractFunctionResolver(xpath);
298            resolver.setRuleExtensions(ruleExtensions);
299            List<String> xPathExpressionsToEvaluate = extractExpressionsToEvaluate(xpath, docContent, ruleExtensions);
300            for (String xPathExpressionToEvaluate : xPathExpressionsToEvaluate) {
301                if (LOG.isDebugEnabled()) {
302                    LOG.debug("Evaluating xPath expression: " + xPathExpressionToEvaluate);
303                }
304                try {
305                    Boolean match = (Boolean) xpath.evaluate(xPathExpressionToEvaluate, docContent.getDocument(), XPathConstants.BOOLEAN);
306                    if (LOG.isDebugEnabled()) {
307                        LOG.debug("Expression match result: " + match);
308                    }
309                    if (match != null && !match.booleanValue()) {
310                        return false;
311                    }
312                } catch (XPathExpressionException e) {
313                    LOG.error("Error in isMatch ", e);
314                    throw new RuntimeException("Error trying to evalute xml content with xpath expression: " + xPathExpressionToEvaluate, e);
315                }
316            }
317            return true;
318        }
319    
320        /**
321         * Extracts the xPath expressions that should be evaluated in order to determine whether or not the rule matches.  THis should take
322         * into account the value of evaluateForMissingExtensions.
323         */
324        protected List<String> extractExpressionsToEvaluate(XPath xpath, DocumentContent docContent, List<RuleExtension> ruleExtensions) {
325            List<String> expressionsToEvaluate = new ArrayList<String>(ruleExtensions.size() + 1);
326            Element configXml = getConfigXML();
327            String findFieldExpressions = "//routingConfig/" + FIELD_DEF_E + "/fieldEvaluation/xpathexpression";
328            try {
329                NodeList xPathExpressions = (NodeList) xpath.evaluate(findFieldExpressions, configXml, XPathConstants.NODESET);
330                for (int index = 0; index < xPathExpressions.getLength(); index++) {
331                    Element expressionElement = (Element) xPathExpressions.item(index);
332                    String expression = expressionElement.getTextContent();
333                    if (!isEvaluateForMissingExtensions()) {
334                        Node parentNode = expressionElement.getParentNode().getParentNode();
335                        Node fieldAttribute = parentNode.getAttributes().getNamedItem("name");
336                        if (fieldAttribute == null || StringUtils.isEmpty(fieldAttribute.getNodeValue())) {
337                            throw new WorkflowRuntimeException("Could not determine field name defined on fieldDef for xpath expression: " + expression);
338                        }
339                        String fieldName = fieldAttribute.getNodeValue();
340                        boolean foundExtension = false;
341                        outer:for (RuleExtension ruleExtension : ruleExtensions) {
342                            if (ruleExtension.getRuleTemplateAttribute().getRuleAttribute().getName().equals(extensionDefinition.getName())) {
343                                for (String ruleExtensionValueKey : ruleExtension.getExtensionValuesMap().keySet()) {
344                                    if (fieldName.equals(ruleExtensionValueKey)) {
345                                        foundExtension = true;
346                                        break outer;
347                                    }
348                                }
349                            }
350                        }
351                        if (!foundExtension) {
352                            // if the rule does not have an extension value for the xpath expression on the corresponding field def, let's skip it
353                            continue;
354                        }
355                    }
356    
357                    if (!StringUtils.isEmpty(expression)) {
358                        if (LOG.isDebugEnabled()) {
359                            LOG.debug("Adding routingConfig XPath expression: " + expression);
360                        }
361                        expressionsToEvaluate.add(expression);
362                    }
363                }
364            } catch (XPathExpressionException e) {
365                throw new WorkflowRuntimeException("Failed to evalute XPath expression for fieldDefs: " + findFieldExpressions);
366            }
367            String findGlobalExpressions = "//routingConfig/globalEvaluations/xpathexpression";
368            try {
369                NodeList xPathExpressions = (NodeList) xpath.evaluate(findGlobalExpressions, configXml, XPathConstants.NODESET);
370                for (int index = 0; index < xPathExpressions.getLength(); index++) {
371                    Element expressionElement = (Element) xPathExpressions.item(index);
372                    //String expression = XmlJotter.jotNode(expressionElement);
373                    String expression = expressionElement.getTextContent();
374                    if (!StringUtils.isEmpty(expression)) {
375                        if (LOG.isDebugEnabled()) {
376                            LOG.debug("Adding global XPath expression: " + expression);
377                        }
378                        expressionsToEvaluate.add(expression);
379                    }
380                }
381            } catch (XPathExpressionException e) {
382                throw new WorkflowRuntimeException("Failed to evalute global XPath expression: " + findGlobalExpressions);
383            }
384            return expressionsToEvaluate;
385        }
386    
387        public List getRuleRows() {
388            if (ruleRows.isEmpty()) {
389                ruleRows = getRows(getConfigXML(), new String[] { "ALL", "RULE" });
390            }
391            return ruleRows;
392        }
393    
394        private String getValidationErrorMessage(XPath xpath, Element root, String fieldName) throws XPathExpressionException {
395            String findErrorMessage = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/message";
396            return (String) xpath.evaluate(findErrorMessage, root, XPathConstants.STRING);
397        }
398    
399        /**
400         * Performs attribute validation producing a list of errors of the parameterized type T generated by the ErrorGenerator<T>
401         * @throws XPathExpressionException
402         */
403        private <T> List<T> validate(Element root, String[] types, Map map, ErrorGenerator<T> errorGenerator) throws XPathExpressionException {
404            List<T> errors = new ArrayList();
405            XPath xpath = XPathHelper.newXPath();
406    
407            NodeList nodes = getFields(xpath, root, types);
408            for (int i = 0; i < nodes.getLength(); i++) {
409                Node field = nodes.item(i);
410                NamedNodeMap fieldAttributes = field.getAttributes();
411                String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
412    
413                LOG.debug("evaluating field: " + fieldName);
414                String findValidation = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation";
415    
416                Node validationNode = (Node) xpath.evaluate(findValidation, root, XPathConstants.NODE);
417                boolean fieldIsRequired = false;
418                if (validationNode != null) {
419                    NamedNodeMap validationAttributes = validationNode.getAttributes();
420                    Node reqAttribNode = validationAttributes.getNamedItem("required");
421                    fieldIsRequired = reqAttribNode != null && "true".equalsIgnoreCase(reqAttribNode.getNodeValue());
422                }
423    
424                String findRegex = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/regex";
425    
426                String regex = null;
427                Node regexNode = (Node) xpath.evaluate(findRegex, root, XPathConstants.NODE);
428    
429                if (regexNode != null && regexNode.getFirstChild() != null) {
430                    regex = regexNode.getFirstChild().getNodeValue();
431                    if (regex == null) {
432                        throw new RuntimeException("Null regex text node");
433                    }
434                }/* else {
435                    if (fieldIsRequired) {
436                        fieldIsOnlyRequired = true;
437                        LOG.debug("Setting empty regex to .+ as field is required");
438                        // NOTE: ok, so technically .+ is not the same as checking merely
439                        // for existence, because a field can be extant but "empty"
440                        // however this has no relevance to the user as an empty field
441                        // is for all intents and purposes non-existent (not-filled-in)
442                        // so let's just use this regex to simplify the logic and
443                        // pass everything through a regex check
444                        regex = ".+";
445                    } else {
446                        LOG.debug("Setting empty regex to .* as field is NOT required");
447                        regex = ".*";
448                    }
449                }*/
450    
451                LOG.debug("regex for field '" + fieldName + "': '" + regex + "'");
452    
453                String fieldValue = null;
454                if (map != null) {
455                    fieldValue = (String) map.get(fieldName);
456                }
457    
458                LOG.debug("field value: " + fieldValue);
459    
460                // fix up non-existent value for regex purposes only
461                if (fieldValue == null) {
462                    fieldValue = "";
463                }
464    
465                if (regex == null){
466                    if (fieldIsRequired) {
467                        if (fieldValue.length() == 0) {
468                            errors.add(errorGenerator.generateMissingFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
469                        }
470                    }
471                } else {
472                    if (!Pattern.compile(regex).matcher(fieldValue).matches()) {
473                        LOG.debug("field value does not match validation regex");
474                        errors.add(errorGenerator.generateInvalidFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
475                    }
476                }
477            }
478            return errors;
479        }
480    
481        public List getRoutingDataRows() {
482            if (routingDataRows.isEmpty()) {
483                routingDataRows = getRows(getConfigXML(), new String[] { "ALL", "REPORT" });
484            }
485            return routingDataRows;
486        }
487    
488        public String getDocContent() {
489            XPath xpath = XPathHelper.newXPath();
490            final String findDocContent = "//routingConfig/xmlDocumentContent";
491            try {
492                Node xmlDocumentContent = (Node) xpath.evaluate(findDocContent, getConfigXML(), XPathConstants.NODE);
493    
494                NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "REPORT", "RULE" });
495    //            if (nodes == null || nodes.getLength() == 0) {
496    //                return "";
497    //            }
498    
499                if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
500                    // Custom doc content in the routingConfig xml.
501                    String documentContent = "";
502                    NodeList customNodes = xmlDocumentContent.getChildNodes();
503                    for (int i = 0; i < customNodes.getLength(); i++) {
504                        Node childNode = customNodes.item(i);
505                        documentContent += XmlJotter.jotNode(childNode);
506                    }
507    
508                    for (int i = 0; i < nodes.getLength(); i++) {
509                        Node field = nodes.item(i);
510                        NamedNodeMap fieldAttributes = field.getAttributes();
511                        String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
512                        LOG.debug("Replacing field '" + fieldName + "'");
513                        Map map = getParamMap();
514                        String fieldValue = (String) map.get(fieldName);
515                        if (map != null && !org.apache.commons.lang.StringUtils.isEmpty(fieldValue)) {
516                            LOG.debug("Replacing %" + fieldName + "% with field value: '" + fieldValue + "'");
517                            documentContent = documentContent.replaceAll("%" + fieldName + "%", fieldValue);
518                        } else {
519                            LOG.debug("Field map is null or fieldValue is empty");
520                        }
521                    }
522                    return documentContent;
523                } else {
524                    // Standard doc content if no doc content is found in the routingConfig xml.
525                    StringBuffer documentContent = new StringBuffer("<xmlRouting>");
526                    for (int i = 0; i < nodes.getLength(); i++) {
527                        Node field = nodes.item(i);
528                        NamedNodeMap fieldAttributes = field.getAttributes();
529                        String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
530                        Map map = getParamMap();
531                        if (map != null && !org.apache.commons.lang.StringUtils.isEmpty((String) map.get(fieldName))) {
532                            documentContent.append("<field name=\"");
533                            documentContent.append(fieldName);
534                            documentContent.append("\"><value>");
535                            documentContent.append((String) map.get(fieldName));
536                            documentContent.append("</value></field>");
537                        }
538                    }
539                    documentContent.append("</xmlRouting>");
540                    return documentContent.toString();
541                }
542            } catch (XPathExpressionException e) {
543                LOG.error("error in getDocContent ", e);
544                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
545            } catch (Exception e) {
546                LOG.error("error in getDocContent attempting to find xml doc content", e);
547                throw new RuntimeException("Error trying to get xml doc content.", e);
548            }
549        }
550    
551        public List getRuleExtensionValues() {
552            List extensionValues = new ArrayList();
553    
554            XPath xpath = XPathHelper.newXPath();
555            try {
556                NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "RULE" });
557                for (int i = 0; i < nodes.getLength(); i++) {
558                    Node field = nodes.item(i);
559                    NamedNodeMap fieldAttributes = field.getAttributes();
560                    String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
561                    Map map = getParamMap();
562                    if (map != null && !org.apache.commons.lang.StringUtils.isEmpty((String) map.get(fieldName))) {
563                        RuleExtensionValue value = new RuleExtensionValue();
564                        value.setKey(fieldName);
565                        value.setValue((String) map.get(fieldName));
566                        extensionValues.add(value);
567                    }
568                }
569            } catch (XPathExpressionException e) {
570                LOG.error("error in getRuleExtensionValues ", e);
571                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
572            }
573            return extensionValues;
574        }
575    
576        public List<RemotableAttributeError> validateRoutingData(Map paramMap) {
577            this.paramMap = paramMap;
578            try {
579                return validate(getConfigXML(), new String[] { "ALL", "REPORT" }, paramMap, new ErrorGenerator<RemotableAttributeError>() {
580                    public RemotableAttributeError generateInvalidFieldError(Node field, String fieldName, String message) {
581                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.error", message).build();
582                    }
583                    public RemotableAttributeError generateMissingFieldError(Node field, String fieldName, String message) {
584                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue()).build();
585                    }
586                });
587            } catch (XPathExpressionException e) {
588                LOG.error("error in validateRoutingData ", e);
589                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
590            }
591        }
592    
593        public List<WorkflowServiceError> validateRuleData(Map paramMap) {
594            this.paramMap = paramMap;
595            try {
596                return validate(getConfigXML(), new String[] { "ALL", "RULE" }, paramMap, new ErrorGenerator<WorkflowServiceError>() {
597                    public WorkflowServiceError generateInvalidFieldError(Node field, String fieldName, String message) {
598                        return new WorkflowServiceErrorImpl("Xml attribute error.", "routetemplate.xmlattribute.error", message);
599                    }
600                    public WorkflowServiceError generateMissingFieldError(Node field, String fieldName, String message) {
601                        return new WorkflowServiceErrorImpl("Xml attribute error.", "routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue());
602                    }
603                });
604            } catch (XPathExpressionException e) {
605                LOG.error("error in validateRoutingData ", e);
606                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
607            }
608        }
609    
610        public void setRequired(boolean required) {
611            this.required = required;
612        }
613    
614        public boolean isRequired() {
615            return required;
616        }
617    
618        public Element getConfigXML() {
619            try {
620                return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(
621                        new StringReader(extensionDefinition.getConfiguration().get(
622                                KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA))))).getDocumentElement();
623            } catch (Exception e) {
624                String str = extensionDefinition == null ? "null" : extensionDefinition.getName();
625                LOG.error("error parsing xml data from rule attribute: " + str, e);
626                throw new RuntimeException("error parsing xml data from rule attribute: " + str, e);
627            }
628        }
629    
630        // TODO: possibly simplify even further by unifying AttributeError and WorkflowServiceError
631        public List<? extends RemotableAttributeErrorContract> validateClientRoutingData() {
632            LOG.debug("validating client routing data");
633            try {
634                return validate(getConfigXML(), new String[] { "ALL", "RULE" }, getParamMap(), new ErrorGenerator<RemotableAttributeError>() {
635                    public RemotableAttributeError generateInvalidFieldError(Node field, String fieldName, String message) {
636                        if (org.apache.commons.lang.StringUtils.isEmpty(message)) {
637                            message = "invalid field value";
638                        } else {
639                            LOG.info("Message: '" + message + "'");
640                        }
641                        return RemotableAttributeError.Builder.create(fieldName, message).build();
642                    }
643                    public RemotableAttributeError generateMissingFieldError(Node field, String fieldName, String message) {
644                        return RemotableAttributeError.Builder.create(fieldName, "Attribute is required; " + message).build();
645                    }
646                });
647            } catch (XPathExpressionException e) {
648                LOG.error("error in validateClientRoutingData ", e);
649                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
650            }
651        }
652    
653        public Map getParamMap() {
654            return paramMap;
655        }
656    
657        public void setParamMap(Map paramMap) {
658            this.paramMap = paramMap;
659        }
660    
661        /**
662         * @return the evaluateForMissingExtensions
663         */
664        public boolean isEvaluateForMissingExtensions() {
665            return this.evaluateForMissingExtensions;
666        }
667    
668        /**
669         * Sets whether or not to evaluate expressions if the extension corresponding to that expressions is not present on the rule.
670         * The correspondence is made by comparing the name of the field declared on the fieldDef element and the name of the
671         * rule extension key.  If this value is set to true then all xpath expressions defined on all fieldDefs will be evaluated
672         * regardless of whether or not the rule has a corresponding extension value.
673         *
674         * <p>By default this is false to preserve backward compatible behavior.
675         */
676        public void setEvaluateForMissingExtensions(boolean evaluateForMissingExtensions) {
677            this.evaluateForMissingExtensions = evaluateForMissingExtensions;
678        }
679    
680    
681    }