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