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