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