View Javadoc

1   /**
2    * Copyright 2005-2013 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 com.google.common.base.Function;
19  import org.apache.commons.collections.CollectionUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.kuali.rice.core.api.search.Range;
22  import org.kuali.rice.core.api.search.SearchExpressionUtils;
23  import org.kuali.rice.core.api.uif.DataType;
24  import org.kuali.rice.core.api.uif.RemotableAbstractControl;
25  import org.kuali.rice.core.api.uif.RemotableAttributeError;
26  import org.kuali.rice.core.api.uif.RemotableAttributeField;
27  import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
28  import org.kuali.rice.core.api.uif.RemotableDatepicker;
29  import org.kuali.rice.core.api.uif.RemotableHiddenInput;
30  import org.kuali.rice.core.api.uif.RemotableQuickFinder;
31  import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
32  import org.kuali.rice.core.api.uif.RemotableSelect;
33  import org.kuali.rice.core.api.uif.RemotableTextInput;
34  import org.kuali.rice.core.api.util.KeyValue;
35  import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
36  import org.kuali.rice.core.web.format.Formatter;
37  import org.kuali.rice.kew.api.KewApiConstants;
38  import org.kuali.rice.kew.api.WorkflowRuntimeException;
39  import org.kuali.rice.kew.api.document.DocumentWithContent;
40  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
41  import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
42  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
43  import org.kuali.rice.kew.api.extension.ExtensionDefinition;
44  import org.kuali.rice.kew.docsearch.CaseAwareSearchableAttributeValue;
45  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
46  import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
47  import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
48  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
49  import org.kuali.rice.kim.api.group.Group;
50  import org.kuali.rice.kim.api.group.GroupService;
51  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
52  import org.kuali.rice.kns.lookup.LookupUtils;
53  import org.kuali.rice.krad.UserSession;
54  import org.kuali.rice.krad.util.GlobalVariables;
55  import org.w3c.dom.Document;
56  import org.w3c.dom.Element;
57  import org.w3c.dom.NamedNodeMap;
58  import org.w3c.dom.Node;
59  import org.w3c.dom.NodeList;
60  import org.xml.sax.InputSource;
61  
62  import javax.management.modelmbean.XMLParseException;
63  import javax.xml.parsers.DocumentBuilderFactory;
64  import javax.xml.parsers.ParserConfigurationException;
65  import javax.xml.xpath.XPath;
66  import javax.xml.xpath.XPathConstants;
67  import javax.xml.xpath.XPathExpressionException;
68  import java.io.BufferedReader;
69  import java.io.StringReader;
70  import java.util.ArrayList;
71  import java.util.LinkedHashMap;
72  import java.util.Collection;
73  import java.util.Collections;
74  import java.util.HashMap;
75  import java.util.List;
76  import java.util.Map;
77  import java.util.regex.Matcher;
78  import java.util.regex.Pattern;
79  
80  
81  /**
82   * Implementation of a {@code SearchableAttribute} whose configuration is driven from XML.
83   *
84   * XML configuration must be supplied in the ExtensionDefinition configuration parameter {@link KewApiConstants#ATTRIBUTE_XML_CONFIG_DATA}.
85   * Parsing of XML search configuration and generation of XML search content proceeds in an analogous fashion to {@link org.kuali.rice.kew.rule.xmlrouting.StandardGenericXMLRuleAttribute}.
86   * Namely, if an <pre>searchingConfig/xmlSearchContent</pre> element is provided, its content is used as a template.  Otherwise a standard XML template is used.
87   * This template is parameterized with variables of the notation <pre>%name%</pre> which are resolved by <pre>searchingConfig/fieldDef[@name]</pre> definitions.
88   *
89   * The XML content is not validated, but it must be well formed.
90   *
91   * Example 1:
92   * <pre>
93   *     <searchingConfig>
94   *         <fieldDef name="def1" ...other attrs/>
95   *             ... other config
96   *         </fieldDef>
97   *         <fieldDef name="def2" ...other attrs/>
98   *             ... other config
99   *         </fieldDef>
100  *     </searchingConfig>
101  * </pre>
102  * Produces, when supplied with the workflow definition parameters: { def1: val1, def2: val2 }:
103  * <pre>
104  *     <xmlRouting>
105  *         <field name="def1"><value>val1</value></field>
106  *         <field name="def2"><value>val2</value></field>
107  *     </xmlRouting>
108  * </pre>
109  *
110  * Example 2:
111  * <pre>
112  *     <searchingConfig>
113  *         <xmlSearchContent>
114  *             <myGeneratedContent>
115  *                 <version>whatever</version>
116  *                 <anythingIWant>Once upon a %def1%...</anythingIWant>
117  *                 <conclusion>Happily ever %def2%.</conclusion>
118  *             </myGeneratedContent>
119  *         </xmlSearchContent>
120  *         <fieldDef name="def1" ...other attrs/>
121  *             ... other config
122  *         </fieldDef>
123  *         <fieldDef name="def2" ...other attrs/>
124  *             ... other config
125  *         </fieldDef>
126  *     </searchingConfig>
127  * </pre>
128  * Produces, when supplied with the workflow definition parameters: { def1: val1, def2: val2 }:
129  * <pre>
130  *     <myGeneratedContent>
131  *         <version>whatever</version>
132  *         <anythingIWant>Once upon a val1...</anythingIWant>
133  *         <conclusion>Happily ever val2.</conclusion>
134  *     </myGeneratedContent>
135  * </pre>
136  * @author Kuali Rice Team (rice.collab@kuali.org)
137  */
138 public class StandardGenericXMLSearchableAttribute implements SearchableAttribute {
139 
140 	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardGenericXMLSearchableAttribute.class);
141     private static final String FIELD_DEF_E = "fieldDef";
142     /**
143      * Compile-time option that controls whether we check and return errors for field bounds options that conflict with searchable attribute configuration.
144      */
145     private static final boolean PEDANTIC_BOUNDS_VALIDATION = true;
146 
147 
148     @Override
149     public String generateSearchContent(ExtensionDefinition extensionDefinition, String documentTypeName, WorkflowAttributeDefinition attributeDefinition) {
150         Map<String, String> propertyDefinitionMap = attributeDefinition.getPropertyDefinitionsAsMap();
151         try {
152             XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
153             return content.generateSearchContent(propertyDefinitionMap);
154         } catch (XPathExpressionException e) {
155             LOG.error("error in getSearchContent ", e);
156             throw new RuntimeException("Error trying to find xml content with xpath expression", e);
157         } catch (Exception e) {
158             LOG.error("error in getSearchContent attempting to find xml search content", e);
159             throw new RuntimeException("Error trying to get xml search content.", e);
160         }
161     }
162 
163     @Override
164     public List<DocumentAttribute> extractDocumentAttributes(ExtensionDefinition extensionDefinition, DocumentWithContent documentWithContent) {
165         List<DocumentAttribute> searchStorageValues = new ArrayList<DocumentAttribute>();
166         String fullDocumentContent = documentWithContent.getDocumentContent().getFullContent();
167         if (StringUtils.isBlank(documentWithContent.getDocumentContent().getFullContent())) {
168             LOG.warn("Empty Document Content found for document id: " + documentWithContent.getDocument().getDocumentId());
169             return searchStorageValues;
170         }
171         Document document;
172         try {
173             document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(fullDocumentContent))));
174         } catch (Exception e){
175             LOG.error("error parsing docContent: "+documentWithContent.getDocumentContent(), e);
176             throw new RuntimeException("Error trying to parse docContent: "+documentWithContent.getDocumentContent(), e);
177         }
178         XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
179         List<XMLSearchableAttributeContent.FieldDef> fields;
180         try {
181             fields = content.getFieldDefList();
182         } catch (XPathExpressionException xpee) {
183             throw new RuntimeException("Error parsing searchable attribute content", xpee);
184         } catch (ParserConfigurationException pce) {
185             throw new RuntimeException("Error parsing searchable attribute content", pce);
186         }
187         XPath xpath = XPathHelper.newXPath(document);
188         for (XMLSearchableAttributeContent.FieldDef field: fields) {
189             if (StringUtils.isNotEmpty(field.fieldEvaluationExpr)) {
190                 List<String> values = new ArrayList<String>();
191                 try {
192                     LOG.debug("Trying to retrieve node set with expression: '" + field.fieldEvaluationExpr + "'.");
193                     NodeList searchValues = (NodeList) xpath.evaluate(field.fieldEvaluationExpr, document.getDocumentElement(), XPathConstants.NODESET);
194                     // being that this is the standard xml attribute we will return the key with an empty value
195                     // so we can find it from a doc search using this key
196                     for (int j = 0; j < searchValues.getLength(); j++) {
197                         Node searchValue = searchValues.item(j);
198                         if (searchValue.getFirstChild() != null && (StringUtils.isNotEmpty(searchValue.getFirstChild().getNodeValue()))) {
199                             values.add(searchValue.getFirstChild().getNodeValue());
200                         }
201                     }
202                 } catch (XPathExpressionException e) {
203                     LOG.debug("Could not retrieve node set with expression: '" + field.fieldEvaluationExpr + "'. Trying string return type.");
204                     //try for a string being returned from the expression.  This
205                     //seems like a poor way to determine our expression return type but
206                     //it's all I can come up with at the moment.
207                     try {
208                         String searchValue = (String) xpath.evaluate(field.fieldEvaluationExpr, document.getDocumentElement(), XPathConstants.STRING);
209                         if (StringUtils.isNotBlank(searchValue)) {
210                             values.add(searchValue);
211                         }
212                     } catch (XPathExpressionException xpee) {
213                         LOG.error("Error retrieving string with expression: '" + field.fieldEvaluationExpr + "'", xpee);
214                         throw new RuntimeException("Error retrieving string with expression: '" + field.fieldEvaluationExpr + "'", xpee);
215                     }
216                 }
217 
218                 // remove any nulls
219                 values.removeAll(Collections.singleton(null));
220                 // being that this is the standard xml attribute we will return the key with an empty value
221                 // so we can find it from a doc search using this key
222                 if (values.isEmpty()) {
223                     values.add(null);
224                 }
225                 for (String value: values) {
226                     DocumentAttribute searchableValue = this.setupSearchableAttributeValue(field.searchDefinition.dataType, field.name, value);
227                     if (searchableValue != null) {
228                         searchStorageValues.add(searchableValue);
229                     }
230                 }
231             }
232         }
233         return searchStorageValues;
234     }
235 
236     private DocumentAttribute setupSearchableAttributeValue(String dataType, String key, String value) {
237         SearchableAttributeValue attValue = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(dataType);
238         if (attValue == null) {
239             String errorMsg = "Cannot find a SearchableAttributeValue associated with the data type '" + dataType + "'";
240             LOG.error("setupSearchableAttributeValue() " + errorMsg);
241             throw new RuntimeException(errorMsg);
242         }
243         value = (value != null) ? value.trim() : null;
244         if ( (StringUtils.isNotBlank(value)) && (!attValue.isPassesDefaultValidation(value)) ) {
245             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";
246             LOG.error("setupSearchableAttributeValue() " + errorMsg);
247             throw new RuntimeException(errorMsg);
248         }
249         attValue.setSearchableAttributeKey(key);
250         attValue.setupAttributeValue(value);
251         return attValue.toDocumentAttribute();
252     }
253 
254     @Override
255     public List<RemotableAttributeField> getSearchFields(ExtensionDefinition extensionDefinition, String documentTypeName) {
256         List<RemotableAttributeField> searchFields = new ArrayList<RemotableAttributeField>();
257         List<SearchableAttributeValue> searchableAttributeValues = DocumentSearchInternalUtils.getSearchableAttributeValueObjectTypes();
258 
259         XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
260         List<XMLSearchableAttributeContent.FieldDef> fields;
261         try {
262             fields = content.getFieldDefList();
263         } catch (XPathExpressionException xpee) {
264             throw new RuntimeException("Error parsing searchable attribute configuration", xpee);
265         } catch (ParserConfigurationException pce) {
266             throw new RuntimeException("Error parsing searchable attribute configuration", pce);
267         }
268         for (XMLSearchableAttributeContent.FieldDef field: fields) {
269             searchFields.add(convertFieldDef(field, searchableAttributeValues));
270         }
271 
272         return searchFields;
273     }
274 
275     /**
276      * Converts a searchable attribute FieldDef to a RemotableAttributeField
277      */
278     private RemotableAttributeField convertFieldDef(XMLSearchableAttributeContent.FieldDef field, Collection<SearchableAttributeValue> searchableAttributeValues) {
279         RemotableAttributeField.Builder fieldBuilder = RemotableAttributeField.Builder.create(field.name);
280 
281         fieldBuilder.setLongLabel(field.title);
282 
283         RemotableAttributeLookupSettings.Builder attributeLookupSettings = RemotableAttributeLookupSettings.Builder.create();
284         fieldBuilder.setAttributeLookupSettings(attributeLookupSettings);
285 
286         // value
287         if (field.defaultValue != null) {
288             fieldBuilder.setDefaultValues(Collections.singletonList(field.defaultValue));
289         }
290 
291         // Visibility
292         applyVisibility(fieldBuilder, attributeLookupSettings, field);
293 
294         // Display
295         RemotableAbstractControl.Builder controlBuilder = constructControl(field.display.type, field.display.options);
296         fieldBuilder.setControl(controlBuilder);
297         if ("date".equals(field.display.type)) {
298             fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
299             fieldBuilder.setDataType(DataType.DATE);
300         }
301         if (!field.display.selectedOptions.isEmpty()) {
302             fieldBuilder.setDefaultValues(field.display.selectedOptions);
303         }
304 
305         // resultcolumn
306         attributeLookupSettings.setInResults(field.isDisplayedInSearchResults());
307 
308         // SearchDefinition
309         // data type operations
310         DataType dataType = DocumentSearchInternalUtils.convertValueToDataType(field.searchDefinition.dataType);
311         fieldBuilder.setDataType(dataType);
312         if (DataType.DATE == fieldBuilder.getDataType()) {
313             fieldBuilder.getWidgets().add(RemotableDatepicker.Builder.create());
314         }
315 
316         boolean isRangeSearchField = isRangeSearchField(searchableAttributeValues, fieldBuilder.getDataType(), field);
317         if (isRangeSearchField) {
318             attributeLookupSettings.setRanged(true);
319             // we've established the search is ranged, so we can inspect the bounds
320             attributeLookupSettings.setLowerBoundInclusive(field.searchDefinition.lowerBound.inclusive);
321             attributeLookupSettings.setUpperBoundInclusive(field.searchDefinition.upperBound.inclusive);
322             attributeLookupSettings.setLowerLabel(field.searchDefinition.lowerBound.label);
323             attributeLookupSettings.setUpperLabel(field.searchDefinition.upperBound.label);
324             attributeLookupSettings.setLowerDatePicker(field.searchDefinition.lowerBound.datePicker);
325             attributeLookupSettings.setUpperDatePicker(field.searchDefinition.upperBound.datePicker);
326         }
327 
328         Boolean caseSensitive = field.searchDefinition.getRangeBoundOptions().caseSensitive;
329         if (caseSensitive != null) {
330             attributeLookupSettings.setCaseSensitive(caseSensitive);
331         }
332 
333         /**
334 
335 
336 
337          String formatterClass = (searchDefAttributes.getNamedItem("formatterClass") == null) ? null : searchDefAttributes.getNamedItem("formatterClass").getNodeValue();
338          if (!StringUtils.isEmpty(formatterClass)) {
339          try {
340          myField.setFormatter((Formatter)Class.forName(formatterClass).newInstance());
341          } catch (InstantiationException e) {
342          LOG.error("Unable to get new instance of formatter class: " + formatterClass);
343          throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
344          }
345          catch (IllegalAccessException e) {
346          LOG.error("Unable to get new instance of formatter class: " + formatterClass);
347          throw new RuntimeException("Unable to get new instance of formatter class: " + formatterClass);
348          } catch (ClassNotFoundException e) {
349          LOG.error("Unable to find formatter class: " + formatterClass);
350          throw new RuntimeException("Unable to find formatter class: " + formatterClass);
351          }
352          }
353 
354          */
355 
356          String formatter = field.display.formatter == null ? null : field.display.formatter;
357          fieldBuilder.setFormatterName(formatter);
358 
359         try {
360         // Register this formatter so that you can use it later in FieldUtils when processing
361             if(StringUtils.isNotEmpty(formatter)){
362                 Formatter.registerFormatter(Class.forName(formatter), Class.forName(formatter));
363             }
364         } catch (ClassNotFoundException e) {
365          LOG.error("Unable to find formatter class: " + formatter);
366          throw new RuntimeException("Unable to find formatter class: " + formatter);
367          }
368 
369 
370         // Lookup
371         // XMLAttributeUtils.establishFieldLookup(fieldBuilder, childNode); // this code can probably die now that parsing has moved out to xmlsearchableattribcontent
372         if (field.lookup.dataObjectClass != null) {
373             RemotableQuickFinder.Builder quickFinderBuilder = RemotableQuickFinder.Builder.create(LookupUtils.getBaseLookupUrl(false), field.lookup.dataObjectClass);
374             quickFinderBuilder.setFieldConversions(field.lookup.fieldConversions);
375             fieldBuilder.getWidgets().add(quickFinderBuilder);
376         }
377 
378         return fieldBuilder.build();
379     }
380 
381 
382     /**
383      * Determines whether the searchable field definition is a ranged search
384      * @param searchableAttributeValues the possible system {@link SearchableAttributeValue}s
385      * @param dataType the UI data type
386      * @return
387      */
388     private boolean isRangeSearchField(Collection<SearchableAttributeValue> searchableAttributeValues, DataType dataType, XMLSearchableAttributeContent.FieldDef field) {
389         for (SearchableAttributeValue attValue : searchableAttributeValues)
390         {
391             DataType attributeValueDataType = DocumentSearchInternalUtils.convertValueToDataType(attValue.getAttributeDataType());
392             if (attributeValueDataType == dataType) {
393                 return isRangeSearchField(attValue, field);
394             }
395         }
396         String errorMsg = "Could not find searchable attribute value for data type '" + dataType + "'";
397         LOG.error("isRangeSearchField(List, String, NamedNodeMap, Node) " + errorMsg);
398         throw new WorkflowRuntimeException(errorMsg);
399     }
400 
401     private boolean isRangeSearchField(SearchableAttributeValue searchableAttributeValue, XMLSearchableAttributeContent.FieldDef field) {
402         // this is a ranged search if
403         // 1) attribute value type allows ranged search
404         boolean allowRangedSearch = searchableAttributeValue.allowsRangeSearches();
405         // AND
406         // 2) the searchDefinition specifies a ranged search
407         return allowRangedSearch && field.searchDefinition.isRangedSearch();
408     }
409 
410     /**
411      * Applies visibility settings to the RemotableAttributeField
412      */
413     private void applyVisibility(RemotableAttributeField.Builder fieldBuilder, RemotableAttributeLookupSettings.Builder attributeLookupSettings, XMLSearchableAttributeContent.FieldDef field) {
414         boolean visible = true;
415         // if visibility is explicitly set, use it
416         if (field.visibility.visible != null) {
417             visible = field.visibility.visible;
418         } else {
419             if (field.visibility.groupName != null) {
420                 UserSession session = GlobalVariables.getUserSession();
421                 if (session == null) {
422                     throw new WorkflowRuntimeException("UserSession is null!  Attempted to render the searchable attribute outside of an established session.");
423                 }
424                 GroupService groupService = KimApiServiceLocator.getGroupService();
425 
426                 Group group = groupService.getGroupByNamespaceCodeAndName(field.visibility.groupNamespace, field.visibility.groupName);
427                 visible =  group == null ? false : groupService.isMemberOfGroup(session.getPerson().getPrincipalId(), group.getId());
428             }
429         }
430         String type = field.visibility.type;
431         if ("field".equals(type) || "fieldAndColumn".equals(type)) {
432             // if it's not visible, coerce this field to a hidden type
433             if (!visible) {
434                 fieldBuilder.setControl(RemotableHiddenInput.Builder.create());
435             }
436         }
437         if ("column".equals(type) || "fieldAndColumn".equals(type)) {
438             attributeLookupSettings.setInResults(visible);
439         }
440     }
441 
442     private RemotableAbstractControl.Builder constructControl(String type, Collection<KeyValue> options) {
443         RemotableAbstractControl.Builder control = null;
444         Map<String, String> optionMap = new LinkedHashMap<String, String>();
445         for (KeyValue option : options) {
446             optionMap.put(option.getKey(), option.getValue());
447         }
448         if ("text".equals(type) || "date".equals(type)) {
449             control = RemotableTextInput.Builder.create();
450         } else if ("select".equals(type)) {
451             control = RemotableSelect.Builder.create(optionMap);
452         } else if ("radio".equals(type)) {
453             control = RemotableRadioButtonGroup.Builder.create(optionMap);
454         } else if ("hidden".equals(type)) {
455             control = RemotableHiddenInput.Builder.create();
456         } else if ("multibox".equals(type)) {
457             RemotableSelect.Builder builder = RemotableSelect.Builder.create(optionMap);
458             builder.setMultiple(true);
459             control = builder;
460         } else {
461             throw new IllegalArgumentException("Illegal field type found: " + type);
462         }
463         return control;
464     }
465 
466     @Override
467     public List<RemotableAttributeError> validateDocumentAttributeCriteria(ExtensionDefinition extensionDefinition, DocumentSearchCriteria documentSearchCriteria) {
468 		List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
469         
470         Map<String, List<String>> documentAttributeValues = documentSearchCriteria.getDocumentAttributeValues();
471         if (documentAttributeValues == null || documentAttributeValues.isEmpty()) {
472             // nothing to validate...
473             return errors;
474         }
475 
476         XMLSearchableAttributeContent content = new XMLSearchableAttributeContent(getConfigXML(extensionDefinition));
477         List<XMLSearchableAttributeContent.FieldDef> fields;
478         try {
479             fields = content.getFieldDefList();
480         } catch (XPathExpressionException xpee) {
481             throw new RuntimeException("Error parsing searchable attribute configuration", xpee);
482         } catch (ParserConfigurationException pce) {
483             throw new RuntimeException("Error parsing searchable attribute configuration", pce);
484         }
485         if (fields.isEmpty()) {
486             LOG.warn("Could not find any field definitions (<" + FIELD_DEF_E + ">) or possibly a searching configuration (<searchingConfig>) for this XMLSearchAttribute");
487             return errors;
488         }
489 
490         for (XMLSearchableAttributeContent.FieldDef field: fields) {
491             String fieldDefName = field.name;
492             String fieldDefTitle = field.title == null ? "" : field.title;
493 
494             List<String> testObject = documentAttributeValues.get(fieldDefName);
495 
496             if (testObject == null || testObject.isEmpty()) {
497                 // no value to validate
498                 // not checking for 'required' here since this is *search* criteria, and required field can be omitted
499                 continue;
500             }
501 
502             // What type of value is this searchable attribute field?
503             // get the searchable attribute value by using the data type
504             SearchableAttributeValue attributeValue = DocumentSearchInternalUtils.getSearchableAttributeValueByDataTypeString(field.searchDefinition.dataType);
505             if (attributeValue == null) {
506                 String errorMsg = "Cannot find SearchableAttributeValue for field data type '" + field.searchDefinition.dataType + "'";
507                 LOG.error("validateUserSearchInputs() " + errorMsg);
508                 throw new RuntimeException(errorMsg);
509             }
510 
511             // 1) parse concrete values from possible range expressions
512             // 2) validate any resulting concrete values whether they were original arguments or parsed from range expressions
513             // 3) if the expression was a range expression, validate the logical validity of the range bounds
514 
515             List<String> terminalValues = new ArrayList<String>();
516             List<Range> rangeValues = new ArrayList<Range>();
517 
518             // we are assuming here that the only expressions evaluated against searchable attributes are simple
519             // non-compound expressions.  parsing compound expressions would require full grammar/parsing support
520             // and would probably be pretty absurd assuming these queries are coming from UIs.
521             // If they are not coming from the UI, do we need to support compound expressions?
522             for (String value: testObject) {
523                 // is this a terminal value or does it look like a range?
524                 if (value == null) {
525                     // assuming null values are not an error condition
526                     continue;
527                 }
528                 // this is just a war of attrition, need real parsing
529                 String[] clauses = SearchExpressionUtils.splitOnClauses(value);
530                 for (String clause: clauses) {
531                     // if it's not empty. see if it's a range
532                     Range r = null;
533                     if (StringUtils.isNotEmpty(value)) {
534                         r = SearchExpressionUtils.parseRange(value);
535                     }
536                     if (r != null) {
537                         // hey, it looks like a range
538                         boolean errs = false;
539                         if (!field.searchDefinition.isRangedSearch()) {
540                             errs = true;
541                             errors.add(RemotableAttributeError.Builder.create(field.name, "field does not support ranged searches but range search expression detected").build());
542                         } else {
543                             // only check bounds if range search is specified
544                             // XXX: FIXME: disabling these pedantic checks as they are causing annoying test breakages
545                             if (PEDANTIC_BOUNDS_VALIDATION) {
546                                 // this is not actually an error. just disregard case-sensitivity for data types that don't support it
547                                 /*if (!attributeValue.allowsCaseInsensitivity() && Boolean.FALSE.equals(field.searchDefinition.getRangeBoundOptions().caseSensitive)) {
548                                     errs = true;
549                                     errors.add(RemotableAttributeError.Builder.create(field.name, "attribute data type does not support case insensitivity but case-insensitivity specified in attribute definition").build());
550                                 }*/
551                                 if (r.getLowerBoundValue() != null && r.isLowerBoundInclusive() != field.searchDefinition.lowerBound.inclusive) {
552                                     errs = true;
553                                     errors.add(RemotableAttributeError.Builder.create(field.name, "range expression ('" + value + "') and attribute definition differ on lower bound inclusivity.  Range is: " + r.isLowerBoundInclusive() + " Attrib is: " + field.searchDefinition.lowerBound.inclusive).build());
554                                 }
555                                 if (r.getUpperBoundValue() != null && r.isUpperBoundInclusive() != field.searchDefinition.upperBound.inclusive) {
556                                     errs = true;
557                                     errors.add(RemotableAttributeError.Builder.create(field.name, "range expression ('" + value + "') and attribute definition differ on upper bound inclusivity.  Range is: " + r.isUpperBoundInclusive() + " Attrib is: " + field.searchDefinition.upperBound.inclusive).build());
558                                 }
559                             }
560                         }
561 
562                         if (!errs) {
563                             rangeValues.add(r);
564                         }
565                     } else {
566                         terminalValues.add(value);
567                     }
568                 }
569             }
570 
571             List<String> parsedValues = new ArrayList<String>();
572             // validate all values
573             for (String value: terminalValues) {
574                 errors.addAll(performValidation(attributeValue, field, value, fieldDefTitle, parsedValues));
575             }
576             for (Range range: rangeValues) {
577                 List<String> parsedLowerValues = new ArrayList<String>();
578                 List<String> parsedUpperValues = new ArrayList<String>();
579                 List<RemotableAttributeError> lowerErrors = performValidation(attributeValue, field,
580                         range.getLowerBoundValue(), constructRangeFieldErrorPrefix(field.title,
581                         field.searchDefinition.lowerBound), parsedLowerValues);
582                 errors.addAll(lowerErrors);
583                 List<RemotableAttributeError> upperErrors = performValidation(attributeValue, field, range.getUpperBoundValue(),
584                         constructRangeFieldErrorPrefix(field.title, field.searchDefinition.upperBound), parsedUpperValues);
585                 errors.addAll(upperErrors);
586 
587                 // if both values check out, perform logical range validation
588                 if (lowerErrors.isEmpty() && upperErrors.isEmpty()) {
589                     // TODO: how to handle multiple values?? doesn't really make sense
590                     String lowerBoundValue = parsedLowerValues.isEmpty() ? null : parsedLowerValues.get(0);
591                     String upperBoundValue = parsedUpperValues.isEmpty() ? null : parsedUpperValues.get(0);
592 
593                     final Boolean rangeValid;
594                     // for the sake of string searches, make sure the bounds are uppercased before comparison if the search
595                     // is case sensitive.
596                     if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING.equals(field.searchDefinition.dataType)) {
597                         boolean caseSensitive = field.searchDefinition.getRangeBoundOptions().caseSensitive == null ? true : field.searchDefinition.getRangeBoundOptions().caseSensitive;
598                         rangeValid = ((CaseAwareSearchableAttributeValue) attributeValue).isRangeValid(lowerBoundValue, upperBoundValue, caseSensitive);
599                     } else {
600                         rangeValid = attributeValue.isRangeValid(lowerBoundValue, upperBoundValue);
601                     }
602 
603                     if (rangeValid != null && !rangeValid) {
604                         String errorMsg = "The " + fieldDefTitle + " range is incorrect.  The " +
605                                 (StringUtils.isNotBlank(field.searchDefinition.lowerBound.label) ? field.searchDefinition.lowerBound.label : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_LOWER_BOUND_LABEL)
606                                 + " value entered must come before the " +
607                                 (StringUtils.isNotBlank(field.searchDefinition.upperBound.label) ? field.searchDefinition.upperBound.label : KewApiConstants.SearchableAttributeConstants.DEFAULT_RANGE_SEARCH_UPPER_BOUND_LABEL)
608                                 + " value";
609                         LOG.debug("validateUserSearchInputs() " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'");
610                         errors.add(RemotableAttributeError.Builder.create(fieldDefName, errorMsg).build());
611                     }
612                 }
613             }
614        }
615         return errors;
616     }
617 
618     private String constructRangeFieldErrorPrefix(String fieldDefLabel, XMLSearchableAttributeContent.FieldDef.SearchDefinition.RangeBound rangeBound) {
619         if ( StringUtils.isNotBlank(rangeBound.label) && StringUtils.isNotBlank(fieldDefLabel)) {
620             return fieldDefLabel + " " + rangeBound.label + " Field";
621         } else if (StringUtils.isNotBlank(fieldDefLabel)) {
622             return fieldDefLabel + " Range Field";
623         } else if (StringUtils.isNotBlank(rangeBound.label)) {
624             return "Range Field " + rangeBound.label + " Field";
625         }
626         return null;
627     }
628 
629     /**
630      * Performs validation on a single DSC attribute value, running any defined custom validation regex after basic validation
631      * @param attributeValue the searchable attribute value type
632      * @param field the XMLSearchableAttributeContent field
633      * @param enteredValue the value to validate
634      * @param errorMessagePrefix a prefix for error messages
635      * @param resultingValues optional list of accumulated parsed values
636      * @return a (possibly empty) list of errors
637      */
638     private List<RemotableAttributeError> performValidation(SearchableAttributeValue attributeValue, final XMLSearchableAttributeContent.FieldDef field, String enteredValue, String errorMessagePrefix, List<String> resultingValues) {
639         return DocumentSearchInternalUtils.validateSearchFieldValue(field.name, attributeValue, enteredValue, errorMessagePrefix, resultingValues, new Function<String, Collection<RemotableAttributeError>>() {
640             @Override
641             public Collection<RemotableAttributeError> apply(String value) {
642                 if (StringUtils.isNotEmpty(field.validation.regex)) {
643                     Pattern pattern = Pattern.compile(field.validation.regex);
644                     Matcher matcher = pattern.matcher(value);
645                     if (!matcher.matches()) {
646                         return Collections.singletonList(RemotableAttributeError.Builder.create(field.name, field.validation.message).build());
647                     }
648                 }
649                 return Collections.emptyList();
650             }
651         });
652     }
653 
654     // preserved only for subclasses
655     protected Element getConfigXML(ExtensionDefinition extensionDefinition) {
656         try {
657             String xmlConfigData = extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA);
658             return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new BufferedReader(new StringReader(xmlConfigData)))).getDocumentElement();
659         } catch (Exception e) {
660             String ruleAttrStr = (extensionDefinition == null ? null : extensionDefinition.getName());
661             LOG.error("error parsing xml data from search attribute: " + ruleAttrStr, e);
662             throw new RuntimeException("error parsing xml data from searchable attribute: " + ruleAttrStr, e);
663         }
664     }
665 }