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