View Javadoc

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