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.collections.CollectionUtils;
19 import org.apache.commons.lang.StringUtils;
20 import org.kuali.rice.core.api.impex.xml.XmlConstants;
21 import org.kuali.rice.core.api.uif.DataType;
22 import org.kuali.rice.core.api.uif.RemotableAbstractControl;
23 import org.kuali.rice.core.api.uif.RemotableAttributeError;
24 import org.kuali.rice.core.api.uif.RemotableAttributeField;
25 import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
26 import org.kuali.rice.core.api.uif.RemotableDatepicker;
27 import org.kuali.rice.core.api.uif.RemotableHiddenInput;
28 import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
29 import org.kuali.rice.core.api.uif.RemotableSelect;
30 import org.kuali.rice.core.api.uif.RemotableTextInput;
31 import org.kuali.rice.core.api.util.ConcreteKeyValue;
32 import org.kuali.rice.core.api.util.KeyValue;
33 import org.kuali.rice.core.api.util.xml.XmlJotter;
34 import org.kuali.rice.kew.api.KewApiConstants;
35 import org.kuali.rice.kew.api.WorkflowRuntimeException;
36 import org.kuali.rice.kew.api.document.DocumentWithContent;
37 import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
38 import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
39 import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
40 import org.kuali.rice.kew.api.extension.ExtensionDefinition;
41 import org.kuali.rice.kew.attribute.XMLAttributeUtils;
42 import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
43 import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
44 import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
45 import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
46 import org.kuali.rice.kew.api.KewApiConstants;
47 import org.kuali.rice.kew.util.Utilities;
48 import org.kuali.rice.kim.api.group.Group;
49 import org.kuali.rice.kim.api.group.GroupService;
50 import org.kuali.rice.kim.api.services.KimApiServiceLocator;
51 import org.kuali.rice.krad.UserSession;
52 import org.kuali.rice.krad.util.GlobalVariables;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.w3c.dom.NamedNodeMap;
56 import org.w3c.dom.Node;
57 import org.w3c.dom.NodeList;
58 import org.xml.sax.InputSource;
59
60 import javax.xml.parsers.DocumentBuilderFactory;
61 import javax.xml.xpath.XPath;
62 import javax.xml.xpath.XPathConstants;
63 import javax.xml.xpath.XPathExpressionException;
64 import java.io.BufferedReader;
65 import java.io.StringReader;
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.HashMap;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.regex.Matcher;
72 import java.util.regex.Pattern;
73
74
75
76
77
78
79
80 public class StandardGenericXMLSearchableAttribute implements SearchableAttribute {
81
82 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLSearchableAttribute.class);
83 private static final String FIELD_DEF_E = "fieldDef";
84
85 @Override
86 public String generateSearchContent(ExtensionDefinition extensionDefinition, String documentTypeName, WorkflowAttributeDefinition attributeDefinition) {
87 XPath xpath = XPathHelper.newXPath();
88 String findDocContent = "//searchingConfig/xmlSearchContent";
89 Map<String, String> propertyDefinitionMap = attributeDefinition.getPropertyDefinitionsAsMap();
90 try {
91 Node xmlDocumentContent = (Node) xpath.evaluate(findDocContent, getConfigXML(extensionDefinition), XPathConstants.NODE);
92 if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
93
94 String docContent = "";
95 NodeList customNodes = xmlDocumentContent.getChildNodes();
96 for (int i = 0; i < customNodes.getLength(); i++) {
97 Node childNode = customNodes.item(i);
98 docContent += XmlJotter.jotNode(childNode);
99 }
100 String findField = "//searchingConfig/" + FIELD_DEF_E;
101 NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(extensionDefinition), XPathConstants.NODESET);
102 if (nodes == null || nodes.getLength() == 0) {
103 return "";
104 }
105 for (int i = 0; i < nodes.getLength(); i++) {
106 Node field = nodes.item(i);
107 NamedNodeMap fieldAttributes = field.getAttributes();
108 if (propertyDefinitionMap != null && !StringUtils.isBlank(propertyDefinitionMap.get(fieldAttributes.getNamedItem("name").getNodeValue()))) {
109 docContent = docContent.replaceAll("%" + fieldAttributes.getNamedItem("name").getNodeValue() + "%", propertyDefinitionMap.get(fieldAttributes.getNamedItem("name").getNodeValue()));
110 }
111 }
112 return docContent;
113 } else {
114
115 StringBuffer documentContent = new StringBuffer("<xmlRouting>");
116 String findField = "//searchingConfig/" + FIELD_DEF_E;
117 NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(extensionDefinition), XPathConstants.NODESET);
118 if (nodes == null || nodes.getLength() == 0) {
119 return "";
120 }
121 for (int i = 0; i < nodes.getLength(); i++) {
122 Node field = nodes.item(i);
123 NamedNodeMap fieldAttributes = field.getAttributes();
124 if (propertyDefinitionMap != null && !StringUtils.isBlank(propertyDefinitionMap.get(fieldAttributes.getNamedItem("name").getNodeValue()))) {
125 documentContent.append("<field name=\"");
126 documentContent.append(fieldAttributes.getNamedItem("name").getNodeValue());
127 documentContent.append("\"><value>");
128 documentContent.append(propertyDefinitionMap.get(fieldAttributes.getNamedItem("name").getNodeValue()));
129 documentContent.append("</value></field>");
130 }
131 }
132 documentContent.append("</xmlRouting>");
133 return documentContent.toString();
134 }
135 } catch (XPathExpressionException e) {
136 LOG.error("error in getSearchContent ", e);
137 throw new RuntimeException("Error trying to find xml content with xpath expression", e);
138 } catch (Exception e) {
139 LOG.error("error in getSearchContent attempting to find xml search content", e);
140 throw new RuntimeException("Error trying to get xml search content.", e);
141 }
142 }
143
144 @Override
145 public List<DocumentAttribute> extractDocumentAttributes(ExtensionDefinition extensionDefinition,
146 DocumentWithContent documentWithContent) {
147 List<DocumentAttribute> searchStorageValues = new ArrayList<DocumentAttribute>();
148 Document document;
149 String fullDocumentContent = documentWithContent.getDocumentContent().getFullContent();
150 if (StringUtils.isBlank(documentWithContent.getDocumentContent().getFullContent())) {
151 LOG.warn("Empty Document Content found for document id: " + documentWithContent.getDocument().getDocumentId());
152 return searchStorageValues;
153 }
154 try {
155 document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
156 new InputSource(new BufferedReader(new StringReader(fullDocumentContent))));
157 } catch (Exception e){
158 LOG.error("error parsing docContent: "+documentWithContent.getDocumentContent(), e);
159 throw new RuntimeException("Error trying to parse docContent: "+documentWithContent.getDocumentContent(), e);
160 }
161 XPath xpath = XPathHelper.newXPath(document);
162 String findField = "//searchingConfig/" + FIELD_DEF_E;
163 try {
164 NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(extensionDefinition), XPathConstants.NODESET);
165 if (nodes == null) {
166 LOG.error("Could not find searching configuration (<searchingConfig>) for this XMLSearchAttribute");
167 } else {
168
169 for (int i = 0; i < nodes.getLength(); i++) {
170 Node field = nodes.item(i);
171 NamedNodeMap fieldAttributes = field.getAttributes();
172
173 String findXpathExpressionPrefix = "//searchingConfig/" + FIELD_DEF_E + "[@name='" + fieldAttributes.getNamedItem("name").getNodeValue() + "']";
174 String findDataTypeXpathExpression = findXpathExpressionPrefix + "/searchDefinition/@dataType";
175 String findXpathExpression = findXpathExpressionPrefix + "/fieldEvaluation/xpathexpression";
176 String fieldDataType = null;
177 String xpathExpression = null;
178 try {
179 fieldDataType = (String) xpath.evaluate(findDataTypeXpathExpression, getConfigXML(extensionDefinition), XPathConstants.STRING);
180 if (org.apache.commons.lang.StringUtils.isEmpty(fieldDataType)) {
181 fieldDataType = KewApiConstants.SearchableAttributeConstants.DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
182 }
183 xpathExpression = (String) xpath.evaluate(findXpathExpression, getConfigXML(extensionDefinition), XPathConstants.STRING);
184 if (!org.apache.commons.lang.StringUtils.isEmpty(xpathExpression)) {
185
186 try {
187 NodeList searchValues = (NodeList) xpath.evaluate(xpathExpression, document.getDocumentElement(), XPathConstants.NODESET);
188
189
190 if (searchValues.getLength() == 0) {
191 DocumentAttribute searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), null);
192 if (searchableValue != null) {
193 searchStorageValues.add(searchableValue);
194 }
195 } else {
196 for (int j = 0; j < searchValues.getLength(); j++) {
197 Node searchValue = searchValues.item(j);
198 String value = null;
199 if (searchValue.getFirstChild() != null && (!StringUtils.isEmpty(searchValue.getFirstChild().getNodeValue()))) {
200 value = searchValue.getFirstChild().getNodeValue();
201 }
202 DocumentAttribute searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), value);
203 if (searchableValue != null) {
204 searchStorageValues.add(searchableValue);
205 }
206 }
207 }
208 } catch (XPathExpressionException e) {
209
210
211
212 String searchValue = (String) xpath.evaluate(xpathExpression, DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
213 new InputSource(new BufferedReader(new StringReader(documentWithContent.getDocumentContent().getFullContent())))).getDocumentElement(), XPathConstants.STRING);
214 String value = null;
215 if (StringUtils.isNotBlank(searchValue)) {
216 value = searchValue;
217 }
218 DocumentAttribute searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), value);
219 if (searchableValue != null) {
220 searchStorageValues.add(searchableValue);
221 }
222 }
223 }
224 } catch (XPathExpressionException e) {
225 LOG.error("error in isMatch ", e);
226 throw new RuntimeException("Error trying to find xml content with xpath expressions: " + findXpathExpression + " or " + xpathExpression, e);
227 } catch (Exception e){
228 LOG.error("error parsing docContent: " + documentWithContent.getDocumentContent(), e);
229 throw new RuntimeException("Error trying to parse docContent: " + documentWithContent.getDocumentContent(), e);
230 }
231 }
232 }
233 } catch (XPathExpressionException e) {
234 LOG.error("error in getSearchStorageValues ", e);
235 throw new RuntimeException("Error trying to find xml content with xpath expression: " + findField, e);
236 }
237 return searchStorageValues;
238 }
239
240 private DocumentAttribute setupSearchableAttributeValue(String dataType, String key, String value) {
241 SearchableAttributeValue attValue = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(
242 dataType);
243 if (attValue == null) {
244 String errorMsg = "Cannot find a SearchableAttributeValue associated with the data type '" + dataType + "'";
245 LOG.error("setupSearchableAttributeValue() " + errorMsg);
246 throw new RuntimeException(errorMsg);
247 }
248 value = (value != null) ? value.trim() : null;
249 if ( (StringUtils.isNotBlank(value)) && (!attValue.isPassesDefaultValidation(value)) ) {
250 String errorMsg = "SearchableAttributeValue with the data type '" + dataType + "', key '" + key + "', and value '" + value + "' does not pass default validation and cannot be saved to the database";
251 LOG.error("setupSearchableAttributeValue() " + errorMsg);
252 throw new RuntimeException(errorMsg);
253 }
254 attValue.setSearchableAttributeKey(key);
255 attValue.setupAttributeValue(value);
256 return attValue.toDocumentAttribute();
257 }
258
259 @Override
260 public List<RemotableAttributeField> getSearchFields(ExtensionDefinition extensionDefinition, String documentTypeName) {
261
262 List<RemotableAttributeField> searchFields = new ArrayList<RemotableAttributeField>();
263 List<SearchableAttributeValue> searchableAttributeValues = DocumentSearchInternalUtils
264 .getSearchableAttributeValueObjectTypes();
265 NodeList fieldNodeList = getConfigXML(extensionDefinition).getElementsByTagName(FIELD_DEF_E);
266 for (int i = 0; i < fieldNodeList.getLength(); i++) {
267 Node field = fieldNodeList.item(i);
268 NamedNodeMap fieldAttributes = field.getAttributes();
269
270 boolean hasXPathExpression = false;
271
272 String attributeName = fieldAttributes.getNamedItem("name").getNodeValue();
273 String attributeTitle = fieldAttributes.getNamedItem("title").getNodeValue();
274 RemotableAttributeField.Builder fieldBuilder = RemotableAttributeField.Builder.create(attributeName);
275 fieldBuilder.setLongLabel(attributeTitle);
276 RemotableAttributeLookupSettings.Builder attributeLookupSettings = RemotableAttributeLookupSettings.Builder.create();
277 fieldBuilder.setAttributeLookupSettings(attributeLookupSettings);
278
279 for (int j = 0; j < field.getChildNodes().getLength(); j++) {
280 Node childNode = field.getChildNodes().item(j);
281 if ("value".equals(childNode.getNodeName())) {
282 String defaultValue = childNode.getFirstChild().getNodeValue();
283 fieldBuilder.setDefaultValues(Collections.singletonList(defaultValue));
284 } else if ("display".equals(childNode.getNodeName())) {
285
286 String typeValue = null;
287 List<KeyValue> options = new ArrayList<KeyValue>();
288 List<String> selectedOptions = new ArrayList<String>();
289
290
291 for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
292 Node displayChildNode = childNode.getChildNodes().item(k);
293 if ("type".equals(displayChildNode.getNodeName())) {
294 typeValue = displayChildNode.getFirstChild().getNodeValue();
295 } else if ("meta".equals(displayChildNode.getNodeName())) {
296
297 } else if ("values".equals(displayChildNode.getNodeName())) {
298 NamedNodeMap valuesAttributes = displayChildNode.getAttributes();
299
300 if (displayChildNode.getFirstChild() != null) {
301 options.add(new ConcreteKeyValue(displayChildNode.getFirstChild().getNodeValue(), valuesAttributes.getNamedItem("title").getNodeValue()));
302 if (valuesAttributes.getNamedItem("selected") != null) {
303 selectedOptions.add(displayChildNode.getFirstChild().getNodeValue());
304 }
305 } else {
306 options.add(new ConcreteKeyValue("", valuesAttributes.getNamedItem("title").getNodeValue()));
307 }
308 }
309 }
310
311 RemotableAbstractControl.Builder controlBuilder = constructControl(typeValue, options);
312 fieldBuilder.setControl(controlBuilder);
313
314 if ("date".equals(typeValue)) {
315 fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
316 fieldBuilder.setDataType(DataType.DATE);
317 }
318
319 if (selectedOptions != null && !selectedOptions.isEmpty()) {
320 fieldBuilder.setDefaultValues(selectedOptions);
321 }
322 } else if ("visibility".equals(childNode.getNodeName())) {
323 applyVisibility(fieldBuilder, attributeLookupSettings, (Element)childNode);
324 } else if ("searchDefinition".equals(childNode.getNodeName())) {
325 NamedNodeMap searchDefAttributes = childNode.getAttributes();
326
327 String dataTypeValue = (searchDefAttributes.getNamedItem("dataType") == null) ? null : searchDefAttributes.getNamedItem("dataType").getNodeValue();
328 DataType dataType = convertValueToDataType(dataTypeValue);
329 fieldBuilder.setDataType(dataType);
330 if (DataType.DATE == fieldBuilder.getDataType()) {
331 fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
332 }
333
334 boolean isRangeSearchField = isRangeSearchField(searchableAttributeValues, fieldBuilder.getDataType(), searchDefAttributes, childNode);
335 if (!isRangeSearchField) {
336 Boolean caseSensitive = getBooleanValue(searchDefAttributes, "caseSensitive");
337 if (caseSensitive != null) {
338 attributeLookupSettings.setCaseSensitive(caseSensitive);
339 }
340 } else {
341 applyAttributeRange(attributeLookupSettings, fieldBuilder, childNode);
342 }
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368 } else if ("resultColumn".equals(childNode.getNodeName())) {
369 NamedNodeMap columnAttributes = childNode.getAttributes();
370 Node showNode = columnAttributes.getNamedItem("show");
371 boolean isColumnVisible = true;
372 if (showNode != null && showNode.getNodeValue() != null) {
373 isColumnVisible = Boolean.valueOf(showNode.getNodeValue());
374 }
375 attributeLookupSettings.setInResults(isColumnVisible);
376 } else if ("fieldEvaluation".equals(childNode.getNodeName())) {
377 for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
378 Node displayChildNode = childNode.getChildNodes().item(k);
379 if ("xpathexpression".equals(displayChildNode.getNodeName())) {
380 hasXPathExpression = true;
381 break;
382 }
383 }
384 } else if ("lookup".equals(childNode.getNodeName())) {
385 XMLAttributeUtils.establishFieldLookup(fieldBuilder, childNode);
386 }
387 }
388
389 searchFields.add(fieldBuilder.build());
390
391 }
392 return searchFields;
393 }
394
395 private DataType convertValueToDataType(String dataTypeValue) {
396 if (StringUtils.isBlank(dataTypeValue)) {
397 return DataType.STRING;
398 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING.equals(dataTypeValue)) {
399 return DataType.STRING;
400 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE.equals(dataTypeValue)) {
401 return DataType.DATE;
402 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG.equals(dataTypeValue)) {
403 return DataType.LONG;
404 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT.equals(dataTypeValue)) {
405 return DataType.FLOAT;
406 }
407 throw new IllegalArgumentException("Invalid dataTypeValue was given: " + dataTypeValue);
408 }
409
410 private boolean isRangeSearchField(List<SearchableAttributeValue> searchableAttributeValues, DataType dataType, NamedNodeMap searchDefAttributes, Node searchDefNode) {
411 for (SearchableAttributeValue attValue : searchableAttributeValues)
412 {
413 DataType attributeValueDataType = convertValueToDataType(attValue.getAttributeDataType());
414 if (attributeValueDataType == dataType) {
415 return isRangeSearchField(attValue, searchDefAttributes, searchDefNode);
416 }
417 }
418 String errorMsg = "Could not find searchable attribute value for data type '" + dataType + "'";
419 LOG.error("isRangeSearchField(List, String, NamedNodeMap, Node) " + errorMsg);
420 throw new WorkflowRuntimeException(errorMsg);
421 }
422
423 private boolean isRangeSearchField(SearchableAttributeValue searchableAttributeValue, NamedNodeMap searchDefAttributes, Node searchDefNode) {
424 boolean allowRangedSearch = searchableAttributeValue.allowsRangeSearches();
425 Boolean rangeSearchBoolean = getBooleanValue(searchDefAttributes, "rangeSearch");
426 boolean rangeSearch = (rangeSearchBoolean != null) && rangeSearchBoolean;
427 Node rangeDefinition = getPotentialChildNode(searchDefNode, "rangeDefinition");
428 return ( (allowRangedSearch) && ((rangeDefinition != null) || (rangeSearch)) );
429 }
430
431 private void applyAttributeRange(RemotableAttributeLookupSettings.Builder attributeLookupSettings, RemotableAttributeField.Builder fieldBuilder, Node searchDefinitionNode) {
432 NamedNodeMap searchDefAttributes = searchDefinitionNode.getAttributes();
433 Node rangeDefinitionNode = getPotentialChildNode(searchDefinitionNode, "rangeDefinition");
434 String lowerBoundDefaultName = KewApiConstants.SearchableAttributeConstants.RANGE_LOWER_BOUND_PROPERTY_PREFIX + fieldBuilder.getName();
435 String upperBoundDefaultName = KewApiConstants.SearchableAttributeConstants.RANGE_UPPER_BOUND_PROPERTY_PREFIX + fieldBuilder.getName();
436 attributeLookupSettings.setRanged(true);
437 attributeLookupSettings.setLowerBoundName(lowerBoundDefaultName);
438 attributeLookupSettings.setUpperBoundName(upperBoundDefaultName);
439 if (rangeDefinitionNode != null) {
440 NamedNodeMap rangeDefinitionAttributes = rangeDefinitionNode.getAttributes();
441 NamedNodeMap lowerBoundNodeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "lower");
442 NamedNodeMap upperBoundNodeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "upper");
443
444 RangeBound lowerRangeBound = determineRangeBoundProperties(searchDefAttributes, rangeDefinitionAttributes, lowerBoundNodeAttributes);
445 if (lowerRangeBound != null) {
446 if (lowerRangeBound.inclusive != null) {
447 attributeLookupSettings.setLowerBoundInclusive(lowerRangeBound.inclusive);
448 }
449 if (StringUtils.isNotBlank(lowerRangeBound.label)) {
450 attributeLookupSettings.setLowerBoundLabel(lowerRangeBound.label);
451 }
452 }
453 RangeBound upperRangeBound = determineRangeBoundProperties(searchDefAttributes, rangeDefinitionAttributes, upperBoundNodeAttributes);
454 if (upperRangeBound != null) {
455 if (upperRangeBound.inclusive != null) {
456 attributeLookupSettings.setUpperBoundInclusive(upperRangeBound.inclusive);
457 }
458 if (StringUtils.isNotBlank(upperRangeBound.label)) {
459 attributeLookupSettings.setUpperBoundLabel(upperRangeBound.label);
460 }
461 }
462 }
463 }
464
465 private NamedNodeMap getAttributesForPotentialChildNode(Node node, String potentialChildNodeName) {
466 Node testNode = getPotentialChildNode(node, potentialChildNodeName);
467 return testNode != null ? testNode.getAttributes() : null;
468 }
469
470 private Node getPotentialChildNode(Node node, String childNodeName) {
471 if (node != null) {
472 for (int k = 0; k < node.getChildNodes().getLength(); k++) {
473 Node testNode = node.getChildNodes().item(k);
474 if (testNode.getNodeName().equals(childNodeName)) {
475 return testNode;
476 }
477 }
478 }
479 return null;
480 }
481
482 private RangeBound determineRangeBoundProperties(NamedNodeMap searchDefinitionAttributes, NamedNodeMap rangeDefinitionAttributes, NamedNodeMap rangeBoundAttributes) {
483 RangeBound rangeBound = new RangeBound();
484 rangeBound.label = getPotentialRangeBoundLabelFromAttributes(rangeBoundAttributes);
485 List<NamedNodeMap> namedNodeMapsByImportance = new ArrayList<NamedNodeMap>();
486 namedNodeMapsByImportance.add(rangeBoundAttributes);
487 namedNodeMapsByImportance.add(rangeDefinitionAttributes);
488 namedNodeMapsByImportance.add(searchDefinitionAttributes);
489 rangeBound.inclusive = getBooleanWithPotentialOverrides(namedNodeMapsByImportance, "inclusive");
490 return rangeBound;
491 }
492
493 private String getPotentialRangeBoundLabelFromAttributes(NamedNodeMap rangeBoundAttributes) {
494 if (rangeBoundAttributes != null) {
495 String boundLabel = (rangeBoundAttributes.getNamedItem("label") == null) ? null : rangeBoundAttributes.getNamedItem("label").getNodeValue();
496 if (!StringUtils.isBlank(boundLabel)) {
497 return boundLabel;
498 }
499 }
500 return null;
501 }
502
503 private Boolean getBooleanWithPotentialOverrides(List<NamedNodeMap> namedNodeMapsByImportance, String attributeName) {
504 for (NamedNodeMap aNamedNodeMapsByImportance : namedNodeMapsByImportance) {
505 NamedNodeMap nodeMap = (NamedNodeMap) aNamedNodeMapsByImportance;
506 Boolean booleanValue = getBooleanValue(nodeMap, attributeName);
507 if (booleanValue != null) {
508 return booleanValue;
509 }
510 }
511 return null;
512 }
513
514 private Boolean getBooleanValue(NamedNodeMap nodeMap, String attributeName) {
515 String nodeValue = getStringValue(nodeMap, attributeName);
516 if (nodeValue != null) {
517 return Boolean.valueOf(nodeValue);
518 }
519 return null;
520 }
521
522 private String getStringValue(NamedNodeMap nodeMap, String attributeName) {
523 if (nodeMap == null || nodeMap.getNamedItem(attributeName) == null || StringUtils.isBlank(nodeMap.getNamedItem(attributeName).getNodeValue())) {
524 return null;
525 }
526 return nodeMap.getNamedItem(attributeName).getNodeValue();
527 }
528
529 private void applyVisibility(RemotableAttributeField.Builder fieldBuilder, RemotableAttributeLookupSettings.Builder attributeLookupSettings, Element visibilityElement) {
530 for (int vIndex = 0; vIndex < visibilityElement.getChildNodes().getLength(); vIndex++) {
531 Node visibilityChildNode = visibilityElement.getChildNodes().item(vIndex);
532 if (visibilityChildNode.getNodeType() == Node.ELEMENT_NODE) {
533 boolean visible = true;
534 NamedNodeMap visibilityAttributes = visibilityChildNode.getAttributes();
535 Node visibleNode = visibilityAttributes.getNamedItem("visible");
536 if (visibleNode != null && visibleNode.getNodeValue() != null) {
537 visible = Boolean.valueOf(visibleNode.getNodeValue());
538 } else {
539 NodeList visibilityDecls = visibilityChildNode.getChildNodes();
540 for (int vdIndex = 0; vdIndex < visibilityDecls.getLength(); vdIndex++) {
541 Node visibilityDecl = visibilityDecls.item(vdIndex);
542 if (visibilityDecl.getNodeType() == Node.ELEMENT_NODE) {
543 boolean hasIsMemberOfGroupElement = false;
544 String groupName = null;
545 String groupNamespace = null;
546 if (XmlConstants.IS_MEMBER_OF_GROUP.equals(visibilityDecl.getNodeName())) {
547 hasIsMemberOfGroupElement = true;
548 groupName = Utilities.substituteConfigParameters(visibilityDecl.getTextContent()).trim();
549 groupNamespace = Utilities.substituteConfigParameters(((Element)visibilityDecl).getAttribute(XmlConstants.NAMESPACE)).trim();
550 }
551 else if (XmlConstants.IS_MEMBER_OF_WORKGROUP.equals(visibilityDecl.getNodeName())) {
552 LOG.warn((new StringBuilder()).append("Rule Attribute XML is using deprecated element '").append(
553 XmlConstants.IS_MEMBER_OF_WORKGROUP).append("', please use '").append(XmlConstants.IS_MEMBER_OF_GROUP).append(
554 "' instead.").toString());
555 hasIsMemberOfGroupElement = true;
556 String workgroupName = Utilities.substituteConfigParameters(visibilityDecl.getFirstChild().getNodeValue());
557 groupNamespace = Utilities.parseGroupNamespaceCode(workgroupName);
558 groupName = Utilities.parseGroupName(workgroupName);
559 }
560 if (hasIsMemberOfGroupElement) {
561 UserSession session = GlobalVariables.getUserSession();
562 if (session == null) {
563 throw new WorkflowRuntimeException("UserSession is null! Attempted to render the searchable attribute outside of an established session.");
564 }
565 GroupService groupService = KimApiServiceLocator.getGroupService();
566
567 Group group = groupService.getGroupByNameAndNamespaceCode(groupNamespace, groupName);
568 visible = group == null ? false : groupService.isMemberOfGroup(session.getPerson().getPrincipalId(), group.getId());
569 }
570 }
571 }
572 }
573 String type = visibilityChildNode.getNodeName();
574 if ("field".equals(type) || "fieldAndColumn".equals(type)) {
575
576 if (!visible) {
577 fieldBuilder.setControl(RemotableHiddenInput.Builder.create());
578 }
579 }
580 if ("column".equals(type) || "fieldAndColumn".equals(type)) {
581 attributeLookupSettings.setInResults(visible);
582 }
583 }
584 }
585 }
586
587 private RemotableAbstractControl.Builder constructControl(String type, List<KeyValue> options) {
588
589 RemotableAbstractControl.Builder control = null;
590 Map<String, String> optionMap = new HashMap<String, String>();
591 for (KeyValue option : options) {
592 optionMap.put(option.getKey(), option.getValue());
593 }
594 if ("text".equals(type) || "date".equals(type)) {
595 control = RemotableTextInput.Builder.create();
596 } else if ("select".equals(type)) {
597 control = RemotableSelect.Builder.create(optionMap);
598 } else if ("radio".equals(type)) {
599 control = RemotableRadioButtonGroup.Builder.create(optionMap);
600 } else if ("hidden".equals(type)) {
601 control = RemotableHiddenInput.Builder.create();
602 } else if ("multibox".equals(type)) {
603 RemotableSelect.Builder builder = RemotableSelect.Builder.create(optionMap);
604 builder.setMultiple(true);
605 control = builder;
606 } else {
607 throw new IllegalArgumentException("Illegal field type found: " + type);
608 }
609 return control;
610
611 }
612
613 @Override
614 public List<RemotableAttributeError> validateDocumentAttributeCriteria(ExtensionDefinition extensionDefinition, DocumentSearchCriteria documentSearchCriteria) {
615 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
616
617 XPath xpath = XPathHelper.newXPath();
618 String findField = "//searchingConfig/" + FIELD_DEF_E;
619 try {
620 NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(extensionDefinition), XPathConstants.NODESET);
621 if (nodes == null) {
622
623 LOG.warn("Could not find any field definitions (<" + FIELD_DEF_E + ">) or possibly a searching configuration (<searchingConfig>) for this XMLSearchAttribute");
624 } else {
625 for (int i = 0; i < nodes.getLength(); i++) {
626 Node field = nodes.item(i);
627 NamedNodeMap fieldAttributes = field.getAttributes();
628 String fieldDefName = fieldAttributes.getNamedItem("name").getNodeValue();
629 String fieldDefTitle = ((fieldAttributes.getNamedItem("title")) != null) ? fieldAttributes.getNamedItem("title").getNodeValue() : "";
630
631
632 boolean rangeMemberInSearchParams = false;
633
634 Map<String, List<String>> documentAttributeValues = documentSearchCriteria.getDocumentAttributeValues();
635 if (documentAttributeValues != null) {
636
637
638
639 String lowerBoundFieldDefName = KewApiConstants.SearchableAttributeConstants.RANGE_LOWER_BOUND_PROPERTY_PREFIX + fieldDefName;
640 String upperBoundFieldDefName = KewApiConstants.SearchableAttributeConstants.RANGE_UPPER_BOUND_PROPERTY_PREFIX + fieldDefName;
641 List<String> lowerBoundValues = documentAttributeValues.get(lowerBoundFieldDefName);
642 rangeMemberInSearchParams |= CollectionUtils.isNotEmpty(lowerBoundValues) && StringUtils.isNotBlank(lowerBoundValues.get(0));
643 List<String> upperBoundValues = documentAttributeValues.get(upperBoundFieldDefName);
644 rangeMemberInSearchParams |= CollectionUtils.isNotEmpty(upperBoundValues) && StringUtils.isNotBlank(upperBoundValues.get(0));
645
646 List<String> testObject = documentAttributeValues.get(fieldDefName);
647 if (testObject != null || rangeMemberInSearchParams) {
648
649
650 if (!rangeMemberInSearchParams) {
651 if (testObject.size() == 1) {
652 String stringVariable = testObject.get(0);
653 if (StringUtils.isBlank(stringVariable)) {
654
655 continue;
656 }
657 } else {
658 boolean allAreBlank = true;
659 for (String testString : testObject) {
660 if (StringUtils.isNotBlank(testString)) {
661 allAreBlank = false;
662 break;
663 }
664 }
665 if (allAreBlank) {
666
667 continue;
668 }
669 }
670 }
671 String findXpathExpressionPrefix = "//searchingConfig/" + FIELD_DEF_E + "[@name='" + fieldDefName + "']";
672 Node searchDefNode = (Node) xpath.evaluate(findXpathExpressionPrefix + "/searchDefinition", getConfigXML(extensionDefinition), XPathConstants.NODE);
673 NamedNodeMap searchDefAttributes = null;
674 String fieldDataType = null;
675 if (searchDefNode != null) {
676
677 searchDefAttributes = searchDefNode.getAttributes();
678 if (searchDefAttributes.getNamedItem("dataType") != null) {
679 fieldDataType = searchDefAttributes.getNamedItem("dataType").getNodeValue();
680 }
681
682 }
683 if (org.apache.commons.lang.StringUtils.isEmpty(fieldDataType)) {
684 fieldDataType = KewApiConstants.SearchableAttributeConstants.DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
685 }
686
687 SearchableAttributeValue attributeValue = DocumentSearchInternalUtils
688 .getSearchableAttributeValueByDataTypeString(fieldDataType);
689 if (attributeValue == null) {
690 String errorMsg = "Cannot find SearchableAttributeValue for field data type '" + fieldDataType + "'";
691 LOG.error("validateUserSearchInputs() " + errorMsg);
692 throw new RuntimeException(errorMsg);
693 }
694
695 if (rangeMemberInSearchParams) {
696
697 NamedNodeMap lowerBoundRangeAttributes = null;
698 NamedNodeMap upperBoundRangeAttributes = null;
699 Node rangeDefinitionNode = getPotentialChildNode(searchDefNode, "rangeDefinition");
700 NamedNodeMap rangeDefinitionAttributes = rangeDefinitionNode != null ? rangeDefinitionNode.getAttributes() : null;
701
702 String lowerBoundValue = null;
703 if (CollectionUtils.isNotEmpty(lowerBoundValues)) {
704 if (lowerBoundValues.size() > 1) {
705 throw new WorkflowRuntimeException("Encountered an illegal lower bound with more then one value for field: " + fieldDefName);
706 }
707 lowerBoundValue = lowerBoundValues.get(0);
708 }
709 String upperBoundValue = null;
710 if (CollectionUtils.isNotEmpty(upperBoundValues)) {
711 if (upperBoundValues.size() > 1) {
712 throw new WorkflowRuntimeException("Encountered an illegal upper bound with more then one value for field: " + fieldDefName);
713 }
714 upperBoundValue = upperBoundValues.get(0);
715 }
716
717 if (StringUtils.isNotBlank(lowerBoundValue)) {
718 lowerBoundRangeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "lower");
719 errors.addAll(performValidation(extensionDefinition, attributeValue,
720 lowerBoundFieldDefName, lowerBoundValue, constructRangeFieldErrorPrefix(fieldDefTitle,lowerBoundRangeAttributes), findXpathExpressionPrefix));
721 }
722 if (StringUtils.isNotBlank(upperBoundValue)) {
723 upperBoundRangeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "upper");
724 errors.addAll(performValidation(extensionDefinition, attributeValue,
725 upperBoundFieldDefName, upperBoundValue, constructRangeFieldErrorPrefix(fieldDefTitle, upperBoundRangeAttributes), findXpathExpressionPrefix));
726 }
727 if (errors.isEmpty()) {
728 Boolean rangeValid = attributeValue.isRangeValid(lowerBoundValue, upperBoundValue);
729 if (rangeValid != null && !rangeValid) {
730 String lowerLabel = getPotentialRangeBoundLabelFromAttributes(lowerBoundRangeAttributes);
731 String upperLabel = getPotentialRangeBoundLabelFromAttributes(upperBoundRangeAttributes);
732 String errorMsg = "The " + fieldDefTitle + " range is incorrect. The " + (StringUtils.isNotBlank(lowerLabel) ? lowerLabel : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL) + " value entered must come before the " + (StringUtils.isNotBlank(upperLabel) ? upperLabel : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL) + " value";
733 LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
734 errors.add(RemotableAttributeError.Builder.create(fieldDefName, errorMsg).build());
735 }
736 }
737
738 } else {
739 List<String> enteredValue = documentAttributeValues.get(fieldDefName);
740 if (enteredValue.size() == 1) {
741 String stringVariable = enteredValue.get(0);
742 errors.addAll(performValidation(extensionDefinition, attributeValue, fieldDefName, stringVariable, fieldDefTitle, findXpathExpressionPrefix));
743 } else {
744 for (String stringVariable : enteredValue) {
745 errors.addAll(performValidation(extensionDefinition, attributeValue, fieldDefName, stringVariable, "One value for " + fieldDefTitle, findXpathExpressionPrefix));
746 }
747
748 }
749 }
750 }
751 }
752 }
753 }
754 } catch (XPathExpressionException e) {
755 LOG.error("error in validateUserSearchInputs ", e);
756 throw new RuntimeException("Error trying to find xml content with xpath expression: " + findField, e);
757 }
758 return errors;
759 }
760
761 private String constructRangeFieldErrorPrefix(String fieldDefLabel, NamedNodeMap rangeBoundAttributes) {
762 String potentialLabel = getPotentialRangeBoundLabelFromAttributes(rangeBoundAttributes);
763 if ( (StringUtils.isNotBlank(potentialLabel)) && (StringUtils.isNotBlank(fieldDefLabel)) ) {
764 return fieldDefLabel + " " + potentialLabel + " Field";
765 } else if (StringUtils.isNotBlank(fieldDefLabel)) {
766 return fieldDefLabel + " Range Field";
767 } else if (StringUtils.isNotBlank(potentialLabel)) {
768 return "Range Field " + potentialLabel + " Field";
769 }
770 return null;
771 }
772
773 private List<RemotableAttributeError> performValidation(ExtensionDefinition extensionDefinition, SearchableAttributeValue attributeValue, String fieldDefName, String enteredValue, String errorMessagePrefix, String findXpathExpressionPrefix) throws XPathExpressionException {
774 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
775 XPath xpath = XPathHelper.newXPath();
776 if ( attributeValue.allowsWildcards()) {
777 enteredValue = enteredValue.replaceAll(KewApiConstants.SearchableAttributeConstants.SEARCH_WILDCARD_CHARACTER_REGEX_ESCAPED, "");
778 }
779 if (!attributeValue.isPassesDefaultValidation(enteredValue)) {
780 errorMessagePrefix = (StringUtils.isNotBlank(errorMessagePrefix)) ? errorMessagePrefix : "Field";
781 String errorMsg = errorMessagePrefix + " with value '" + enteredValue + "' does not conform to standard validation for field type.";
782 LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
783 errors.add(RemotableAttributeError.Builder.create(fieldDefName, errorMsg).build());
784 } else {
785 String findValidation = findXpathExpressionPrefix + "/validation/regex";
786 String regex = (String) xpath.evaluate(findValidation, getConfigXML(extensionDefinition), XPathConstants.STRING);
787 if (!org.apache.commons.lang.StringUtils.isEmpty(regex)) {
788 Pattern pattern = Pattern.compile(regex);
789 Matcher matcher = pattern.matcher(enteredValue);
790 if (!matcher.matches()) {
791 String findErrorMessage = findXpathExpressionPrefix + "/validation/message";
792 String message = (String) xpath.evaluate(findErrorMessage, getConfigXML(extensionDefinition), XPathConstants.STRING);
793 errors.add(RemotableAttributeError.Builder.create(fieldDefName, message).build());
794 }
795 }
796 }
797 return errors;
798 }
799
800 public Element getConfigXML(ExtensionDefinition extensionDefinition) {
801 try {
802 String xmlConfigData = extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
803 return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(xmlConfigData)))).getDocumentElement();
804 } catch (Exception e) {
805 String ruleAttrStr = (extensionDefinition == null ? null : extensionDefinition.getName());
806 LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
807 throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
808 }
809 }
810
811
812
813
814
815 private static final class RangeBound {
816 Boolean inclusive;
817 String label;
818 }
819
820 }