View Javadoc

1   /*
2    * Copyright 2005-2007 The Kuali Foundation
3    *
4    *
5    * Licensed under the Educational Community License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.opensource.org/licenses/ecl2.php
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.kuali.rice.kew.docsearch.xml;
18  
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.util.KeyLabelPair;
21  import org.kuali.rice.kew.attribute.XMLAttributeUtils;
22  import org.kuali.rice.kew.docsearch.DocSearchUtils;
23  import org.kuali.rice.kew.docsearch.DocumentSearchContext;
24  import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
25  import org.kuali.rice.kew.exception.WorkflowRuntimeException;
26  import org.kuali.rice.kew.rule.WorkflowAttributeValidationError;
27  import org.kuali.rice.kew.rule.bo.RuleAttribute;
28  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
29  import org.kuali.rice.kew.util.Utilities;
30  import org.kuali.rice.kew.util.XmlHelper;
31  import org.kuali.rice.kew.web.session.UserSession;
32  import org.kuali.rice.kew.xml.XmlConstants;
33  import org.kuali.rice.kim.service.KIMServiceLocator;
34  import org.kuali.rice.kns.web.format.Formatter;
35  import org.kuali.rice.kns.web.ui.Field;
36  import org.kuali.rice.kns.web.ui.Row;
37  import org.w3c.dom.*;
38  import org.xml.sax.InputSource;
39  
40  import javax.xml.parsers.DocumentBuilderFactory;
41  import javax.xml.xpath.XPath;
42  import javax.xml.xpath.XPathConstants;
43  import javax.xml.xpath.XPathExpressionException;
44  import java.io.BufferedReader;
45  import java.io.StringReader;
46  import java.util.*;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  
51  /**
52   * implementation of {@link GenericXMLSearchableAttribute}.
53   *
54   * @author Kuali Rice Team (rice.collab@kuali.org)
55   */
56  public class StandardGenericXMLSearchableAttribute implements GenericXMLSearchableAttribute {
57  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLSearchableAttribute.class);
58  
59      private static final String FIELD_DEF_E = "fieldDef";
60  
61  	private Map paramMap = new HashMap();
62  	private RuleAttribute ruleAttribute;
63  	private List<Row> searchRows = new ArrayList<Row>();
64  
65  	public void setRuleAttribute(RuleAttribute ruleAttribute) {
66  		this.ruleAttribute = ruleAttribute;
67  	}
68  
69  	public void setParamMap(Map paramMap) {
70  		this.paramMap = paramMap;
71  	}
72  
73  	public Map getParamMap() {
74  		return paramMap;
75  	}
76  
77  	public String getSearchContent(DocumentSearchContext documentSearchContext) {
78  		XPath xpath = XPathHelper.newXPath();
79  		String findDocContent = "//searchingConfig/xmlSearchContent";
80  		try {
81  			Node xmlDocumentContent = (Node) xpath.evaluate(findDocContent, getConfigXML(), XPathConstants.NODE);
82  			if (xmlDocumentContent != null && xmlDocumentContent.hasChildNodes()) {
83  				// Custom doc content in the searchingConfig xml.
84  				String docContent = "";
85  				NodeList customNodes = xmlDocumentContent.getChildNodes();
86  				for (int i = 0; i < customNodes.getLength(); i++) {
87  					Node childNode = customNodes.item(i);
88  					docContent += XmlHelper.writeNode(childNode);
89  				}
90  				String findField = "//searchingConfig/" + FIELD_DEF_E;
91  				NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(), XPathConstants.NODESET);
92  				if (nodes == null || nodes.getLength() == 0) {
93  					return "";
94  				}
95  				for (int i = 0; i < nodes.getLength(); i++) {
96  					Node field = nodes.item(i);
97  					NamedNodeMap fieldAttributes = field.getAttributes();
98  					if (getParamMap() != null && !Utilities.isEmpty((String) getParamMap().get(fieldAttributes.getNamedItem("name").getNodeValue()))) {
99  						docContent = docContent.replaceAll("%" + fieldAttributes.getNamedItem("name").getNodeValue() + "%", (String) getParamMap().get(fieldAttributes.getNamedItem("name").getNodeValue()));
100 					}
101 				}
102 				return docContent;
103 			} else {
104 				// Standard doc content if no doc content is found in the searchingConfig xml.
105 				StringBuffer documentContent = new StringBuffer("<xmlRouting>");
106 				String findField = "//searchingConfig/" + FIELD_DEF_E;
107 				NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(), XPathConstants.NODESET);
108 				if (nodes == null || nodes.getLength() == 0) {
109 					return "";
110 				}
111 				for (int i = 0; i < nodes.getLength(); i++) {
112 					Node field = nodes.item(i);
113 					NamedNodeMap fieldAttributes = field.getAttributes();
114 					if (getParamMap() != null && !Utilities.isEmpty((String) getParamMap().get(fieldAttributes.getNamedItem("name").getNodeValue()))) {
115 						documentContent.append("<field name=\"");
116 						documentContent.append(fieldAttributes.getNamedItem("name").getNodeValue());
117 						documentContent.append("\"><value>");
118 						documentContent.append((String) getParamMap().get(fieldAttributes.getNamedItem("name").getNodeValue()));
119 						documentContent.append("</value></field>");
120 					}
121 				}
122 				documentContent.append("</xmlRouting>");
123 				return documentContent.toString();
124 			}
125 		} catch (XPathExpressionException e) {
126 			LOG.error("error in getSearchContent ", e);
127 			throw new RuntimeException("Error trying to find xml content with xpath expression", e);
128 		} catch (Exception e) {
129 			LOG.error("error in getSearchContent attempting to find xml search content", e);
130 			throw new RuntimeException("Error trying to get xml search content.", e);
131 		}
132 	}
133 
134 	public List getSearchStorageValues(DocumentSearchContext documentSearchContext) {
135 		List<SearchableAttributeValue> searchStorageValues = new ArrayList<SearchableAttributeValue>();
136 		Document document;
137         if (StringUtils.isBlank(documentSearchContext.getDocumentContent())) {
138             LOG.warn("Empty Document Content found '" + documentSearchContext.getDocumentContent() + "'");
139             return searchStorageValues;
140         }
141 		try {
142 			document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
143 					new InputSource(new BufferedReader(new StringReader(documentSearchContext.getDocumentContent()))));
144 		} catch (Exception e){
145 			LOG.error("error parsing docContent: "+documentSearchContext.getDocumentContent(), e);
146 			throw new RuntimeException("Error trying to parse docContent: "+documentSearchContext.getDocumentContent(), e);
147 		}
148 		XPath xpath = XPathHelper.newXPath(document);
149 		String findField = "//searchingConfig/" + FIELD_DEF_E;
150 		try {
151 			NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(), XPathConstants.NODESET);
152             if (nodes == null) {
153                 LOG.error("Could not find searching configuration (<searchingConfig>) for this XMLSearchAttribute");
154             } else {
155 
156     			for (int i = 0; i < nodes.getLength(); i++) {
157     				Node field = nodes.item(i);
158     				NamedNodeMap fieldAttributes = field.getAttributes();
159 
160     				String findXpathExpressionPrefix = "//searchingConfig/" + FIELD_DEF_E + "[@name='" + fieldAttributes.getNamedItem("name").getNodeValue() + "']";
161     				String findDataTypeXpathExpression = findXpathExpressionPrefix + "/searchDefinition/@dataType";
162     				String findXpathExpression = findXpathExpressionPrefix + "/fieldEvaluation/xpathexpression";
163     				String fieldDataType = null;
164     				String xpathExpression = null;
165     				try {
166                         fieldDataType = (String) xpath.evaluate(findDataTypeXpathExpression, getConfigXML(), XPathConstants.STRING);
167     					if (Utilities.isEmpty(fieldDataType)) {
168     						fieldDataType = DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
169     					}
170     				    xpathExpression = (String) xpath.evaluate(findXpathExpression, getConfigXML(), XPathConstants.STRING);
171     					if (!Utilities.isEmpty(xpathExpression)) {
172 
173                             try {
174                                 NodeList searchValues = (NodeList) xpath.evaluate(xpathExpression, document.getDocumentElement(), XPathConstants.NODESET);
175                               //being that this is the standard xml attribute we will return the key with an empty value
176                                 // so we can find it from a doc search using this key
177                                 if (searchValues.getLength() == 0) {
178                                 	SearchableAttributeValue searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), null);
179                                 	if (searchableValue != null) {
180                                         searchStorageValues.add(searchableValue);
181                                 	}
182                                 } else {
183                                 	for (int j = 0; j < searchValues.getLength(); j++) {
184                                         Node searchValue = searchValues.item(j);
185                                         String value = null;
186                                         if (searchValue.getFirstChild() != null && (!StringUtils.isEmpty(searchValue.getFirstChild().getNodeValue()))) {
187                                         	value = searchValue.getFirstChild().getNodeValue();
188                                         }
189                                     	SearchableAttributeValue searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), value);
190                                     	if (searchableValue != null) {
191                                             searchStorageValues.add(searchableValue);
192                                     	}
193                                     }
194                                 }
195                             } catch (XPathExpressionException e) {
196                                 //try for a string being returned from the expression.  This
197                                 //seems like a poor way to determine our expression return type but
198                                 //it's all I can come up with at the moment.
199                                 String searchValue = (String) xpath.evaluate(xpathExpression, DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
200                                 		new InputSource(new BufferedReader(new StringReader(documentSearchContext.getDocumentContent())))).getDocumentElement(), XPathConstants.STRING);
201                                 String value = null;
202                                 if (StringUtils.isNotBlank(searchValue)) {
203                                     value = searchValue;
204                                 }
205                             	SearchableAttributeValue searchableValue = this.setupSearchableAttributeValue(fieldDataType, fieldAttributes.getNamedItem("name").getNodeValue(), value);
206                             	if (searchableValue != null) {
207                                     searchStorageValues.add(searchableValue);
208                             	}
209                             }
210     					}
211     				} catch (XPathExpressionException e) {
212     					LOG.error("error in isMatch ", e);
213     					throw new RuntimeException("Error trying to find xml content with xpath expressions: " + findXpathExpression + " or " + xpathExpression, e);
214     				} catch (Exception e){
215     					LOG.error("error parsing docContent: "+documentSearchContext.getDocumentContent(), e);
216     					throw new RuntimeException("Error trying to parse docContent: "+documentSearchContext.getDocumentContent(), e);
217     				}
218                 }
219 			}
220 		} catch (XPathExpressionException e) {
221 			LOG.error("error in getSearchStorageValues ", e);
222 			throw new RuntimeException("Error trying to find xml content with xpath expression: " + findField, e);
223 		}
224 		return searchStorageValues;
225 	}
226 
227 	private SearchableAttributeValue setupSearchableAttributeValue(String dataType,String key,String value) {
228 		SearchableAttributeValue attValue = DocSearchUtils.getSearchableAttributeValueByDataTypeString(dataType);
229 		if (attValue == null) {
230 			String errorMsg = "Cannot find a SearchableAttributeValue associated with the data type '" + dataType + "'";
231 		    LOG.error("setupSearchableAttributeValue() " + errorMsg);
232 		    throw new RuntimeException(errorMsg);
233 		}
234         value = (value != null) ? value.trim() : null;
235         if ( (StringUtils.isNotBlank(value)) && (!attValue.isPassesDefaultValidation(value)) ) {
236             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";
237             LOG.error("setupSearchableAttributeValue() " + errorMsg);
238             throw new RuntimeException(errorMsg);
239         }
240 		attValue.setSearchableAttributeKey(key);
241 		attValue.setupAttributeValue(value);
242     	return attValue;
243 	}
244 
245 	public List<Row> getSearchingRows(DocumentSearchContext documentSearchContext) {
246 		if (searchRows.isEmpty()) {
247 			List<SearchableAttributeValue> searchableAttributeValues = DocSearchUtils.getSearchableAttributeValueObjectTypes();
248 			List<Row> rows = new ArrayList<Row>();
249 			NodeList fieldNodeList = getConfigXML().getElementsByTagName(FIELD_DEF_E);
250 			for (int i = 0; i < fieldNodeList.getLength(); i++) {
251 				Node field = fieldNodeList.item(i);
252 				NamedNodeMap fieldAttributes = field.getAttributes();
253 
254 				List<Field> fields = new ArrayList<Field>();
255 				boolean isColumnVisible = true;
256                 boolean hasXPathExpression = false;
257 				Field myField = new Field(fieldAttributes.getNamedItem("name").getNodeValue(), fieldAttributes.getNamedItem("title").getNodeValue());
258 
259 				String quickfinderService = null;
260 				// range search details
261 				Field rangeLowerBoundField = null;
262 				Field rangeUpperBoundField = null;
263 				myField.setUpperCase(true); // this defaults us to case insensitive.
264 				for (int j = 0; j < field.getChildNodes().getLength(); j++) {
265 					Node childNode = field.getChildNodes().item(j);
266 					if ("value".equals(childNode.getNodeName())) {
267 						myField.setPropertyValue(childNode.getFirstChild().getNodeValue());
268 					} else if ("display".equals(childNode.getNodeName())) {
269 						List<KeyLabelPair> options = new ArrayList<KeyLabelPair>();
270                         List<String> selectedOptions = new ArrayList<String>();
271 						for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
272 							Node displayChildNode = childNode.getChildNodes().item(k);
273 							if ("type".equals(displayChildNode.getNodeName())) {
274 								String typeValue = displayChildNode.getFirstChild().getNodeValue();
275 								myField.setFieldType(convertTypeToFieldType(typeValue));
276 								if ("date".equals(typeValue)) {
277 									myField.setDatePicker(Boolean.TRUE);
278 									myField.setFieldDataType(DATA_TYPE_DATE);
279 								}
280 							} else if ("meta".equals(displayChildNode.getNodeName())) {
281 
282 							} else if ("values".equals(displayChildNode.getNodeName())) {
283 								NamedNodeMap valuesAttributes = displayChildNode.getAttributes();
284 //                              this is to allow an empty drop down choice and can probably implemented in a better way
285                                 if (displayChildNode.getFirstChild() != null) {
286                                     options.add(new KeyLabelPair(displayChildNode.getFirstChild().getNodeValue(), valuesAttributes.getNamedItem("title").getNodeValue()));
287                                     if (valuesAttributes.getNamedItem("selected") != null) {
288                                         selectedOptions.add(displayChildNode.getFirstChild().getNodeValue());
289                                     }
290                                 } else {
291                                     options.add(new KeyLabelPair("", valuesAttributes.getNamedItem("title").getNodeValue()));
292                                 }
293 							}
294 						}
295 						if (!options.isEmpty()) {
296 							myField.setFieldValidValues(options);
297                             if (!selectedOptions.isEmpty()) {
298                                 if (Field.MULTI_VALUE_FIELD_TYPES.contains(myField.getFieldType())) {
299                                     String[] newSelectedOptions = new String[selectedOptions.size()];
300                                     int k = 0;
301                                     for (String option : selectedOptions)
302                                     {
303                                         newSelectedOptions[k] = option;
304                                         k++;
305                                     }
306                                     myField.setPropertyValues(newSelectedOptions);
307                                 } else {
308                                     myField.setPropertyValue((String)selectedOptions.get(0));
309                                 }
310                             }
311 						}
312 					} else if ("visibility".equals(childNode.getNodeName())) {
313 						parseVisibility(myField, (Element)childNode);
314 					} else if ("searchDefinition".equals(childNode.getNodeName())) {
315 						NamedNodeMap searchDefAttributes = childNode.getAttributes();
316 						// data type operations
317 						String dataType = (searchDefAttributes.getNamedItem("dataType") == null) ? null : searchDefAttributes.getNamedItem("dataType").getNodeValue();
318 						if (!Utilities.isEmpty(dataType)) {
319 							myField.setFieldDataType(dataType);
320 						} else {
321 							// no data type means we default to String which disallows range search
322 							myField.setFieldDataType(DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME);
323 						}
324 						if (DATA_TYPE_DATE.equalsIgnoreCase(myField.getFieldDataType())) {
325 							myField.setDatePicker(Boolean.TRUE);
326 						}
327 						//if () {
328 						//    myField.setFormatter((Formatter) formatterClass.newInstance());
329 						//}
330 
331 						// figure out if this is a range search
332 						myField.setMemberOfRange(isRangeSearchField(searchableAttributeValues, myField.getFieldDataType(), searchDefAttributes, childNode));
333 						if (!myField.isMemberOfRange()) {
334 							Boolean caseSensitive = getBooleanValue(searchDefAttributes, "caseSensitive");
335 							if (caseSensitive == null) {
336 								caseSensitive = false; // we mimmic the KNS. KNS is case insensitive by default
337 							}
338 							myField.setUpperCase(!caseSensitive);
339 						} else {
340     						// by now we know we have a range that uses the default values at least
341     						// these will be
342     						rangeLowerBoundField = new Field("",DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL);
343     						rangeLowerBoundField.setMemberOfRange(true);
344     						rangeUpperBoundField = new Field("",DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL);
345     						rangeUpperBoundField.setMemberOfRange(true);
346     						setupBoundFields(childNode, rangeLowerBoundField, rangeUpperBoundField);
347                         }
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 					} else if ("resultColumn".equals(childNode.getNodeName())) {
367 						NamedNodeMap columnAttributes = childNode.getAttributes();
368 						Node showNode = columnAttributes.getNamedItem("show");
369 						if (showNode != null && showNode.getNodeValue() != null) {
370 							isColumnVisible = Boolean.valueOf(showNode.getNodeValue());
371 						}
372 						myField.setColumnVisible(isColumnVisible);
373                     } else if ("fieldEvaluation".equals(childNode.getNodeName())) {
374                         for (int k = 0; k < childNode.getChildNodes().getLength(); k++) {
375                             Node displayChildNode = childNode.getChildNodes().item(k);
376                             if ("xpathexpression".equals(displayChildNode.getNodeName())) {
377                                 hasXPathExpression = true;
378                                 break;
379                             }
380                         }
381 					} else if ("lookup".equals(childNode.getNodeName())) {
382 						XMLAttributeUtils.establishFieldLookup(myField, childNode);
383 					}
384 				}
385                 myField.setIndexedForSearch(hasXPathExpression);
386 
387 				if (myField.isMemberOfRange()) {
388 					// we have a ranged search... we need to add the bound fields and NOT the myField object
389 					addRangeFields(RANGE_LOWER_BOUND_PROPERTY_PREFIX, rangeLowerBoundField, myField, rows, quickfinderService);
390 					addRangeFields(RANGE_UPPER_BOUND_PROPERTY_PREFIX, rangeUpperBoundField, myField, rows, quickfinderService);
391 				} else {
392 					fields.add(myField);
393 					if (!myField.getFieldType().equals(Field.HIDDEN)) {
394 						if (myField.isDatePicker()) {
395 							addDatePickerField(fields, myField.getPropertyName());
396 						}
397 					}
398 					rows.add(new Row(fields));
399 				}
400 			}
401 			searchRows = rows;
402 		}
403 		return searchRows;
404 	}
405 
406     private boolean isRangeSearchField(List<SearchableAttributeValue> searchableAttributeValues, String dataType, NamedNodeMap searchDefAttributes, Node searchDefNode) {
407         for (SearchableAttributeValue attValue : searchableAttributeValues)
408         {
409             if (dataType.equalsIgnoreCase(attValue.getAttributeDataType()))
410             {
411                 return isRangeSearchField(attValue, dataType, searchDefAttributes, searchDefNode);
412             }
413         }
414         String errorMsg = "Could not find searchable attribute value for data type '" + dataType + "'";
415         LOG.error("isRangeSearchField(List, String, NamedNodeMap, Node) " + errorMsg);
416         throw new RuntimeException(errorMsg);
417     }
418 
419     private boolean isRangeSearchField(SearchableAttributeValue searchableAttributeValue, String dataType, NamedNodeMap searchDefAttributes, Node searchDefNode) {
420         boolean allowRangedSearch = searchableAttributeValue.allowsRangeSearches();
421         Boolean rangeSearchBoolean = getBooleanValue(searchDefAttributes, "rangeSearch");
422         boolean rangeSearch = (rangeSearchBoolean != null) && rangeSearchBoolean;
423         Node rangeDefinition = getPotentialChildNode(searchDefNode, "rangeDefinition");
424         return ( (allowRangedSearch) && ((rangeDefinition != null) || (rangeSearch)) );
425     }
426 
427     private void setupBoundFields(Node searchDefinitionNode, Field lowerBoundField, Field upperBoundField) {
428         NamedNodeMap searchDefAttributes = searchDefinitionNode.getAttributes();
429         Node rangeDefinitionNode = getPotentialChildNode(searchDefinitionNode, "rangeDefinition");
430         NamedNodeMap rangeDefinitionAttributes = null;
431         NamedNodeMap lowerBoundNodeAttributes = null;
432         NamedNodeMap upperBoundNodeAttributes = null;
433         if (rangeDefinitionNode != null) {
434             rangeDefinitionAttributes = rangeDefinitionNode.getAttributes();
435             lowerBoundNodeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "lower");
436             upperBoundNodeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "upper");
437         }
438         // below methods allow for nullable attribute NamedNodeMaps
439         setupRangeBoundFieldOverridableSettings(searchDefAttributes, rangeDefinitionAttributes, lowerBoundNodeAttributes, lowerBoundField);
440         setupRangeBoundFieldOverridableSettings(searchDefAttributes, rangeDefinitionAttributes, upperBoundNodeAttributes, upperBoundField);
441     }
442 
443 	private void addRangeFields(String propertyPrefix, Field rangeBoundField,Field mainField, List<Row> rows,String quickfinderService) {
444 		List<Field> rangeFields = new ArrayList<Field>();
445 		rangeBoundField.setColumnVisible(mainField.isColumnVisible());
446 		rangeBoundField.setFieldDataType(mainField.getFieldDataType());
447 		rangeBoundField.setFieldHelpUrl(mainField.getFieldHelpUrl());
448 		rangeBoundField.setFieldType(mainField.getFieldType());
449         rangeBoundField.setMainFieldLabel(mainField.getFieldLabel());
450 		rangeBoundField.setFieldValidValues(mainField.getFieldValidValues());
451 		rangeBoundField.setPropertyName(propertyPrefix + mainField.getPropertyName());
452 		rangeBoundField.setQuickFinderClassNameImpl(mainField.getQuickFinderClassNameImpl());
453 		//rangeBoundField.setDefaultLookupableName(mainField.getDefaultLookupableName());
454 		rangeFields.add(rangeBoundField);
455 		if (!mainField.getFieldType().equals(Field.HIDDEN)) {
456 			// disabling the additional quickfinder field for now, should be included as a single field in the KNS instead of 2 as it was in KEW
457 //			if (!Utilities.isEmpty(quickfinderService)) {
458 //				rangeFields.add(new Field("", "", Field.QUICKFINDER, "", "", null, quickfinderService));
459 //			}
460 			if (rangeBoundField.isDatePicker()) {
461 				// variable was set on the bound field
462 				if (rangeBoundField.isDatePicker()) {
463 					addDatePickerField(rangeFields, rangeBoundField.getPropertyName());
464 				}
465 			} else {
466 				if (mainField.isDatePicker()) {
467 					addDatePickerField(rangeFields, rangeBoundField.getPropertyName());
468 				}
469 			}
470 		}
471 		rows.add(new Row(rangeFields));
472 	}
473 
474     private void addDatePickerField(List<Field> fields,String propertyName) {
475         Field Field = new Field(propertyName,"");
476         Field.setDatePicker(true);
477 		fields.add(Field);
478     }
479 
480 	private NamedNodeMap getAttributesForPotentialChildNode(Node node, String potentialChildNodeName) {
481 		Node testNode = getPotentialChildNode(node, potentialChildNodeName);
482 		return (testNode != null) ? testNode.getAttributes() : null;
483 	}
484 
485 	private Node getPotentialChildNode(Node node, String childNodeName) {
486 		if (node != null) {
487 			for (int k = 0; k < node.getChildNodes().getLength(); k++) {
488 				Node testNode = node.getChildNodes().item(k);
489 				if (testNode.getNodeName().equals(childNodeName)) {
490 					return testNode;
491 				}
492 			}
493 		}
494 		return null;
495 	}
496 
497 	private void setupRangeBoundFieldOverridableSettings(NamedNodeMap searchDefinitionAttributes,NamedNodeMap rangeDefinitionAttributes,NamedNodeMap rangeBoundAttributes,Field boundField) {
498         String potentialLabel = getPotentialRangeBoundLabelFromAttributes(rangeBoundAttributes);
499         if (StringUtils.isNotBlank(potentialLabel)) {
500             boundField.setFieldLabel(potentialLabel);
501         }
502 		ArrayList<NamedNodeMap> namedNodeMapsByImportance = new ArrayList<NamedNodeMap>();
503 		namedNodeMapsByImportance.add(rangeBoundAttributes);
504 		namedNodeMapsByImportance.add(rangeDefinitionAttributes);
505 		namedNodeMapsByImportance.add(searchDefinitionAttributes);
506 		Boolean caseSensitive = getBooleanWithPotentialOverrides(namedNodeMapsByImportance, "caseSensitive");
507 		if (caseSensitive == null) {
508 			caseSensitive = false; // we mimmic the KNS. KNS is case insensitive by default
509 		}
510 		boundField.setUpperCase(!caseSensitive);
511 		// TODO: after face-to-face work in december 2008, this was throwing a nullpointerexception for lookups with date pickers
512 		// assuming this code will go away after the document search conversion
513 		Boolean datePickerBoolean = getBooleanWithPotentialOverrides(namedNodeMapsByImportance, "datePicker");
514 		if (datePickerBoolean == null) {
515 			datePickerBoolean = false;
516 		}
517 		boundField.setDatePicker(datePickerBoolean);
518 		boundField.setRangeFieldInclusive(getBooleanWithPotentialOverrides(namedNodeMapsByImportance, "inclusive"));
519 
520 	}
521 
522     private String getPotentialRangeBoundLabelFromAttributes(NamedNodeMap rangeBoundAttributes) {
523         if (rangeBoundAttributes != null) {
524             String boundLabel = (rangeBoundAttributes.getNamedItem("label") == null) ? null : rangeBoundAttributes.getNamedItem("label").getNodeValue();
525             if (!Utilities.isEmpty(boundLabel)) {
526                 return boundLabel;
527             }
528         }
529         return null;
530     }
531 
532 	private Boolean getBooleanWithPotentialOverrides(String attributeName,NamedNodeMap searchDefinitionAttributes,NamedNodeMap rangeDefinitionAttributes,NamedNodeMap rangeBoundAttributes) {
533 		ArrayList<NamedNodeMap> namedNodeMapsByImportance = new ArrayList<NamedNodeMap>();
534 		namedNodeMapsByImportance.add(rangeBoundAttributes);
535 		namedNodeMapsByImportance.add(rangeDefinitionAttributes);
536 		namedNodeMapsByImportance.add(searchDefinitionAttributes);
537 		return getBooleanWithPotentialOverrides(namedNodeMapsByImportance, attributeName);
538 	}
539 
540 	private Boolean getBooleanWithPotentialOverrides(ArrayList<NamedNodeMap> namedNodeMapsByImportance, String attributeName) {
541         for (NamedNodeMap aNamedNodeMapsByImportance : namedNodeMapsByImportance)
542         {
543             NamedNodeMap nodeMap = (NamedNodeMap) aNamedNodeMapsByImportance;
544             Boolean booleanValue = getBooleanValue(nodeMap, attributeName);
545             if (booleanValue != null)
546             {
547                 return booleanValue;
548             }
549         }
550 		return null;
551 	}
552 
553 	private Boolean getBooleanValue(NamedNodeMap nodeMap, String attributeName) {
554 		String nodeValue = getStringValue(nodeMap, attributeName);
555 		if (nodeValue != null) {
556 			return Boolean.valueOf(nodeValue);
557 		}
558 		return null;
559 	}
560 
561 	private String getStringValue(NamedNodeMap nodeMap, String attributeName) {
562 		return ( (nodeMap == null) || (nodeMap.getNamedItem(attributeName) == null) || (Utilities.isEmpty(nodeMap.getNamedItem(attributeName).getNodeValue())) ) ? null : nodeMap.getNamedItem(attributeName).getNodeValue();
563 	}
564 
565 	private void parseVisibility(Field field, Element visibilityElement) {
566 		for (int vIndex = 0; vIndex < visibilityElement.getChildNodes().getLength(); vIndex++) {
567 			Node visibilityChildNode = visibilityElement.getChildNodes().item(vIndex);
568 			if (visibilityChildNode.getNodeType() == Node.ELEMENT_NODE) {
569 				boolean visible = true;
570 				NamedNodeMap visibilityAttributes = visibilityChildNode.getAttributes();
571 				Node visibleNode = visibilityAttributes.getNamedItem("visible");
572 				if (visibleNode != null && visibleNode.getNodeValue() != null) {
573 					visible = Boolean.valueOf(visibleNode.getNodeValue());
574 				} else {
575 					NodeList visibilityDecls = visibilityChildNode.getChildNodes();
576 					for (int vdIndex = 0; vdIndex < visibilityDecls.getLength(); vdIndex++) {
577 						Node visibilityDecl = visibilityDecls.item(vdIndex);
578                         if (visibilityDecl.getNodeType() == Node.ELEMENT_NODE) {
579                         	boolean hasIsMemberOfGroupElement = false;
580                         	String groupName = null;
581                         	String groupNamespace = null;
582                         	if (XmlConstants.IS_MEMBER_OF_GROUP.equals(visibilityDecl.getNodeName())) { // Found an "isMemberOfGroup" element.
583                         		hasIsMemberOfGroupElement = true;
584                         		groupName = Utilities.substituteConfigParameters(visibilityDecl.getTextContent()).trim();
585                         		groupNamespace = Utilities.substituteConfigParameters(((Element)visibilityDecl).getAttribute(XmlConstants.NAMESPACE)).trim();
586                         	}
587                         	else if (XmlConstants.IS_MEMBER_OF_WORKGROUP.equals(visibilityDecl.getNodeName())) { // Found a deprecated "isMemberOfWorkgroup" element.
588                         		LOG.warn((new StringBuilder()).append("Rule Attribute XML is using deprecated element '").append(
589                         				XmlConstants.IS_MEMBER_OF_WORKGROUP).append("', please use '").append(XmlConstants.IS_MEMBER_OF_GROUP).append(
590                         						"' instead.").toString());
591                         		hasIsMemberOfGroupElement = true;
592     							String workgroupName = Utilities.substituteConfigParameters(visibilityDecl.getFirstChild().getNodeValue());
593     							groupNamespace = Utilities.parseGroupNamespaceCode(workgroupName);
594     							groupName = Utilities.parseGroupName(workgroupName);
595     						}
596     						if (hasIsMemberOfGroupElement) { // Found one of the "isMemberOf..." elements.
597     							UserSession session = UserSession.getAuthenticatedUser();
598     							if (session == null) {
599     								throw new WorkflowRuntimeException("UserSession is null!  Attempted to render the searchable attribute outside of an established session.");
600     							}
601     							visible = KIMServiceLocator.getIdentityManagementService().isMemberOfGroup(session.getPerson().getPrincipalId(), groupNamespace, groupName);
602     						}
603                         }
604 					}
605 				}
606 				String type = visibilityChildNode.getNodeName();
607 				if ("field".equals(type) || "fieldAndColumn".equals(type)) {
608 					// if it's not visible, coerce this field to a hidden type
609 					if (!visible) {
610 						field.setFieldType(Field.HIDDEN);
611 					}
612 				}
613 				if ("column".equals(type) || "fieldAndColumn".equals(type)) {
614 					field.setColumnVisible(visible);
615 				}
616 			}
617 
618 		}
619 	}
620 
621 	private String convertTypeToFieldType(String type) {
622 		if ("text".equals(type)) {
623 			return Field.TEXT;
624 		} else if ("select".equals(type)) {
625 			return Field.DROPDOWN;
626 		} else if ("radio".equals(type)) {
627 			return Field.RADIO;
628 		} else if ("quickfinder".equals(type)) {
629 			return Field.QUICKFINDER;
630 		} else if ("hidden".equals(type)) {
631 			return Field.HIDDEN;
632 		} else if ("date".equals(type)) {
633 			return Field.TEXT;
634         } else if ("multibox".equals(type)) {
635             return Field.MULTIBOX;
636         }
637 		throw new IllegalArgumentException("Illegal field type found: " + type);
638 	}
639 
640 	public List validateUserSearchInputs(Map paramMap, DocumentSearchContext documentSearchContext) {
641 		this.paramMap = paramMap;
642 		List<WorkflowAttributeValidationError> errors = new ArrayList<WorkflowAttributeValidationError>();
643 
644 		XPath xpath = XPathHelper.newXPath();
645 		String findField = "//searchingConfig/" + FIELD_DEF_E;
646 		try {
647 			NodeList nodes = (NodeList) xpath.evaluate(findField, getConfigXML(), XPathConstants.NODESET);
648 			if (nodes == null) {
649 				// no field definitions is de facto valid
650 			    LOG.warn("Could not find any field definitions (<" + FIELD_DEF_E + ">) or possibly a searching configuration (<searchingConfig>) for this XMLSearchAttribute");
651 			} else {
652     			for (int i = 0; i < nodes.getLength(); i++) {
653     				Node field = nodes.item(i);
654     				NamedNodeMap fieldAttributes = field.getAttributes();
655 					String fieldDefName = fieldAttributes.getNamedItem("name").getNodeValue();
656                     String fieldDefTitle = ((fieldAttributes.getNamedItem("title")) != null) ? fieldAttributes.getNamedItem("title").getNodeValue() : "";
657                     // check for range search members in the parameter map
658                     boolean rangeMemberInSearchParams = false;
659                     if (getParamMap() != null) {
660                         Object lowerObj = getParamMap().get(RANGE_LOWER_BOUND_PROPERTY_PREFIX + fieldDefName);
661                         if ( (lowerObj != null) && (lowerObj instanceof String) ) {
662                             rangeMemberInSearchParams |= StringUtils.isNotBlank((String) lowerObj);
663                         }
664                         Object upperObj = getParamMap().get(RANGE_UPPER_BOUND_PROPERTY_PREFIX + fieldDefName);
665                         if ( (upperObj != null) && (upperObj instanceof String) ) {
666                             rangeMemberInSearchParams |= StringUtils.isNotBlank((String) upperObj);
667                         }
668                         Object testObject = getParamMap().get(fieldDefName);
669     					if ( (testObject != null) || rangeMemberInSearchParams ) {
670                             // check to see if we need to process this field at all
671                             if (!rangeMemberInSearchParams) {
672                                 if (testObject instanceof String) {
673                                     String stringVariable = (String) testObject;
674                                     if (StringUtils.isBlank(stringVariable)) {
675                                         // field is not multi value and is empty... skip it
676                                         continue;
677                                     }
678                                 } else if (testObject instanceof Collection) {
679                                     Collection stringVariables = (Collection<String>)testObject;
680                                     boolean allAreBlank = true;
681                                     for (Iterator iter = stringVariables.iterator(); iter.hasNext();) {
682                                         String testString = (String) iter.next();
683                                         if (StringUtils.isNotBlank(testString)) {
684                                             allAreBlank = false;
685                                             break;
686                                         }
687                                     }
688                                     if (allAreBlank) {
689                                         // field is multivalue but all values are blank... skip it
690                                         continue;
691                                     }
692                                 } else {
693                                     String errorMessage = "Only String or String[] objects should come from entered parameters of an attribute. Current parameter is '" + testObject.getClass() + "'";
694                                     LOG.error(errorMessage);
695                                     throw new RuntimeException(errorMessage);
696                                 }
697                             }
698                             String findXpathExpressionPrefix = "//searchingConfig/" + FIELD_DEF_E + "[@name='" + fieldDefName + "']";
699         					Node searchDefNode = (Node) xpath.evaluate(findXpathExpressionPrefix + "/searchDefinition", getConfigXML(), XPathConstants.NODE);
700         					NamedNodeMap searchDefAttributes = null;
701             				String fieldDataType = null;
702         					if (searchDefNode != null) {
703             					// get the data type from the xml
704         						searchDefAttributes = searchDefNode.getAttributes();
705         						if (searchDefAttributes.getNamedItem("dataType") != null) {
706             						fieldDataType = searchDefAttributes.getNamedItem("dataType").getNodeValue();
707         						}
708 
709         					}
710         					if (Utilities.isEmpty(fieldDataType)) {
711         						fieldDataType = DEFAULT_SEARCHABLE_ATTRIBUTE_TYPE_NAME;
712         					}
713         					// get the searchable attribute value by using the data type
714         					SearchableAttributeValue attributeValue = DocSearchUtils.getSearchableAttributeValueByDataTypeString(fieldDataType);
715         					if (attributeValue == null) {
716         						String errorMsg = "Cannot find SearchableAttributeValue for field data type '" + fieldDataType + "'";
717         						LOG.error("validateUserSearchInputs() " + errorMsg);
718         						throw new RuntimeException(errorMsg);
719         					}
720 
721         					if (rangeMemberInSearchParams) {
722                                 String lowerBoundFieldDefName = RANGE_LOWER_BOUND_PROPERTY_PREFIX + fieldDefName;
723                                 String upperBoundFieldDefName = RANGE_UPPER_BOUND_PROPERTY_PREFIX + fieldDefName;
724                                 String lowerBoundEnteredValue = null;
725                                 String upperBoundEnteredValue = null;
726                                 NamedNodeMap lowerBoundRangeAttributes = null;
727                                 NamedNodeMap upperBoundRangeAttributes = null;
728         						Node rangeDefinitionNode = getPotentialChildNode(searchDefNode, "rangeDefinition");
729         						NamedNodeMap rangeDefinitionAttributes = (rangeDefinitionNode != null) ? rangeDefinitionNode.getAttributes() : null;
730         						lowerBoundEnteredValue = (String) getParamMap().get(lowerBoundFieldDefName);
731         						upperBoundEnteredValue = (String) getParamMap().get(upperBoundFieldDefName);
732         						if (!Utilities.isEmpty(lowerBoundEnteredValue)) {
733                                     lowerBoundRangeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "lower");
734         							errors.addAll(performValidation(attributeValue,
735         									lowerBoundFieldDefName, lowerBoundEnteredValue, constructRangeFieldErrorPrefix(fieldDefTitle,lowerBoundRangeAttributes), findXpathExpressionPrefix));
736         						}
737                                 if (!Utilities.isEmpty(upperBoundEnteredValue)) {
738                                     upperBoundRangeAttributes = getAttributesForPotentialChildNode(rangeDefinitionNode, "upper");
739         							errors.addAll(performValidation(attributeValue,
740         									upperBoundFieldDefName, upperBoundEnteredValue, constructRangeFieldErrorPrefix(fieldDefTitle,upperBoundRangeAttributes), findXpathExpressionPrefix));
741         						}
742                                 if (errors.isEmpty()) {
743                                     Boolean rangeValid = attributeValue.isRangeValid(lowerBoundEnteredValue, upperBoundEnteredValue);
744                                     if ( (rangeValid != null) && (!rangeValid) ) {
745                                         String lowerLabel = getPotentialRangeBoundLabelFromAttributes(lowerBoundRangeAttributes);
746                                         String upperLabel = getPotentialRangeBoundLabelFromAttributes(upperBoundRangeAttributes);
747                                         String errorMsg = "The " + fieldDefTitle + " range is incorrect.  The " + (StringUtils.isNotBlank(lowerLabel) ? lowerLabel : DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL) + " value entered must come before the " + (StringUtils.isNotBlank(upperLabel) ? upperLabel : DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL) + " value";
748                                         LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
749                                         errors.add(new WorkflowAttributeValidationError(fieldDefName, errorMsg));
750                                     }
751                                 }
752 
753         					} else {
754                                 Object enteredValue = getParamMap().get(fieldDefName);
755                                 if (enteredValue instanceof String) {
756                                     String stringVariable = (String) enteredValue;
757                                     errors.addAll(performValidation(attributeValue, fieldDefName, stringVariable, fieldDefTitle, findXpathExpressionPrefix));
758                                 } else if (enteredValue instanceof Collection) {
759                                     Collection stringVariables = (Collection<String>)enteredValue;
760                                     for (Iterator iter = stringVariables.iterator(); iter.hasNext();) {
761                                         String stringVariable = (String) iter.next();
762                                         errors.addAll(performValidation(attributeValue, fieldDefName, stringVariable, "One value for " + fieldDefTitle, findXpathExpressionPrefix));
763                                     }
764 
765                                 } else {
766                                     String errorMessage = "Only String or String[] objects should come from entered parameters of an attribute.";
767                                     LOG.error(errorMessage);
768                                     throw new RuntimeException(errorMessage);
769                                 }
770             				}
771         				} else {
772 //        				    String findValidation = "//searchingConfig/field[@name='" + fieldAttributes.getNamedItem("name").getNodeValue() + "']/validation";
773 //        				    Node validation = (Node) xpath.evaluate(findValidation, getConfigXML(), XPathConstants.NODE);
774 //        				    if (validation != null) {
775 //        				        NamedNodeMap validationAttributes = validation.getAttributes();
776 //        				        Node required = validationAttributes.getNamedItem("required");
777 //        				        if (required != null && "true".equalsIgnoreCase(required.getNodeValue())) {
778 //        				            errors.add(new WorkflowAttributeValidationError(fieldAttributes.getNamedItem("name").getNodeValue(),fieldAttributes.getNamedItem("title").getNodeValue()+" is required."));
779 //        				        }
780 //                            }
781     				    }
782                     }
783     			}
784             }
785 		} catch (XPathExpressionException e) {
786 			LOG.error("error in validateUserSearchInputs ", e);
787 			throw new RuntimeException("Error trying to find xml content with xpath expression: " + findField, e);
788 		}
789 		return errors;
790 	}
791 
792     private String constructRangeFieldErrorPrefix(String fieldDefLabel, NamedNodeMap rangeBoundAttributes) {
793         String potentialLabel = getPotentialRangeBoundLabelFromAttributes(rangeBoundAttributes);
794         if ( (StringUtils.isNotBlank(potentialLabel)) && (StringUtils.isNotBlank(fieldDefLabel)) ) {
795             return fieldDefLabel + " " + potentialLabel + " Field";
796         } else if (StringUtils.isNotBlank(fieldDefLabel)) {
797             return fieldDefLabel + " Range Field";
798         } else if (StringUtils.isNotBlank(potentialLabel)) {
799             return "Range Field " + potentialLabel + " Field";
800         }
801         return null;
802     }
803 
804 	private List<WorkflowAttributeValidationError> performValidation(SearchableAttributeValue attributeValue, String fieldDefName, String enteredValue, String errorMessagePrefix, String findXpathExpressionPrefix) throws XPathExpressionException {
805 		List<WorkflowAttributeValidationError> errors = new ArrayList<WorkflowAttributeValidationError>();
806 		XPath xpath = XPathHelper.newXPath();
807 		if ( attributeValue.allowsWildcards()) {
808 			enteredValue = enteredValue.replaceAll(SEARCH_WILDCARD_CHARACTER_REGEX_ESCAPED, "");
809 		}
810 		if (!attributeValue.isPassesDefaultValidation(enteredValue)) {
811             errorMessagePrefix = (StringUtils.isNotBlank(errorMessagePrefix)) ? errorMessagePrefix : "Field";
812 			String errorMsg = errorMessagePrefix + " with value '" + enteredValue + "' does not conform to standard validation for field type.";
813 			LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
814 			errors.add(new WorkflowAttributeValidationError(fieldDefName, errorMsg));
815 		} else {
816 			String findValidation = findXpathExpressionPrefix + "/validation/regex";
817 			String regex = (String) xpath.evaluate(findValidation, getConfigXML(), XPathConstants.STRING);
818 			if (!Utilities.isEmpty(regex)) {
819 				Pattern pattern = Pattern.compile(regex);
820 				Matcher matcher = pattern.matcher(enteredValue);
821 				if (!matcher.matches()) {
822 					String findErrorMessage = findXpathExpressionPrefix + "/validation/message";
823 					String message = (String) xpath.evaluate(findErrorMessage, getConfigXML(), XPathConstants.STRING);
824 					errors.add(new WorkflowAttributeValidationError(fieldDefName, message));
825 				}
826 			}
827 		}
828 		return errors;
829 	}
830 
831 	public Element getConfigXML() {
832 		try {
833 			return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(ruleAttribute.getXmlConfigData())))).getDocumentElement();
834 		} catch (Exception e) {
835 			String ruleAttrStr = (ruleAttribute == null ? null : ruleAttribute.getName());
836 			LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
837 			throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
838 		}
839 	}
840 }