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 }