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