View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * Implementation of a {@code SearchableAttribute} whose configuration is driven from XML.
77   *
78   * @author Kuali Rice Team (rice.collab@kuali.org)
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  				// Custom doc content in the searchingConfig xml.
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 				// Standard doc content if no doc content is found in the searchingConfig xml.
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                                 // being that this is the standard xml attribute we will return the key with an empty value
189                                 // so we can find it from a doc search using this key
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                                 //try for a string being returned from the expression.  This
210                                 //seems like a poor way to determine our expression return type but
211                                 //it's all I can come up with at the moment.
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                             // this is to allow an empty drop down choice and can probably implemented in a better way
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                     // data type operations
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                      TODO - KULRICE-5737 - Figure out how to handle these formatters
348 
349                      String formatterClass = (searchDefAttributes.getNamedItem("formatterClass") == null) ? null : searchDefAttributes.getNamedItem("formatterClass").getNodeValue();
350                      if (!StringUtils.isEmpty(formatterClass)) {
351                      try {
352                      myField.setFormatter((Formatter)Class.forName(formatterClass).newInstance());
353                      } catch (InstantiationException e) {
354                      LOG.error("Unable to get new instance of formatter class: " + formatterClass);
355                      throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
356                      }
357                      catch (IllegalAccessException e) {
358                      LOG.error("Unable to get new instance of formatter class: " + formatterClass);
359                      throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
360                      } catch (ClassNotFoundException e) {
361                      LOG.error("Unable to find formatter class: " + formatterClass);
362                      throw new RuntimeException("Unable to find formatter class: " + formatterClass);
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             // below methods allow for nullable attribute NamedNodeMaps
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())) { // Found an "isMemberOfGroup" element.
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())) { // Found a deprecated "isMemberOfWorkgroup" element.
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) { // Found one of the "isMemberOf..." elements.
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 					// if it's not visible, coerce this field to a hidden type
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 				// no field definitions is de facto valid
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                     // check for range search members in the parameter map
632                     boolean rangeMemberInSearchParams = false;
633 
634                     Map<String, List<String>> documentAttributeValues = documentSearchCriteria.getDocumentAttributeValues();
635                     if (documentAttributeValues != null) {
636 
637                         // TODO - KULRICE-5630 - I'm pretty sure this *won't* work because we don't store lower and upper bound keys in the document attributes
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                             // check to see if we need to process this field at all
650                             if (!rangeMemberInSearchParams) {
651                                 if (testObject.size() == 1) {
652                                     String stringVariable = testObject.get(0);
653                                     if (StringUtils.isBlank(stringVariable)) {
654                                         // field is not multi value and is empty... skip it
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                                         // field is multivalue but all values are blank... skip it
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             					// get the data type from the xml
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         					// get the searchable attribute value by using the data type
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      * Simple structure for internal usage that includes a case sensitive indicator and label value for one end of
813      * a bounded range.
814      */
815     private static final class RangeBound {
816         Boolean inclusive;
817         String label;
818     }
819 
820 }