View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.rule.xmlrouting;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.uif.RemotableAttributeErrorContract;
20  import org.kuali.rice.core.api.uif.RemotableAttributeError;
21  import org.kuali.rice.core.api.util.ConcreteKeyValue;
22  import org.kuali.rice.core.api.util.KeyValue;
23  import org.kuali.rice.core.api.util.xml.XmlJotter;
24  import org.kuali.rice.kew.api.KewApiConstants;
25  import org.kuali.rice.kew.api.WorkflowRuntimeException;
26  import org.kuali.rice.kew.api.extension.ExtensionDefinition;
27  import org.kuali.rice.kew.api.rule.RuleExtension;
28  import org.kuali.rice.kew.attribute.XMLAttributeUtils;
29  import org.kuali.rice.kew.engine.RouteContext;
30  import org.kuali.rice.kew.exception.WorkflowServiceError;
31  import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
32  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
33  import org.kuali.rice.kew.routeheader.DocumentContent;
34  import org.kuali.rice.kew.rule.RuleExtensionBo;
35  import org.kuali.rice.kew.rule.RuleExtensionValue;
36  import org.kuali.rice.kew.rule.WorkflowAttributeXmlValidator;
37  import org.kuali.rice.kns.web.ui.Field;
38  import org.kuali.rice.kns.web.ui.Row;
39  import org.w3c.dom.Element;
40  import org.w3c.dom.NamedNodeMap;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  import org.xml.sax.InputSource;
44  
45  import javax.xml.parsers.DocumentBuilderFactory;
46  import javax.xml.xpath.XPath;
47  import javax.xml.xpath.XPathConstants;
48  import javax.xml.xpath.XPathExpressionException;
49  import java.io.BufferedReader;
50  import java.io.StringReader;
51  import java.util.ArrayList;
52  import java.util.HashMap;
53  import java.util.Iterator;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.regex.Pattern;
57  
58  
59  /**
60   * A generic WorkflowAttribute implementation that can be defined completely by XML.
61   * <ol>
62   *   <li>This attribute implementation takes "properties" defined on the the {@link org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition}
63   *       and maps them to the param map of {@link GenericXMLRuleAttribute}, which relate directly to a set of fields defined by the
64   *       XML <code>&lt;routingConfig&gt;</code> configuration.</li>
65   *   <li>Application of the properties defined on the WorkflowAttributeDefinition
66   *       to the actual attribute is performed in  {@link org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver#invokeProperties(Object, java.util.Collection)}</li>
67   *   <li>These params are then used to perform one of either EITHER:
68   *     <ul>
69   *       <li>Replace parameters of the syntax <code>%<i>field name</i>%</code> in the doc content if doc content is
70   *           defined in the <code>&lt;xmlDocumentContent&gt;</code></li>
71   *       <li>Generate a generic doc content, containing the parameter key/value pairs, e.g.:
72   *           <blockquote>
73   *           <code><pre>
74   *             &lt;xmlrouting&gt;
75   *               &lt;field name="color"&gt;&lt;value&gt;red&lt;/value&gt;&lt;/field&gt;
76   *               &lt;field name="shape"&gt;&lt;value&gt;circle&lt;/value&gt;&lt;/field&gt;
77   *             &lt;/xmlrouting&gt;
78   *           </pre></code>
79   *           </blockquote>
80   *       </li>
81   *     </ul>
82   *     Currently, only parameters that match fields configured in the routingConfig are honored (the others are ignored)
83   *     (NOTE: to make this even more reusable we might want to consider generating content for all parameters, even those that
84   *      do not have corresponding fields)
85   *   </li>
86   *   <li>The routingConfig element defines a set of <code>fieldDef</code>s, each of which may have an <code>xpathexpression</code> for field evaluation.
87   *       This <code>xpathexpression</code> is used to determine whether the attribute's {@link #isMatch(DocumentContent, List)} will
88   *       succeed.  Each fieldDef may also have a <code>validation</code> element which supplies a regular expression against which
89   *       to validate the field value (given by the param map)</li>
90   * </ol>
91   *
92   * @author Kuali Rice Team (rice.collab@kuali.org)
93   */
94  public class StandardGenericXMLRuleAttribute implements GenericXMLRuleAttribute, WorkflowAttributeXmlValidator {
95      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLRuleAttribute.class);
96  
97      private static final String FIELD_DEF_E = "fieldDef";
98  
99      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 }