1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
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
107 if ("ALL".equals(type)) {
108 findField.append("not(@workflowType)" + OR);
109 }
110 }
111 if (types.length > 0) {
112
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
157
158 } else if ("values".equals(displayChildNode.getNodeName())) {
159 NamedNodeMap valuesAttributes = displayChildNode.getAttributes();
160 String optionValue = "";
161
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
181
182
183
184
185
186
187
188
189
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
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
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
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
322
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
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
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
401
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 }
435
436
437
438
439
440
441
442
443
444
445
446
447
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
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
496
497
498
499 if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
500
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
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
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
663
664 public boolean isEvaluateForMissingExtensions() {
665 return this.evaluateForMissingExtensions;
666 }
667
668
669
670
671
672
673
674
675
676 public void setEvaluateForMissingExtensions(boolean evaluateForMissingExtensions) {
677 this.evaluateForMissingExtensions = evaluateForMissingExtensions;
678 }
679
680
681 }