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 = null;
297            String xPathCacheKey = "xPath" + docContent.getRouteContext().getNodeInstance().getRouteNodeInstanceId() + "-" + docContent.getRouteContext().getNodeInstance().getName();
298            if(docContent.getRouteContext().getParameters().containsKey(xPathCacheKey)) {
299                    xpath = (XPath)docContent.getRouteContext().getParameters().get(xPathCacheKey);
300            } else {
301                    xpath = XPathHelper.newXPath(docContent.getDocument());
302                    docContent.getRouteContext().getParameters().put(xPathCacheKey, xpath);
303            }
304            WorkflowFunctionResolver resolver = XPathHelper.extractFunctionResolver(xpath);
305            resolver.setRuleExtensions(ruleExtensions);
306            List<String> xPathExpressionsToEvaluate = extractExpressionsToEvaluate(xpath, docContent, ruleExtensions);
307            for (String xPathExpressionToEvaluate : xPathExpressionsToEvaluate) {
308                if (LOG.isDebugEnabled()) {
309                    LOG.debug("Evaluating xPath expression: " + xPathExpressionToEvaluate);
310                }
311                try {
312                    Boolean match = (Boolean) xpath.evaluate(xPathExpressionToEvaluate, docContent.getDocument(), XPathConstants.BOOLEAN);
313                    if (LOG.isDebugEnabled()) {
314                        LOG.debug("Expression match result: " + match);
315                    }
316                    if (match != null && !match.booleanValue()) {
317                        return false;
318                    }
319                } catch (XPathExpressionException e) {
320                    LOG.error("Error in isMatch ", e);
321                    throw new RuntimeException("Error trying to evalute xml content with xpath expression: " + xPathExpressionToEvaluate, e);
322                }
323            }
324            return true;
325        }
326    
327        /**
328         * Extracts the xPath expressions that should be evaluated in order to determine whether or not the rule matches.  THis should take
329         * into account the value of evaluateForMissingExtensions.
330         */
331        protected List<String> extractExpressionsToEvaluate(XPath xpath, DocumentContent docContent, List<RuleExtension> ruleExtensions) {
332            List<String> expressionsToEvaluate = new ArrayList<String>(ruleExtensions.size() + 1);
333            Element configXml = getConfigXML();
334            String findFieldExpressions = "//routingConfig/" + FIELD_DEF_E + "/fieldEvaluation/xpathexpression";
335            try {
336                NodeList xPathExpressions = (NodeList) xpath.evaluate(findFieldExpressions, configXml, XPathConstants.NODESET);
337                for (int index = 0; index < xPathExpressions.getLength(); index++) {
338                    Element expressionElement = (Element) xPathExpressions.item(index);
339                    String expression = expressionElement.getTextContent();
340                    if (!isEvaluateForMissingExtensions()) {
341                        Node parentNode = expressionElement.getParentNode().getParentNode();
342                        Node fieldAttribute = parentNode.getAttributes().getNamedItem("name");
343                        if (fieldAttribute == null || StringUtils.isEmpty(fieldAttribute.getNodeValue())) {
344                            throw new WorkflowRuntimeException("Could not determine field name defined on fieldDef for xpath expression: " + expression);
345                        }
346                        String fieldName = fieldAttribute.getNodeValue();
347                        boolean foundExtension = false;
348                        outer:for (RuleExtension ruleExtension : ruleExtensions) {
349                            if (ruleExtension.getRuleTemplateAttribute().getRuleAttribute().getName().equals(extensionDefinition.getName())) {
350                                for (String ruleExtensionValueKey : ruleExtension.getExtensionValuesMap().keySet()) {
351                                    if (fieldName.equals(ruleExtensionValueKey)) {
352                                        foundExtension = true;
353                                        break outer;
354                                    }
355                                }
356                            }
357                        }
358                        if (!foundExtension) {
359                            // if the rule does not have an extension value for the xpath expression on the corresponding field def, let's skip it
360                            continue;
361                        }
362                    }
363    
364                    if (!StringUtils.isEmpty(expression)) {
365                        if (LOG.isDebugEnabled()) {
366                            LOG.debug("Adding routingConfig XPath expression: " + expression);
367                        }
368                        expressionsToEvaluate.add(expression);
369                    }
370                }
371            } catch (XPathExpressionException e) {
372                throw new WorkflowRuntimeException("Failed to evalute XPath expression for fieldDefs: " + findFieldExpressions);
373            }
374            String findGlobalExpressions = "//routingConfig/globalEvaluations/xpathexpression";
375            try {
376                NodeList xPathExpressions = (NodeList) xpath.evaluate(findGlobalExpressions, configXml, XPathConstants.NODESET);
377                for (int index = 0; index < xPathExpressions.getLength(); index++) {
378                    Element expressionElement = (Element) xPathExpressions.item(index);
379                    //String expression = XmlJotter.jotNode(expressionElement);
380                    String expression = expressionElement.getTextContent();
381                    if (!StringUtils.isEmpty(expression)) {
382                        if (LOG.isDebugEnabled()) {
383                            LOG.debug("Adding global XPath expression: " + expression);
384                        }
385                        expressionsToEvaluate.add(expression);
386                    }
387                }
388            } catch (XPathExpressionException e) {
389                throw new WorkflowRuntimeException("Failed to evalute global XPath expression: " + findGlobalExpressions);
390            }
391            return expressionsToEvaluate;
392        }
393    
394        public List getRuleRows() {
395            if (ruleRows.isEmpty()) {
396                ruleRows = getRows(getConfigXML(), new String[] { "ALL", "RULE" });
397            }
398            return ruleRows;
399        }
400    
401        private String getValidationErrorMessage(XPath xpath, Element root, String fieldName) throws XPathExpressionException {
402            String findErrorMessage = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/message";
403            return (String) xpath.evaluate(findErrorMessage, root, XPathConstants.STRING);
404        }
405    
406        /**
407         * Performs attribute validation producing a list of errors of the parameterized type T generated by the ErrorGenerator<T>
408         * @throws XPathExpressionException
409         */
410        private <T> List<T> validate(Element root, String[] types, Map map, ErrorGenerator<T> errorGenerator) throws XPathExpressionException {
411            List<T> errors = new ArrayList();
412            XPath xpath = XPathHelper.newXPath();
413    
414            NodeList nodes = getFields(xpath, root, types);
415            for (int i = 0; i < nodes.getLength(); i++) {
416                Node field = nodes.item(i);
417                NamedNodeMap fieldAttributes = field.getAttributes();
418                String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
419    
420                LOG.debug("evaluating field: " + fieldName);
421                String findValidation = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation";
422    
423                Node validationNode = (Node) xpath.evaluate(findValidation, root, XPathConstants.NODE);
424                boolean fieldIsRequired = false;
425                if (validationNode != null) {
426                    NamedNodeMap validationAttributes = validationNode.getAttributes();
427                    Node reqAttribNode = validationAttributes.getNamedItem("required");
428                    fieldIsRequired = reqAttribNode != null && "true".equalsIgnoreCase(reqAttribNode.getNodeValue());
429                }
430    
431                String findRegex = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/regex";
432    
433                String regex = null;
434                Node regexNode = (Node) xpath.evaluate(findRegex, root, XPathConstants.NODE);
435    
436                if (regexNode != null && regexNode.getFirstChild() != null) {
437                    regex = regexNode.getFirstChild().getNodeValue();
438                    if (regex == null) {
439                        throw new RuntimeException("Null regex text node");
440                    }
441                }/* else {
442                    if (fieldIsRequired) {
443                        fieldIsOnlyRequired = true;
444                        LOG.debug("Setting empty regex to .+ as field is required");
445                        // NOTE: ok, so technically .+ is not the same as checking merely
446                        // for existence, because a field can be extant but "empty"
447                        // however this has no relevance to the user as an empty field
448                        // is for all intents and purposes non-existent (not-filled-in)
449                        // so let's just use this regex to simplify the logic and
450                        // pass everything through a regex check
451                        regex = ".+";
452                    } else {
453                        LOG.debug("Setting empty regex to .* as field is NOT required");
454                        regex = ".*";
455                    }
456                }*/
457    
458                LOG.debug("regex for field '" + fieldName + "': '" + regex + "'");
459    
460                String fieldValue = null;
461                if (map != null) {
462                    fieldValue = (String) map.get(fieldName);
463                }
464    
465                LOG.debug("field value: " + fieldValue);
466    
467                // fix up non-existent value for regex purposes only
468                if (fieldValue == null) {
469                    fieldValue = "";
470                }
471    
472                if (regex == null){
473                    if (fieldIsRequired) {
474                        if (fieldValue.length() == 0) {
475                            errors.add(errorGenerator.generateMissingFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
476                        }
477                    }
478                } else {
479                    if (!Pattern.compile(regex).matcher(fieldValue).matches()) {
480                        LOG.debug("field value does not match validation regex");
481                        errors.add(errorGenerator.generateInvalidFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
482                    }
483                }
484            }
485            return errors;
486        }
487    
488        public List getRoutingDataRows() {
489            if (routingDataRows.isEmpty()) {
490                routingDataRows = getRows(getConfigXML(), new String[] { "ALL", "REPORT" });
491            }
492            return routingDataRows;
493        }
494    
495        public String getDocContent() {
496            XPath xpath = XPathHelper.newXPath();
497            final String findDocContent = "//routingConfig/xmlDocumentContent";
498            try {
499                Node xmlDocumentContent = (Node) xpath.evaluate(findDocContent, getConfigXML(), XPathConstants.NODE);
500    
501                NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "REPORT", "RULE" });
502    //            if (nodes == null || nodes.getLength() == 0) {
503    //                return "";
504    //            }
505    
506                if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
507                    // Custom doc content in the routingConfig xml.
508                    String documentContent = "";
509                    NodeList customNodes = xmlDocumentContent.getChildNodes();
510                    for (int i = 0; i < customNodes.getLength(); i++) {
511                        Node childNode = customNodes.item(i);
512                        documentContent += XmlJotter.jotNode(childNode);
513                    }
514    
515                    for (int i = 0; i < nodes.getLength(); i++) {
516                        Node field = nodes.item(i);
517                        NamedNodeMap fieldAttributes = field.getAttributes();
518                        String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
519                        LOG.debug("Replacing field '" + fieldName + "'");
520                        Map map = getParamMap();
521                        String fieldValue = (String) map.get(fieldName);
522                        if (map != null && !org.apache.commons.lang.StringUtils.isEmpty(fieldValue)) {
523                            LOG.debug("Replacing %" + fieldName + "% with field value: '" + fieldValue + "'");
524                            documentContent = documentContent.replaceAll("%" + fieldName + "%", fieldValue);
525                        } else {
526                            LOG.debug("Field map is null or fieldValue is empty");
527                        }
528                    }
529                    return documentContent;
530                } else {
531                    // Standard doc content if no doc content is found in the routingConfig xml.
532                    StringBuffer documentContent = new StringBuffer("<xmlRouting>");
533                    for (int i = 0; i < nodes.getLength(); i++) {
534                        Node field = nodes.item(i);
535                        NamedNodeMap fieldAttributes = field.getAttributes();
536                        String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
537                        Map map = getParamMap();
538                        if (map != null && !org.apache.commons.lang.StringUtils.isEmpty((String) map.get(fieldName))) {
539                            documentContent.append("<field name=\"");
540                            documentContent.append(fieldName);
541                            documentContent.append("\"><value>");
542                            documentContent.append((String) map.get(fieldName));
543                            documentContent.append("</value></field>");
544                        }
545                    }
546                    documentContent.append("</xmlRouting>");
547                    return documentContent.toString();
548                }
549            } catch (XPathExpressionException e) {
550                LOG.error("error in getDocContent ", e);
551                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
552            } catch (Exception e) {
553                LOG.error("error in getDocContent attempting to find xml doc content", e);
554                throw new RuntimeException("Error trying to get xml doc content.", e);
555            }
556        }
557    
558        public List getRuleExtensionValues() {
559            List extensionValues = new ArrayList();
560    
561            XPath xpath = XPathHelper.newXPath();
562            try {
563                NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "RULE" });
564                for (int i = 0; i < nodes.getLength(); i++) {
565                    Node field = nodes.item(i);
566                    NamedNodeMap fieldAttributes = field.getAttributes();
567                    String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
568                    Map map = getParamMap();
569                    if (map != null && !org.apache.commons.lang.StringUtils.isEmpty((String) map.get(fieldName))) {
570                        RuleExtensionValue value = new RuleExtensionValue();
571                        value.setKey(fieldName);
572                        value.setValue((String) map.get(fieldName));
573                        extensionValues.add(value);
574                    }
575                }
576            } catch (XPathExpressionException e) {
577                LOG.error("error in getRuleExtensionValues ", e);
578                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
579            }
580            return extensionValues;
581        }
582    
583        public List<RemotableAttributeError> validateRoutingData(Map paramMap) {
584            this.paramMap = paramMap;
585            try {
586                return validate(getConfigXML(), new String[] { "ALL", "REPORT" }, paramMap, new ErrorGenerator<RemotableAttributeError>() {
587                    public RemotableAttributeError generateInvalidFieldError(Node field, String fieldName, String message) {
588                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.error", message).build();
589                    }
590                    public RemotableAttributeError generateMissingFieldError(Node field, String fieldName, String message) {
591                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue()).build();
592                    }
593                });
594            } catch (XPathExpressionException e) {
595                LOG.error("error in validateRoutingData ", e);
596                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
597            }
598        }
599    
600        public List<RemotableAttributeError> validateRuleData(Map paramMap) {
601            this.paramMap = paramMap;
602            try {
603                return validate(getConfigXML(), new String[] { "ALL", "RULE" }, paramMap, new ErrorGenerator<RemotableAttributeError>() {
604                    public RemotableAttributeError generateInvalidFieldError(Node field, String fieldName, String message) {
605                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.error", message).build();
606                    }
607                    public RemotableAttributeError generateMissingFieldError(Node field, String fieldName, String message) {
608                        return RemotableAttributeError.Builder.create("routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue()).build();
609                    }
610                });
611            } catch (XPathExpressionException e) {
612                LOG.error("error in validateRoutingData ", e);
613                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
614            }
615        }
616    
617        public void setRequired(boolean required) {
618            this.required = required;
619        }
620    
621        public boolean isRequired() {
622            return required;
623        }
624    
625        public Element getConfigXML() {
626            try {
627                return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(
628                        new StringReader(extensionDefinition.getConfiguration().get(
629                                KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA))))).getDocumentElement();
630            } catch (Exception e) {
631                String str = extensionDefinition == null ? "null" : extensionDefinition.getName();
632                LOG.error("error parsing xml data from rule attribute: " + str, e);
633                throw new RuntimeException("error parsing xml data from rule attribute: " + str, e);
634            }
635        }
636    
637        // TODO: possibly simplify even further by unifying AttributeError and WorkflowServiceError
638        public List<RemotableAttributeError> validateClientRoutingData() {
639            LOG.debug("validating client routing data");
640            try {
641                return validate(getConfigXML(), new String[] { "ALL", "RULE" }, getParamMap(), new ErrorGenerator<RemotableAttributeError>() {
642                    public RemotableAttributeError generateInvalidFieldError(Node field, String fieldName, String message) {
643                        if (org.apache.commons.lang.StringUtils.isEmpty(message)) {
644                            message = "invalid field value";
645                        } else {
646                            LOG.info("Message: '" + message + "'");
647                        }
648                        return RemotableAttributeError.Builder.create(fieldName, message).build();
649                    }
650                    public RemotableAttributeError generateMissingFieldError(Node field, String fieldName, String message) {
651                        return RemotableAttributeError.Builder.create(fieldName, "Attribute is required; " + message).build();
652                    }
653                });
654            } catch (XPathExpressionException e) {
655                LOG.error("error in validateClientRoutingData ", e);
656                throw new RuntimeException("Error trying to find xml content with xpath expression", e);
657            }
658        }
659    
660        public Map getParamMap() {
661            return paramMap;
662        }
663    
664        public void setParamMap(Map paramMap) {
665            this.paramMap = paramMap;
666        }
667    
668        /**
669         * @return the evaluateForMissingExtensions
670         */
671        public boolean isEvaluateForMissingExtensions() {
672            return this.evaluateForMissingExtensions;
673        }
674    
675        /**
676         * Sets whether or not to evaluate expressions if the extension corresponding to that expressions is not present on the rule.
677         * The correspondence is made by comparing the name of the field declared on the fieldDef element and the name of the
678         * rule extension key.  If this value is set to true then all xpath expressions defined on all fieldDefs will be evaluated
679         * regardless of whether or not the rule has a corresponding extension value.
680         *
681         * <p>By default this is false to preserve backward compatible behavior.
682         */
683        public void setEvaluateForMissingExtensions(boolean evaluateForMissingExtensions) {
684            this.evaluateForMissingExtensions = evaluateForMissingExtensions;
685        }
686    
687    
688    }