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