001 /**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kew.docsearch.xml;
017
018 import com.google.common.base.Function;
019 import org.apache.commons.collections.CollectionUtils;
020 import org.apache.commons.lang.StringUtils;
021 import org.kuali.rice.core.api.search.Range;
022 import org.kuali.rice.core.api.search.SearchExpressionUtils;
023 import org.kuali.rice.core.api.uif.DataType;
024 import org.kuali.rice.core.api.uif.RemotableAbstractControl;
025 import org.kuali.rice.core.api.uif.RemotableAttributeError;
026 import org.kuali.rice.core.api.uif.RemotableAttributeField;
027 import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
028 import org.kuali.rice.core.api.uif.RemotableDatepicker;
029 import org.kuali.rice.core.api.uif.RemotableHiddenInput;
030 import org.kuali.rice.core.api.uif.RemotableQuickFinder;
031 import org.kuali.rice.core.api.uif.RemotableRadioButtonGroup;
032 import org.kuali.rice.core.api.uif.RemotableSelect;
033 import org.kuali.rice.core.api.uif.RemotableTextInput;
034 import org.kuali.rice.core.api.util.KeyValue;
035 import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
036 import org.kuali.rice.core.web.format.Formatter;
037 import org.kuali.rice.kew.api.KewApiConstants;
038 import org.kuali.rice.kew.api.WorkflowRuntimeException;
039 import org.kuali.rice.kew.api.document.DocumentWithContent;
040 import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
041 import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
042 import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
043 import org.kuali.rice.kew.api.extension.ExtensionDefinition;
044 import org.kuali.rice.kew.docsearch.CaseAwareSearchableAttributeValue;
045 import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
046 import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
047 import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
048 import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
049 import org.kuali.rice.kim.api.group.Group;
050 import org.kuali.rice.kim.api.group.GroupService;
051 import org.kuali.rice.kim.api.services.KimApiServiceLocator;
052 import org.kuali.rice.kns.lookup.LookupUtils;
053 import org.kuali.rice.krad.UserSession;
054 import org.kuali.rice.krad.util.GlobalVariables;
055 import org.w3c.dom.Document;
056 import org.w3c.dom.Element;
057 import org.w3c.dom.NamedNodeMap;
058 import org.w3c.dom.Node;
059 import org.w3c.dom.NodeList;
060 import org.xml.sax.InputSource;
061
062 import javax.management.modelmbean.XMLParseException;
063 import javax.xml.parsers.DocumentBuilderFactory;
064 import javax.xml.parsers.ParserConfigurationException;
065 import javax.xml.xpath.XPath;
066 import javax.xml.xpath.XPathConstants;
067 import javax.xml.xpath.XPathExpressionException;
068 import java.io.BufferedReader;
069 import java.io.StringReader;
070 import java.util.ArrayList;
071 import java.util.LinkedHashMap;
072 import java.util.Collection;
073 import java.util.Collections;
074 import java.util.HashMap;
075 import java.util.List;
076 import java.util.Map;
077 import java.util.regex.Matcher;
078 import java.util.regex.Pattern;
079
080
081 /**
082 * Implementation of a {@code SearchableAttribute} whose configuration is driven from XML.
083 *
084 * XML configuration must be supplied in the ExtensionDefinition configuration parameter {@link KewApiConstants#ATTRIBUTE_XML_CONFIG_DATA}.
085 * 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}.
086 * Namely, if an <pre>searchingConfig/xmlSearchContent</pre> element is provided, its content is used as a template. Otherwise a standard XML template is used.
087 * This template is parameterized with variables of the notation <pre>%name%</pre> which are resolved by <pre>searchingConfig/fieldDef[@name]</pre> definitions.
088 *
089 * The XML content is not validated, but it must be well formed.
090 *
091 * Example 1:
092 * <pre>
093 * <searchingConfig>
094 * <fieldDef name="def1" ...other attrs/>
095 * ... other config
096 * </fieldDef>
097 * <fieldDef name="def2" ...other attrs/>
098 * ... other config
099 * </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 }