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 }