1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.kew.docsearch.xml;
17
18 import org.apache.commons.lang.StringUtils;
19 import org.apache.log4j.Logger;
20 import org.kuali.rice.core.api.config.ConfigurationException;
21 import org.kuali.rice.core.api.impex.xml.XmlConstants;
22 import org.kuali.rice.core.api.util.ConcreteKeyValue;
23 import org.kuali.rice.core.api.util.KeyValue;
24 import org.kuali.rice.core.api.util.xml.XmlHelper;
25 import org.kuali.rice.core.api.util.xml.XmlJotter;
26 import org.kuali.rice.kew.api.KewApiConstants;
27 import org.kuali.rice.kew.api.extension.ExtensionDefinition;
28 import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
29 import org.kuali.rice.kew.util.Utilities;
30 import org.w3c.dom.Attr;
31 import org.w3c.dom.Element;
32 import org.w3c.dom.NamedNodeMap;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35 import org.xml.sax.InputSource;
36
37 import javax.xml.parsers.DocumentBuilderFactory;
38 import javax.xml.parsers.ParserConfigurationException;
39 import javax.xml.transform.TransformerException;
40 import javax.xml.xpath.XPath;
41 import javax.xml.xpath.XPathConstants;
42 import javax.xml.xpath.XPathExpressionException;
43 import java.io.BufferedReader;
44 import java.io.StringReader;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.LinkedHashMap;
50 import java.util.List;
51 import java.util.Map;
52
53
54
55
56 class XMLSearchableAttributeContent {
57 private static final Logger LOG = Logger.getLogger(XMLSearchableAttributeContent.class);
58
59 private ExtensionDefinition def;
60 private Element attributeConfig;
61 private Node searchingConfig;
62 private String searchContent;
63 private Map<String, FieldDef> fieldDefs;
64
65 XMLSearchableAttributeContent(ExtensionDefinition ed) {
66 this.def = ed;
67 }
68
69 XMLSearchableAttributeContent(String configXML) throws TransformerException {
70 this.attributeConfig = XmlHelper.readXml(configXML).getDocumentElement();
71 }
72
73 XMLSearchableAttributeContent(Element configXML) {
74 if (configXML == null) {
75 throw new IllegalArgumentException("Configuration element must not be nil");
76 }
77 this.attributeConfig = configXML;
78 }
79
80 Node getSearchingConfig() throws XPathExpressionException, ParserConfigurationException {
81 if (searchingConfig == null) {
82 XPath xpath = XPathHelper.newXPath();
83
84 String searchingConfigExpr = "//searchingConfig";
85 searchingConfig = (Node) xpath.evaluate(searchingConfigExpr, getAttributeConfig(), XPathConstants.NODE);
86 }
87 return searchingConfig;
88 }
89
90 String getSearchContent() throws XPathExpressionException, ParserConfigurationException {
91 if (searchContent == null) {
92 Node cfg = getSearchingConfig();
93 XPath xpath = XPathHelper.newXPath();
94 Node n = (Node) xpath.evaluate("xmlSearchContent", cfg, XPathConstants.NODE);
95 if (n != null) {
96 StringBuilder sb = new StringBuilder();
97 NodeList list = n.getChildNodes();
98 for (int i = 0; i < list.getLength(); i++) {
99 sb.append(XmlJotter.jotNode(list.item(i)));
100 }
101 this.searchContent = sb.toString();
102 }
103 }
104 return searchContent;
105 }
106
107 String generateSearchContent(Map<String, String> properties) throws XPathExpressionException, ParserConfigurationException {
108 if (properties == null) {
109 properties = new HashMap<String, String>();
110 }
111
112 List<FieldDef> fields = getFieldDefList();
113 if (fields.size() == 0) {
114 return "";
115 }
116
117 String searchContent = getSearchContent();
118
119
120 if (searchContent != null) {
121 String generatedContent = searchContent;
122
123
124
125
126
127 for (FieldDef field: fields) {
128 if (StringUtils.isNotBlank(field.name)) {
129 String propValue = properties.get(field.name);
130 if (StringUtils.isNotBlank(propValue)) {
131 generatedContent = generatedContent.replaceAll("%" + field.name + "%", propValue);
132 }
133 }
134 }
135 return generatedContent;
136 } else {
137
138 StringBuilder buf = new StringBuilder("<xmlRouting>");
139 for (FieldDef field: fields) {
140 if (StringUtils.isNotBlank(field.name)) {
141 String propValue = properties.get(field.name);
142 if (StringUtils.isNotBlank(propValue)) {
143 buf.append("<field name=\"");
144 buf.append(field.name);
145 buf.append("\"><value>");
146 buf.append(propValue);
147 buf.append("</value></field>");
148 }
149 }
150 }
151 buf.append("</xmlRouting>");
152 return buf.toString();
153 }
154 }
155
156
157
158
159
160
161
162 List<FieldDef> getFieldDefList() throws XPathExpressionException, ParserConfigurationException {
163 return Collections.unmodifiableList(new ArrayList<FieldDef>(getFieldDefs().values()));
164 }
165
166 Map<String, FieldDef> getFieldDefs() throws XPathExpressionException, ParserConfigurationException {
167 if (fieldDefs == null) {
168 fieldDefs = new LinkedHashMap<String, FieldDef>();
169 XPath xpath = XPathHelper.newXPath();
170 Node searchingConfig = getSearchingConfig();
171 if (searchingConfig != null) {
172 NodeList list = (NodeList) xpath.evaluate("fieldDef", searchingConfig, XPathConstants.NODESET);
173 for (int i = 0; i < list.getLength(); i++) {
174 FieldDef def = new FieldDef(list.item(i));
175 fieldDefs.put(def.name, def);
176 }
177 }
178 }
179 return fieldDefs;
180 }
181
182 protected Element getAttributeConfig() {
183 if (attributeConfig == null) {
184 try {
185 String xmlConfigData = def.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
186 this.attributeConfig = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(xmlConfigData)))).getDocumentElement();
187 } catch (Exception e) {
188 String ruleAttrStr = (def == null ? null : def.getName());
189 LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
190 throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
191 }
192 }
193 return attributeConfig;
194 }
195
196
197
198
199 static class FieldDef {
200 final String name;
201 final String title;
202 final String defaultValue;
203 final Display display;
204 final Validation validation;
205 final Visibility visibility;
206 final SearchDefinition searchDefinition;
207 final String fieldEvaluationExpr;
208 final Boolean showResultColumn;
209 final Lookup lookup;
210
211 FieldDef(Node n) throws XPathExpressionException {
212 XPath xpath = XPathHelper.newXPath();
213 this.name = getStringAttr(n, "name");
214 this.title= getStringAttr(n, "title");
215 this.defaultValue = getNodeText(xpath, n, "value");
216 this.fieldEvaluationExpr = getNodeText(xpath, n, "fieldEvaluation/xpathexpression");
217 this.showResultColumn = getBoolean(xpath, n, "resultColumn/@show");
218
219
220 this.display = new Display(xpath, n);
221 this.validation = new Validation(xpath, n);
222 this.visibility = new Visibility(xpath, n);
223 this.searchDefinition = new SearchDefinition(xpath, n);
224 this.lookup = new Lookup(xpath, n, name);
225 }
226
227
228
229
230
231
232 boolean isDisplayedInSearchResults() {
233 return showResultColumn != null ? showResultColumn : (visibility.visible != null ? visibility.visible : true);
234 }
235
236
237
238
239 static class Display {
240 final String type;
241 final String meta;
242 final String formatter;
243 final Collection<KeyValue> options;
244 final Collection<String> selectedOptions;
245
246 Display(XPath xpath, Node n) throws XPathExpressionException {
247 type = getNodeText(xpath, n, "display/type");
248 meta = getNodeText(xpath, n, "display/meta");
249 formatter = getNodeText(xpath, n, "display/formatter");
250 Collection<KeyValue> options = new ArrayList<KeyValue>();
251 Collection<String> selectedOptions = new ArrayList<String>();
252
253 NodeList nodes = (NodeList) xpath.evaluate("display[1]/values", n, XPathConstants.NODESET);
254 for (int i = 0; i < nodes.getLength(); i++) {
255 Node node = nodes.item(i);
256 boolean selected = getBooleanAttr(node, "selected", false);
257 String title = getStringAttr(node, "title");
258
259
260 String value = node.getTextContent();
261 if (value == null) {
262 value = "";
263 }
264 options.add(new ConcreteKeyValue(value, title));
265 if (selected) {
266 selectedOptions.add(node.getTextContent());
267 }
268 }
269
270 this.options = Collections.unmodifiableCollection(options);
271 this.selectedOptions = Collections.unmodifiableCollection(selectedOptions);
272 }
273 }
274
275
276
277
278 static class Validation {
279 final boolean required;
280 final String regex;
281 final String message;
282
283 Validation(XPath xpath, Node n) throws XPathExpressionException {
284 required = Boolean.parseBoolean(getNodeText(xpath, n, "validation/@required"));
285 regex = getNodeText(xpath, n, "validation/regex");
286 message = getNodeText(xpath, n, "validation/message");
287 }
288 }
289
290
291
292
293 static class Visibility {
294 final Boolean visible;
295 final String type;
296 final String groupName;
297 final String groupNamespace;
298
299 Visibility(XPath xpath, Node n) throws XPathExpressionException {
300 Boolean visible = null;
301 String type = null;
302 String groupName = null;
303 String groupNamespace = null;
304 Node node = (Node) xpath.evaluate("(visibility/field | visibility/column | visibility/fieldAndColumn)", n, XPathConstants.NODE);
305 if (node != null && node instanceof Element) {
306 Element visibilityEl = (Element) node;
307 type = visibilityEl.getNodeName();
308 Attr attr = visibilityEl.getAttributeNode("visible");
309 if (attr != null) {
310 visible = Boolean.valueOf(attr.getValue());
311 }
312 Node groupMember = (Node) xpath.evaluate("(" + XmlConstants.IS_MEMBER_OF_GROUP + "|" + XmlConstants.IS_MEMBER_OF_WORKGROUP + ")", visibilityEl, XPathConstants.NODE);
313 if (groupMember != null && groupMember instanceof Element) {
314 Element groupMemberEl = (Element) groupMember;
315 boolean group_def_found = false;
316 if (XmlConstants.IS_MEMBER_OF_GROUP.equals(groupMember.getNodeName())) {
317 group_def_found = true;
318 groupName = Utilities.substituteConfigParameters(groupMember.getTextContent().trim());
319 groupNamespace = Utilities.substituteConfigParameters(groupMemberEl.getAttribute(XmlConstants.NAMESPACE)).trim();
320 } else if (XmlConstants.IS_MEMBER_OF_WORKGROUP.equals(groupMember.getNodeName())) {
321 group_def_found = true;
322 LOG.warn("Rule Attribute XML is using deprecated element '" + XmlConstants.IS_MEMBER_OF_WORKGROUP +
323 "', please use '" + XmlConstants.IS_MEMBER_OF_GROUP + "' instead.");
324 String workgroupName = Utilities.substituteConfigParameters(groupMember.getTextContent());
325 groupNamespace = Utilities.parseGroupNamespaceCode(workgroupName);
326 groupName = Utilities.parseGroupName(workgroupName);
327 }
328 if (group_def_found) {
329 if (StringUtils.isEmpty(groupName) || StringUtils.isEmpty(groupNamespace)) {
330 throw new RuntimeException("Both group name and group namespace must be present for group-based visibility.");
331 }
332 }
333 }
334 }
335 this.visible = visible;
336 this.type = type;
337 this.groupName = groupName;
338 this.groupNamespace = groupNamespace;
339 }
340 }
341
342
343
344
345 static class SearchDefinition {
346 final RangeOptions DEFAULTS = new RangeOptions(null, false, false);
347
348
349
350 final String dataType;
351 final boolean rangeSearch;
352 final RangeOptions searchDef;
353 final RangeOptions rangeDef;
354 final RangeBound lowerBound;
355 final RangeBound upperBound;
356
357 SearchDefinition(XPath xpath, Node n) throws XPathExpressionException {
358 String dataType = KewApiConstants.SearchableAttributeConstants.DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
359
360
361 RangeOptions searchDefDefaults = new RangeOptions();
362 RangeOptions rangeDef = null;
363 RangeBound lowerBound = null;
364 RangeBound upperBound = null;
365 boolean rangeSearch = false;
366 Node searchDefNode = (Node) xpath.evaluate("searchDefinition", n, XPathConstants.NODE);
367 if (searchDefNode != null) {
368 String s = getStringAttr(searchDefNode, "dataType");
369
370 if (StringUtils.isNotEmpty(s)) {
371 dataType = s;
372 }
373
374
375 rangeSearch = getBooleanAttr(searchDefNode, "rangeSearch", false);
376
377 searchDefDefaults = new RangeOptions(xpath, searchDefNode, DEFAULTS);
378 Node rangeDefinition = (Node) xpath.evaluate("rangeDefinition", searchDefNode, XPathConstants.NODE);
379
380 if (rangeDefinition != null) {
381 rangeDef = new RangeOptions(xpath, rangeDefinition, searchDefDefaults);
382 Node lower = (Node) xpath.evaluate("lower", rangeDefinition, XPathConstants.NODE);
383 lowerBound = lower == null ? new RangeBound(defaultInclusive(rangeDef, true)) : new RangeBound(xpath, lower, defaultInclusive(rangeDef, true));
384 Node upper = (Node) xpath.evaluate("upper", rangeDefinition, XPathConstants.NODE);
385 upperBound = upper == null ? new RangeBound(defaultInclusive(rangeDef, false)) : new RangeBound(xpath, upper, defaultInclusive(rangeDef, false));
386 } else if (rangeSearch) {
387
388
389 lowerBound = new RangeBound(defaultInclusive(searchDefDefaults, true));
390 upperBound = new RangeBound(defaultInclusive(searchDefDefaults, false));
391 }
392 }
393 this.dataType = dataType;
394 this.rangeSearch = rangeSearch;
395 this.searchDef = searchDefDefaults;
396 this.rangeDef = rangeDef;
397 this.lowerBound = lowerBound;
398 this.upperBound = upperBound;
399 }
400
401 private static BaseRangeOptions defaultInclusive(BaseRangeOptions opts, boolean inclusive) {
402 boolean inc = opts.inclusive == null ? inclusive : opts.inclusive;
403 return new BaseRangeOptions(inc, opts.datePicker);
404 }
405
406
407
408
409 public RangeOptions getRangeBoundOptions() {
410 return rangeDef == null ? searchDef : rangeDef;
411 }
412
413
414
415
416 public boolean isRangedSearch() {
417
418
419
420
421 return this.rangeSearch || (rangeDef != null);
422 }
423
424
425
426
427 static class BaseRangeOptions {
428 protected final Boolean inclusive;
429 protected final Boolean datePicker;
430
431 BaseRangeOptions() {
432 this.inclusive = this.datePicker = null;
433 }
434 BaseRangeOptions(Boolean inclusive, Boolean datePicker) {
435 this.inclusive = inclusive;
436 this.datePicker = datePicker;
437 }
438 BaseRangeOptions(BaseRangeOptions defaults) {
439 this.inclusive = defaults.inclusive;
440 this.datePicker = defaults.datePicker;
441 }
442 BaseRangeOptions(XPath xpath, Node n, BaseRangeOptions defaults) {
443 this.inclusive = getBooleanAttr(n, "inclusive", defaults.inclusive);
444 this.datePicker = getBooleanAttr(n, "datePicker", defaults.datePicker);
445 }
446 }
447
448
449
450
451
452 static class RangeOptions extends BaseRangeOptions {
453 protected final Boolean caseSensitive;
454 RangeOptions() {
455 super();
456 this.caseSensitive = null;
457 }
458 RangeOptions(Boolean inclusive, Boolean caseSensitive, Boolean datePicker) {
459 super(inclusive, datePicker);
460 this.caseSensitive = caseSensitive;
461 }
462 RangeOptions(RangeOptions defaults) {
463 super(defaults);
464 this.caseSensitive = defaults.caseSensitive;
465 }
466 RangeOptions(XPath xpath, Node n, RangeOptions defaults) {
467 super(xpath, n, defaults);
468 this.caseSensitive = getBooleanAttr(n, "caseSensitive", defaults.caseSensitive);
469 }
470 }
471
472
473
474
475 static class RangeBound extends BaseRangeOptions {
476 final String label;
477 RangeBound(BaseRangeOptions defaults) {
478 super(defaults);
479 this.label = null;
480 }
481 RangeBound(XPath xpath, Node n, BaseRangeOptions defaults) {
482 super(xpath, n, defaults);
483 this.label = getStringAttr(n, "label");
484 }
485 }
486 }
487
488
489
490
491
492
493
494
495
496 static class Lookup {
497 final String dataObjectClass;
498 final Map<String, String> fieldConversions;
499
500 Lookup(XPath xpath, Node n, String fieldName) throws XPathExpressionException {
501 String dataObjectClass = null;
502 Map<String, String> fieldConversions = new HashMap<String, String>();
503
504 Node lookupNode = (Node) xpath.evaluate("lookup", n, XPathConstants.NODE);
505 if (lookupNode != null) {
506 NamedNodeMap quickfinderAttributes = lookupNode.getAttributes();
507 Node dataObjectNode = quickfinderAttributes.getNamedItem("dataObjectClass");
508 if (dataObjectNode == null) {
509
510 dataObjectNode = quickfinderAttributes.getNamedItem("businessObjectClass");
511 if (dataObjectNode != null) {
512 LOG.warn("Field is using deprecated 'businessObjectClass' instead of 'dataObjectClass' for lookup definition, field name is: " + fieldName);
513 } else {
514 throw new ConfigurationException("Failed to locate 'dataObjectClass' for lookup definition.");
515 }
516 }
517 dataObjectClass = dataObjectNode.getNodeValue();
518 NodeList list = (NodeList) xpath.evaluate("fieldConversions/fieldConversion", lookupNode, XPathConstants.NODESET);
519 for (int i = 0; i < list.getLength(); i++) {
520 Node fieldConversionChildNode = list.item(i);
521 NamedNodeMap fieldConversionAttributes = fieldConversionChildNode.getAttributes();
522
523 String lookupFieldName = fieldConversionAttributes.getNamedItem("lookupFieldName").getNodeValue();
524 String localFieldName = fieldConversionAttributes.getNamedItem("localFieldName").getNodeValue();
525 fieldConversions.put(lookupFieldName, localFieldName);
526 }
527 }
528
529 this.dataObjectClass = dataObjectClass;
530 this.fieldConversions = Collections.unmodifiableMap(fieldConversions);
531 }
532 }
533 }
534
535 private static Boolean getBooleanAttr(Node n, String attributeName, Boolean dflt) {
536 String nodeValue = getStringAttr(n, attributeName);
537 return nodeValue == null ? dflt : Boolean.valueOf(nodeValue);
538 }
539
540 private static String getStringAttr(Node n, String attributeName) {
541 Node attr = n.getAttributes().getNamedItem(attributeName);
542 return attr == null ? null : attr.getNodeValue();
543 }
544
545 private static String getNodeText(XPath xpath, Node n, String expression) throws XPathExpressionException {
546 Node node = (Node) xpath.evaluate(expression, n, XPathConstants.NODE);
547 if (node == null) return null;
548 return node.getTextContent();
549 }
550
551 private static Boolean getBoolean(XPath xpath, Node n, String expression) throws XPathExpressionException {
552 String val = getNodeText(xpath, n, expression);
553 return val == null ? null : Boolean.valueOf(val);
554 }
555 }