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