1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.kuali.rice.kew.rule.xmlrouting;
18
19 import java.io.BufferedReader;
20 import java.io.StringReader;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.regex.Pattern;
27
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.xpath.XPath;
30 import javax.xml.xpath.XPathConstants;
31 import javax.xml.xpath.XPathExpressionException;
32
33 import org.apache.commons.lang.StringUtils;
34 import org.kuali.rice.core.util.KeyLabelPair;
35 import org.kuali.rice.kew.attribute.XMLAttributeUtils;
36 import org.kuali.rice.kew.exception.WorkflowRuntimeException;
37 import org.kuali.rice.kew.exception.WorkflowServiceErrorImpl;
38 import org.kuali.rice.kew.routeheader.DocumentContent;
39 import org.kuali.rice.kew.rule.RuleExtension;
40 import org.kuali.rice.kew.rule.RuleExtensionValue;
41 import org.kuali.rice.kew.rule.WorkflowAttributeValidationError;
42 import org.kuali.rice.kew.rule.WorkflowAttributeXmlValidator;
43 import org.kuali.rice.kew.rule.bo.RuleAttribute;
44 import org.kuali.rice.kew.util.Utilities;
45 import org.kuali.rice.kew.util.XmlHelper;
46 import org.kuali.rice.kns.web.ui.Field;
47 import org.kuali.rice.kns.web.ui.Row;
48 import org.w3c.dom.Element;
49 import org.w3c.dom.NamedNodeMap;
50 import org.w3c.dom.Node;
51 import org.w3c.dom.NodeList;
52 import org.xml.sax.InputSource;
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90 public class StandardGenericXMLRuleAttribute implements GenericXMLRuleAttribute, WorkflowAttributeXmlValidator {
91 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLRuleAttribute.class);
92
93 private static final String FIELD_DEF_E = "fieldDef";
94
95 private boolean evaluateForMissingExtensions = false;
96
97 private NodeList getFields(XPath xpath, Element root, String[] types) throws XPathExpressionException {
98 final String OR = " or ";
99 StringBuffer findField = new StringBuffer("//routingConfig/" + FIELD_DEF_E);
100 if (types != null && types.length > 0) {
101 findField.append("[");
102 for (int i = 0; i < types.length; i++) {
103 findField.append("@workflowType='" + types[i] + "'" + OR);
104
105 if ("ALL".equals(types[i])) {
106 findField.append("not(@workflowType)" + OR);
107 }
108 }
109 if (types.length > 0) {
110
111 findField.setLength(findField.length() - OR.length());
112 }
113 findField.append("]");
114 }
115
116 try {
117 return (NodeList) xpath.evaluate(findField.toString(), root, XPathConstants.NODESET);
118 } catch (XPathExpressionException e) {
119 LOG.error("Error evaluating expression: '" + findField + "'");
120 throw e;
121 }
122 }
123
124 private List<Row> getRows(Element root, String[] types) {
125 List<Row> rows = new ArrayList<Row>();
126 XPath xpath = XPathHelper.newXPath();
127 NodeList fieldNodeList;
128 try {
129 fieldNodeList = getFields(xpath, root, types);
130 } catch (XPathExpressionException e) {
131 LOG.error("Error evaluating fields expression");
132 return rows;
133 }
134 if (fieldNodeList != null) {
135 for (int i = 0; i < fieldNodeList.getLength(); i++) {
136 Node field = fieldNodeList.item(i);
137 NamedNodeMap fieldAttributes = field.getAttributes();
138
139 List<Field> fields = new ArrayList<Field>();
140 Field myField = new Field(fieldAttributes.getNamedItem("title").getNodeValue(), "", "", false, fieldAttributes.getNamedItem("name").getNodeValue(), "", false, false, null, "");
141 String quickfinderService = null;
142 for (int j = 0; j < field.getChildNodes().getLength(); j++) {
143 Node childNode = field.getChildNodes().item(j);
144 if ("value".equals(childNode.getNodeName())) {
145 myField.setPropertyValue(childNode.getFirstChild().getNodeValue());
146 } else if ("display".equals(childNode.getNodeName())) {
147 List<KeyLabelPair> options = new ArrayList<KeyLabelPair>();
148 List<String> selectedOptions = new ArrayList<String>();
149 for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
150 Node displayChildNode = childNode.getChildNodes().item(k);
151 if ("type".equals(displayChildNode.getNodeName())) {
152 myField.setFieldType(convertTypeToFieldType(displayChildNode.getFirstChild().getNodeValue()));
153 } else if ("meta".equals(displayChildNode.getNodeName())) {
154
155
156 } else if ("values".equals(displayChildNode.getNodeName())) {
157 NamedNodeMap valuesAttributes = displayChildNode.getAttributes();
158 String optionValue = "";
159
160 Node firstChild = displayChildNode.getFirstChild();
161 if (firstChild != null) {
162 optionValue = firstChild.getNodeValue();
163 }
164 if (valuesAttributes.getNamedItem("selected") != null) {
165 selectedOptions.add(optionValue);
166 }
167 String title = "";
168 Node titleAttribute = valuesAttributes.getNamedItem("title");
169 if (titleAttribute != null) {
170 title = titleAttribute.getNodeValue();
171 }
172 options.add(new KeyLabelPair(optionValue, title));
173 }
174 }
175 if (!options.isEmpty()) {
176 myField.setFieldValidValues(options);
177 if (!selectedOptions.isEmpty()) {
178
179
180
181
182
183
184
185
186
187
188
189 myField.setPropertyValue((String)selectedOptions.get(0));
190
191 }
192 }
193 } else if ("lookup".equals(childNode.getNodeName())) {
194 XMLAttributeUtils.establishFieldLookup(myField, childNode);
195 }
196 }
197 fields.add(myField);
198 rows.add(new Row(fields));
199 }
200 }
201 return rows;
202 }
203
204 private static String convertTypeToFieldType(String type) {
205 if ("text".equals(type)) {
206 return Field.TEXT;
207 } else if ("select".equals(type)) {
208 return Field.DROPDOWN;
209 } else if ("radio".equals(type)) {
210 return Field.RADIO;
211 } else if ("quickfinder".equals(type)) {
212 return Field.QUICKFINDER;
213 }
214 return null;
215 }
216
217 private static interface ErrorGenerator {
218 Object generateInvalidFieldError(Node field, String fieldName, String message);
219 Object generateMissingFieldError(Node field, String fieldName, String message);
220 }
221
222 private RuleAttribute ruleAttribute;
223 private Map paramMap = new HashMap();
224 private List ruleRows = new ArrayList();
225 private List routingDataRows = new ArrayList();
226 private boolean required;
227
228 public StandardGenericXMLRuleAttribute() {
229 }
230
231 public void setRuleAttribute(RuleAttribute ruleAttribute) {
232 this.ruleAttribute = ruleAttribute;
233 }
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292 public boolean isMatch(DocumentContent docContent, List ruleExtensions) {
293 XPath xpath = XPathHelper.newXPath(docContent.getDocument());
294 WorkflowFunctionResolver resolver = XPathHelper.extractFunctionResolver(xpath);
295 resolver.setRuleExtensions(ruleExtensions);
296 List<String> xPathExpressionsToEvaluate = extractExpressionsToEvaluate(xpath, docContent, ruleExtensions);
297 for (String xPathExpressionToEvaluate : xPathExpressionsToEvaluate) {
298 if (LOG.isDebugEnabled()) {
299 LOG.debug("Evaluating xPath expression: " + xPathExpressionToEvaluate);
300 }
301 try {
302 Boolean match = (Boolean) xpath.evaluate(xPathExpressionToEvaluate, docContent.getDocument(), XPathConstants.BOOLEAN);
303 if (LOG.isDebugEnabled()) {
304 LOG.debug("Expression match result: " + match);
305 }
306 if (match != null && !match.booleanValue()) {
307 return false;
308 }
309 } catch (XPathExpressionException e) {
310 LOG.error("Error in isMatch ", e);
311 throw new RuntimeException("Error trying to evalute xml content with xpath expression: " + xPathExpressionToEvaluate, e);
312 }
313 }
314 return true;
315 }
316
317
318
319
320
321 protected List<String> extractExpressionsToEvaluate(XPath xpath, DocumentContent docContent, List ruleExtensions) {
322 List<String> expressionsToEvaluate = new ArrayList<String>(ruleExtensions.size() + 1);
323 Element configXml = getConfigXML();
324 String findFieldExpressions = "//routingConfig/" + FIELD_DEF_E + "/fieldEvaluation/xpathexpression";
325 try {
326 NodeList xPathExpressions = (NodeList) xpath.evaluate(findFieldExpressions, configXml, XPathConstants.NODESET);
327 for (int index = 0; index < xPathExpressions.getLength(); index++) {
328 Element expressionElement = (Element) xPathExpressions.item(index);
329 String expression = XmlHelper.getTextContent(expressionElement);
330 if (!isEvaluateForMissingExtensions()) {
331 Node parentNode = expressionElement.getParentNode().getParentNode();
332 Node fieldAttribute = parentNode.getAttributes().getNamedItem("name");
333 if (fieldAttribute == null || StringUtils.isEmpty(fieldAttribute.getNodeValue())) {
334 throw new WorkflowRuntimeException("Could not determine field name defined on fieldDef for xpath expression: " + expression);
335 }
336 String fieldName = fieldAttribute.getNodeValue();
337 boolean foundExtension = false;
338 outer:for (Iterator iterator = ruleExtensions.iterator(); iterator.hasNext();) {
339 RuleExtension ruleExtension = (RuleExtension) iterator.next();
340 if (ruleExtension.getRuleTemplateAttribute().getRuleAttribute().getName().equals(ruleAttribute.getName())) {
341 for (RuleExtensionValue ruleExtensionValue : ruleExtension.getExtensionValues()) {
342 if (fieldName.equals(ruleExtensionValue.getKey())) {
343 foundExtension = true;
344 break outer;
345 }
346 }
347 }
348 }
349 if (!foundExtension) {
350
351 continue;
352 }
353 }
354
355 if (!StringUtils.isEmpty(expression)) {
356 if (LOG.isDebugEnabled()) {
357 LOG.debug("Adding routingConfig XPath expression: " + expression);
358 }
359 expressionsToEvaluate.add(expression);
360 }
361 }
362 } catch (XPathExpressionException e) {
363 throw new WorkflowRuntimeException("Failed to evalute XPath expression for fieldDefs: " + findFieldExpressions);
364 }
365 String findGlobalExpressions = "//routingConfig/globalEvaluations/xpathexpression";
366 try {
367 NodeList xPathExpressions = (NodeList) xpath.evaluate(findGlobalExpressions, configXml, XPathConstants.NODESET);
368 for (int index = 0; index < xPathExpressions.getLength(); index++) {
369 Element expressionElement = (Element) xPathExpressions.item(index);
370 String expression = XmlHelper.getTextContent(expressionElement);
371 if (!StringUtils.isEmpty(expression)) {
372 if (LOG.isDebugEnabled()) {
373 LOG.debug("Adding global XPath expression: " + expression);
374 }
375 expressionsToEvaluate.add(expression);
376 }
377 }
378 } catch (XPathExpressionException e) {
379 throw new WorkflowRuntimeException("Failed to evalute global XPath expression: " + findGlobalExpressions);
380 }
381 return expressionsToEvaluate;
382 }
383
384 public List getRuleRows() {
385 if (ruleRows.isEmpty()) {
386 ruleRows = getRows(getConfigXML(), new String[] { "ALL", "RULE" });
387 }
388 return ruleRows;
389 }
390
391 private String getValidationErrorMessage(XPath xpath, Element root, String fieldName) throws XPathExpressionException {
392 String findErrorMessage = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/message";
393 return (String) xpath.evaluate(findErrorMessage, root, XPathConstants.STRING);
394 }
395
396 private List validate(Element root, String[] types, Map map, ErrorGenerator errorGenerator) throws XPathExpressionException {
397 List errors = new ArrayList();
398 XPath xpath = XPathHelper.newXPath();
399
400 NodeList nodes = getFields(xpath, root, types);
401 for (int i = 0; i < nodes.getLength(); i++) {
402 Node field = nodes.item(i);
403 NamedNodeMap fieldAttributes = field.getAttributes();
404 String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
405
406 LOG.debug("evaluating field: " + fieldName);
407 String findValidation = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation";
408
409 Node validationNode = (Node) xpath.evaluate(findValidation, root, XPathConstants.NODE);
410 boolean fieldIsRequired = false;
411 if (validationNode != null) {
412 NamedNodeMap validationAttributes = validationNode.getAttributes();
413 Node reqAttribNode = validationAttributes.getNamedItem("required");
414 fieldIsRequired = reqAttribNode != null && "true".equalsIgnoreCase(reqAttribNode.getNodeValue());
415 }
416
417 String findRegex = "//routingConfig/" + FIELD_DEF_E + "[@name='" + fieldName + "']/validation/regex";
418
419 String regex = null;
420 Node regexNode = (Node) xpath.evaluate(findRegex, root, XPathConstants.NODE);
421
422 if (regexNode != null && regexNode.getFirstChild() != null) {
423 regex = regexNode.getFirstChild().getNodeValue();
424 if (regex == null) {
425 throw new RuntimeException("Null regex text node");
426 }
427 }
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444 LOG.debug("regex for field '" + fieldName + "': '" + regex + "'");
445
446 String fieldValue = null;
447 if (map != null) {
448 fieldValue = (String) map.get(fieldName);
449 }
450
451 LOG.debug("field value: " + fieldValue);
452
453
454 if (fieldValue == null) {
455 fieldValue = "";
456 }
457
458 if (regex == null){
459 if (fieldIsRequired) {
460 if (fieldValue.length() == 0) {
461 errors.add(errorGenerator.generateMissingFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
462 }
463 }
464 } else {
465 if (!Pattern.compile(regex).matcher(fieldValue).matches()) {
466 LOG.debug("field value does not match validation regex");
467 errors.add(errorGenerator.generateInvalidFieldError(field, fieldName, getValidationErrorMessage(xpath, root, fieldName)));
468 }
469 }
470 }
471 return errors;
472 }
473
474 public List getRoutingDataRows() {
475 if (routingDataRows.isEmpty()) {
476 routingDataRows = getRows(getConfigXML(), new String[] { "ALL", "REPORT" });
477 }
478 return routingDataRows;
479 }
480
481 public String getDocContent() {
482 XPath xpath = XPathHelper.newXPath();
483 final String findDocContent = "//routingConfig/xmlDocumentContent";
484 try {
485 Node xmlDocumentContent = (Node) xpath.evaluate(findDocContent, getConfigXML(), XPathConstants.NODE);
486
487 NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "REPORT", "RULE" });
488
489
490
491
492 if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
493
494 String documentContent = "";
495 NodeList customNodes = xmlDocumentContent.getChildNodes();
496 for (int i = 0; i < customNodes.getLength(); i++) {
497 Node childNode = customNodes.item(i);
498 documentContent += XmlHelper.writeNode(childNode);
499 }
500
501 for (int i = 0; i < nodes.getLength(); i++) {
502 Node field = nodes.item(i);
503 NamedNodeMap fieldAttributes = field.getAttributes();
504 String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
505 LOG.debug("Replacing field '" + fieldName + "'");
506 Map map = getParamMap();
507 String fieldValue = (String) map.get(fieldName);
508 if (map != null && !Utilities.isEmpty(fieldValue)) {
509 LOG.debug("Replacing %" + fieldName + "% with field value: '" + fieldValue + "'");
510 documentContent = documentContent.replaceAll("%" + fieldName + "%", fieldValue);
511 } else {
512 LOG.debug("Field map is null or fieldValue is empty");
513 }
514 }
515 return documentContent;
516 } else {
517
518 StringBuffer documentContent = new StringBuffer("<xmlRouting>");
519 for (int i = 0; i < nodes.getLength(); i++) {
520 Node field = nodes.item(i);
521 NamedNodeMap fieldAttributes = field.getAttributes();
522 String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
523 Map map = getParamMap();
524 if (map != null && !Utilities.isEmpty((String) map.get(fieldName))) {
525 documentContent.append("<field name=\"");
526 documentContent.append(fieldName);
527 documentContent.append("\"><value>");
528 documentContent.append((String) map.get(fieldName));
529 documentContent.append("</value></field>");
530 }
531 }
532 documentContent.append("</xmlRouting>");
533 return documentContent.toString();
534 }
535 } catch (XPathExpressionException e) {
536 LOG.error("error in getDocContent ", e);
537 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
538 } catch (Exception e) {
539 LOG.error("error in getDocContent attempting to find xml doc content", e);
540 throw new RuntimeException("Error trying to get xml doc content.", e);
541 }
542 }
543
544 public List getRuleExtensionValues() {
545 List extensionValues = new ArrayList();
546
547 XPath xpath = XPathHelper.newXPath();
548 try {
549 NodeList nodes = getFields(xpath, getConfigXML(), new String[] { "ALL", "RULE" });
550 for (int i = 0; i < nodes.getLength(); i++) {
551 Node field = nodes.item(i);
552 NamedNodeMap fieldAttributes = field.getAttributes();
553 String fieldName = fieldAttributes.getNamedItem("name").getNodeValue();
554 Map map = getParamMap();
555 if (map != null && !Utilities.isEmpty((String) map.get(fieldName))) {
556 RuleExtensionValue value = new RuleExtensionValue();
557 value.setKey(fieldName);
558 value.setValue((String) map.get(fieldName));
559 extensionValues.add(value);
560 }
561 }
562 } catch (XPathExpressionException e) {
563 LOG.error("error in getRuleExtensionValues ", e);
564 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
565 }
566 return extensionValues;
567 }
568
569 public List validateRoutingData(Map paramMap) {
570 this.paramMap = paramMap;
571 try {
572 return validate(getConfigXML(), new String[] { "ALL", "REPORT" }, paramMap, new ErrorGenerator() {
573 public Object generateInvalidFieldError(Node field, String fieldName, String message) {
574 return new WorkflowAttributeValidationError("routetemplate.xmlattribute.error", message);
575 }
576 public Object generateMissingFieldError(Node field, String fieldName, String message) {
577 return new WorkflowAttributeValidationError("routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue());
578 }
579 });
580 } catch (XPathExpressionException e) {
581 LOG.error("error in validateRoutingData ", e);
582 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
583 }
584 }
585
586 public List validateRuleData(Map paramMap) {
587 this.paramMap = paramMap;
588 try {
589 return validate(getConfigXML(), new String[] { "ALL", "RULE" }, paramMap, new ErrorGenerator() {
590 public Object generateInvalidFieldError(Node field, String fieldName, String message) {
591 return new WorkflowServiceErrorImpl("Xml attribute error.", "routetemplate.xmlattribute.error", message);
592 }
593 public Object generateMissingFieldError(Node field, String fieldName, String message) {
594 return new WorkflowServiceErrorImpl("Xml attribute error.", "routetemplate.xmlattribute.required.error", field.getAttributes().getNamedItem("title").getNodeValue());
595 }
596 });
597 } catch (XPathExpressionException e) {
598 LOG.error("error in validateRoutingData ", e);
599 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
600 }
601 }
602
603 public void setRequired(boolean required) {
604 this.required = required;
605 }
606
607 public boolean isRequired() {
608 return required;
609 }
610
611 public Element getConfigXML() {
612 try {
613 return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(ruleAttribute.getXmlConfigData())))).getDocumentElement();
614 } catch (Exception e) {
615 String str = ruleAttribute == null ? "null" : ruleAttribute.getName();
616 LOG.error("error parsing xml data from rule attribute: " + str, e);
617 throw new RuntimeException("error parsing xml data from rule attribute: " + str, e);
618 }
619 }
620
621
622
623 public List validateClientRoutingData() {
624 LOG.debug("validating client routing data");
625 try {
626 return validate(getConfigXML(), new String[] { "ALL", "RULE" }, getParamMap(), new ErrorGenerator() {
627 public Object generateInvalidFieldError(Node field, String fieldName, String message) {
628 if (Utilities.isEmpty(message)) {
629 message = "invalid field value";
630 } else {
631 LOG.info("Message: '" + message + "'");
632 }
633 return new WorkflowAttributeValidationError(fieldName, message);
634 }
635 public Object generateMissingFieldError(Node field, String fieldName, String message) {
636 return new WorkflowAttributeValidationError(fieldName, "Attribute is required; " + message);
637 }
638 });
639 } catch (XPathExpressionException e) {
640 LOG.error("error in validateClientRoutingData ", e);
641 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
642 }
643 }
644
645 public Map getParamMap() {
646 return paramMap;
647 }
648
649 public void setParamMap(Map paramMap) {
650 this.paramMap = paramMap;
651 }
652
653
654
655
656 public boolean isEvaluateForMissingExtensions() {
657 return this.evaluateForMissingExtensions;
658 }
659
660
661
662
663
664
665
666
667
668 public void setEvaluateForMissingExtensions(boolean evaluateForMissingExtensions) {
669 this.evaluateForMissingExtensions = evaluateForMissingExtensions;
670 }
671
672
673 }