001/** 002 * Copyright 2005-2017 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 */ 016package org.kuali.rice.kew.docsearch; 017 018import java.io.IOException; 019import java.sql.Date; 020import java.sql.Timestamp; 021import java.text.ParseException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.EnumSet; 026import java.util.List; 027 028import com.fasterxml.jackson.annotation.JsonInclude; 029import com.fasterxml.jackson.databind.ObjectMapper; 030import com.fasterxml.jackson.datatype.joda.JodaModule; 031import org.apache.commons.collections.CollectionUtils; 032import org.apache.commons.lang.StringUtils; 033import org.apache.log4j.Logger; 034import org.joda.time.DateTime; 035import org.joda.time.MutableDateTime; 036import org.kuali.rice.core.api.CoreApiServiceLocator; 037import org.kuali.rice.core.api.data.DataType; 038import org.kuali.rice.core.api.reflect.ObjectDefinition; 039import org.kuali.rice.core.api.search.Range; 040import org.kuali.rice.core.api.search.SearchExpressionUtils; 041import org.kuali.rice.core.api.uif.AttributeLookupSettings; 042import org.kuali.rice.core.api.uif.RemotableAttributeError; 043import org.kuali.rice.core.api.uif.RemotableAttributeField; 044import org.kuali.rice.core.api.util.ClassLoaderUtils; 045import org.kuali.rice.core.api.util.RiceConstants; 046import org.kuali.rice.core.api.util.RiceKeyConstants; 047import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils; 048import org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver; 049import org.kuali.rice.kew.api.KewApiConstants; 050import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria; 051import org.kuali.rice.krad.util.GlobalVariables; 052 053import com.google.common.base.Function; 054 055/** 056 * Defines various utilities for internal use in the reference implementation of the document search functionality. 057 * 058 * @author Kuali Rice Team (rice.collab@kuali.org) 059 */ 060public class DocumentSearchInternalUtils { 061 062 private static final Logger LOG = Logger.getLogger(DocumentSearchInternalUtils.class); 063 064 private static final boolean CASE_SENSITIVE_DEFAULT = false; 065 066 private static final String STRING_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_T"; 067 private static final String DATE_TIME_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_DT_T"; 068 private static final String DECIMAL_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_FLT_T"; 069 private static final String INTEGER_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_LONG_T"; 070 071 private static final List<SearchableAttributeConfiguration> CONFIGURATIONS = 072 new ArrayList<SearchableAttributeConfiguration>(); 073 public static final List<Class<? extends SearchableAttributeValue>> SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST = 074 new ArrayList<Class<? extends SearchableAttributeValue>>(); 075 076 static { 077 SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeStringValue.class); 078 SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeFloatValue.class); 079 SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeLongValue.class); 080 SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeDateTimeValue.class); 081 } 082 083 static { 084 085 CONFIGURATIONS.add(new SearchableAttributeConfiguration( 086 STRING_ATTRIBUTE_TABLE_NAME, 087 EnumSet.of(DataType.BOOLEAN, DataType.STRING, DataType.MARKUP), 088 String.class)); 089 090 CONFIGURATIONS.add(new SearchableAttributeConfiguration( 091 DATE_TIME_ATTRIBUTE_TABLE_NAME, 092 EnumSet.of(DataType.DATE, DataType.TRUNCATED_DATE, DataType.DATETIME), 093 Timestamp.class)); 094 095 CONFIGURATIONS.add(new SearchableAttributeConfiguration( 096 DECIMAL_ATTRIBUTE_TABLE_NAME, 097 EnumSet.of(DataType.FLOAT, DataType.DOUBLE, DataType.CURRENCY), 098 Float.TYPE)); 099 100 CONFIGURATIONS.add(new SearchableAttributeConfiguration( 101 INTEGER_ATTRIBUTE_TABLE_NAME, 102 EnumSet.of(DataType.INTEGER, DataType.LONG), 103 Long.TYPE)); 104 105 } 106 107 // initialize-on-demand holder class idiom - see Effective Java item #71 108 /** 109 * KULRICE-6704 - cached ObjectMapper for improved performance 110 * 111 */ 112 private static ObjectMapper getObjectMapper() { return ObjectMapperHolder.objectMapper; } 113 114 private static class ObjectMapperHolder { 115 static final ObjectMapper objectMapper = initializeObjectMapper(); 116 117 private static ObjectMapper initializeObjectMapper() { 118 ObjectMapper jsonMapper = new ObjectMapper(); 119 jsonMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 120 jsonMapper.registerModule(new JodaModule()); 121 122 return jsonMapper; 123 } 124 } 125 126 public static boolean isLookupCaseSensitive(RemotableAttributeField remotableAttributeField) { 127 if (remotableAttributeField == null) { 128 throw new IllegalArgumentException("remotableAttributeField was null"); 129 } 130 AttributeLookupSettings lookupSettings = remotableAttributeField.getAttributeLookupSettings(); 131 if (lookupSettings != null) { 132 if (lookupSettings.isCaseSensitive() != null) { 133 return lookupSettings.isCaseSensitive().booleanValue(); 134 } 135 } 136 return CASE_SENSITIVE_DEFAULT; 137 } 138 139 public static String getAttributeTableName(RemotableAttributeField attributeField) { 140 return getConfigurationForField(attributeField).getTableName(); 141 } 142 143 public static Class<?> getDataTypeClass(RemotableAttributeField attributeField) { 144 return getConfigurationForField(attributeField).getDataTypeClass(); 145 } 146 147 private static SearchableAttributeConfiguration getConfigurationForField(RemotableAttributeField attributeField) { 148 for (SearchableAttributeConfiguration configuration : CONFIGURATIONS) { 149 DataType dataType = attributeField.getDataType(); 150 if (dataType == null) { 151 dataType = DataType.STRING; 152 } 153 if (configuration.getSupportedDataTypes().contains(dataType)) { 154 return configuration; 155 } 156 } 157 throw new IllegalArgumentException("Failed to determine proper searchable attribute configuration for given data type of '" + attributeField.getDataType() + "'"); 158 } 159 160 public static List<SearchableAttributeValue> getSearchableAttributeValueObjectTypes() { 161 List<SearchableAttributeValue> searchableAttributeValueClasses = new ArrayList<SearchableAttributeValue>(); 162 for (Class<? extends SearchableAttributeValue> searchAttributeValueClass : SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST) { 163 ObjectDefinition objDef = new ObjectDefinition(searchAttributeValueClass); 164 SearchableAttributeValue attributeValue = (SearchableAttributeValue) ObjectDefinitionResolver.createObject( 165 objDef, ClassLoaderUtils.getDefaultClassLoader(), false); 166 searchableAttributeValueClasses.add(attributeValue); 167 } 168 return searchableAttributeValueClasses; 169 } 170 171 public static SearchableAttributeValue getSearchableAttributeValueByDataTypeString(String dataType) { 172 SearchableAttributeValue returnableValue = null; 173 if (StringUtils.isBlank(dataType)) { 174 return returnableValue; 175 } 176 for (SearchableAttributeValue attValue : getSearchableAttributeValueObjectTypes()) 177 { 178 if (dataType.equalsIgnoreCase(attValue.getAttributeDataType())) 179 { 180 if (returnableValue != null) 181 { 182 String errorMsg = "Found two SearchableAttributeValue objects with same data type string ('" + dataType + "' while ignoring case): " + returnableValue.getClass().getName() + " and " + attValue.getClass().getName(); 183 LOG.error("getSearchableAttributeValueByDataTypeString() " + errorMsg); 184 throw new RuntimeException(errorMsg); 185 } 186 LOG.debug("getSearchableAttributeValueByDataTypeString() SearchableAttributeValue class name is " + attValue.getClass().getName() + "... ojbConcreteClassName is " + attValue.getOjbConcreteClass()); 187 ObjectDefinition objDef = new ObjectDefinition(attValue.getClass()); 188 returnableValue = (SearchableAttributeValue) ObjectDefinitionResolver.createObject(objDef, ClassLoaderUtils.getDefaultClassLoader(), false); 189 } 190 } 191 return returnableValue; 192 } 193 194 public static String getDisplayValueWithDateOnly(DateTime value) { 195 return getDisplayValueWithDateOnly(new Timestamp(value.getMillis())); 196 } 197 198 public static String getDisplayValueWithDateOnly(Timestamp value) { 199 return RiceConstants.getDefaultDateFormat().format(new Date(value.getTime())); 200 } 201 202 public static DateTime getLowerDateTimeBound(String dateRange) throws ParseException { 203 Range range = SearchExpressionUtils.parseRange(dateRange); 204 if (range == null) { 205 throw new IllegalArgumentException("Failed to parse date range from given string: " + dateRange); 206 } 207 if (range.getLowerBoundValue() != null) { 208 java.util.Date lowerRangeDate = null; 209 try{ 210 lowerRangeDate = CoreApiServiceLocator.getDateTimeService().convertToDate(range.getLowerBoundValue()); 211 }catch(ParseException pe){ 212 GlobalVariables.getMessageMap().putError("dateFrom", RiceKeyConstants.ERROR_CUSTOM, pe.getMessage()); 213 } 214 MutableDateTime dateTime = new MutableDateTime(lowerRangeDate); 215 dateTime.setMillisOfDay(0); 216 return dateTime.toDateTime(); 217 } 218 return null; 219 } 220 221 public static DateTime getUpperDateTimeBound(String dateRange) throws ParseException { 222 Range range = SearchExpressionUtils.parseRange(dateRange); 223 if (range == null) { 224 throw new IllegalArgumentException("Failed to parse date range from given string: " + dateRange); 225 } 226 if (range.getUpperBoundValue() != null) { 227 java.util.Date upperRangeDate = null; 228 try{ 229 upperRangeDate = CoreApiServiceLocator.getDateTimeService().convertToDate(range.getUpperBoundValue()); 230 }catch(ParseException pe){ 231 GlobalVariables.getMessageMap().putError("dateCreated", RiceKeyConstants.ERROR_CUSTOM, pe.getMessage()); 232 } 233 MutableDateTime dateTime = new MutableDateTime(upperRangeDate); 234 // set it to the last millisecond of the day 235 dateTime.setMillisOfDay((24 * 60 * 60 * 1000) - 1); 236 return dateTime.toDateTime(); 237 } 238 return null; 239 } 240 241 public static class SearchableAttributeConfiguration { 242 243 private final String tableName; 244 private final EnumSet<DataType> supportedDataTypes; 245 private final Class<?> dataTypeClass; 246 247 public SearchableAttributeConfiguration(String tableName, 248 EnumSet<DataType> supportedDataTypes, 249 Class<?> dataTypeClass) { 250 this.tableName = tableName; 251 this.supportedDataTypes = supportedDataTypes; 252 this.dataTypeClass = dataTypeClass; 253 } 254 255 public String getTableName() { 256 return tableName; 257 } 258 259 public EnumSet<DataType> getSupportedDataTypes() { 260 return supportedDataTypes; 261 } 262 263 public Class<?> getDataTypeClass() { 264 return dataTypeClass; 265 } 266 267 } 268 269 /** 270 * Unmarshals a DocumentSearchCriteria from JSON string 271 * @param string the JSON 272 * @return unmarshalled DocumentSearchCriteria 273 * @throws IOException 274 */ 275 public static DocumentSearchCriteria unmarshalDocumentSearchCriteria(String string) throws IOException { 276 DocumentSearchCriteria.Builder builder = getObjectMapper().readValue(string, DocumentSearchCriteria.Builder.class); 277 // fix up the Joda DateTimes 278 builder.normalizeDateTimes(); 279 // build() it 280 return builder.build(); 281 } 282 283 /** 284 * Marshals a DocumentSearchCriteria to JSON string 285 * @param criteria the criteria 286 * @return a JSON string 287 * @throws IOException 288 */ 289 public static String marshalDocumentSearchCriteria(DocumentSearchCriteria criteria) throws IOException { 290 // Jackson XC support not included by Rice, so no auto-magic JAXB-compatibility 291 // AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(); 292 // // make deserializer use JAXB annotations (only) 293 // mapper.getDeserializationConfig().setAnnotationIntrospector(introspector); 294 // // make serializer use JAXB annotations (only) 295 // mapper.getSerializationConfig().setAnnotationIntrospector(introspector); 296 return getObjectMapper().writeValueAsString(criteria); 297 } 298 299 public static List<RemotableAttributeError> validateSearchFieldValues(String fieldName, SearchableAttributeValue attributeValue, List<String> searchValues, String errorMessagePrefix, List<String> resultingValues, Function<String, Collection<RemotableAttributeError>> customValidator) { 300 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 301 // nothing to validate 302 if (CollectionUtils.isEmpty(searchValues)) { 303 return errors; 304 } 305 for (String searchValue: searchValues) { 306 errors.addAll(validateSearchFieldValue(fieldName, attributeValue, searchValue, errorMessagePrefix, resultingValues, customValidator)); 307 } 308 return Collections.unmodifiableList(errors); 309 } 310 311 /** 312 * Validates a single DocumentSearchCriteria searchable attribute field value (of the list of possibly multiple values) 313 * @param attributeValue the searchable attribute value type 314 * @param enteredValue the incoming DSC field value 315 * @param fieldName the name of the searchable attribute field/key 316 * @param errorMessagePrefix error message prefix 317 * @param resultingValues optional list of accumulated parsed values 318 * @param customValidator custom value validator to invoke on default validation success 319 * @return (possibly empty) list of validation error 320 */ 321 public static List<RemotableAttributeError> validateSearchFieldValue(String fieldName, SearchableAttributeValue attributeValue, String enteredValue, String errorMessagePrefix, List<String> resultingValues, Function<String, Collection<RemotableAttributeError>> customValidator) { 322 List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(); 323 if (enteredValue == null) { 324 return errors; 325 } 326 // TODO: this also parses compound expressions and therefore produces a list of strings 327 // how does this relate to DocumentSearchInternalUtils.parseRange... which should consume which? 328 List<String> parsedValues = SQLUtils.getCleanedSearchableValues(enteredValue, attributeValue.getAttributeDataType()); 329 for (String value: parsedValues) { 330 errors.addAll(validateParsedSearchFieldValue(fieldName, attributeValue, value, errorMessagePrefix, resultingValues, customValidator)); 331 } 332 return errors; 333 } 334 335 /** 336 * Validates a single terminal value from a single search field (list of values); calls a custom validator if default validation passes and 337 * custom validator is given 338 * @param attributeValue the searchable value type 339 * @param parsedValue the parsed value to validate 340 * @param fieldName the field name for error message 341 * @param errorMessagePrefix the prefix for error message 342 * @param resultingValues parsed value is appended to this list if present (non-null) 343 * @return immutable collection of errors (possibly empty) 344 */ 345 public static Collection<RemotableAttributeError> validateParsedSearchFieldValue(String fieldName, SearchableAttributeValue attributeValue, String parsedValue, String errorMessagePrefix, List<String> resultingValues, Function<String, Collection<RemotableAttributeError>> customValidator) { 346 Collection<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>(1); 347 String value = parsedValue; 348 if (attributeValue.allowsWildcards()) { // TODO: how should this work in relation to criteria expressions?? clean above removes * 349 value = value.replaceAll(KewApiConstants.SearchableAttributeConstants.SEARCH_WILDCARD_CHARACTER_REGEX_ESCAPED, ""); 350 } 351 352 if (resultingValues != null) { 353 resultingValues.add(value); 354 } 355 356 if (!attributeValue.isPassesDefaultValidation(value)) { 357 errorMessagePrefix = (StringUtils.isNotBlank(errorMessagePrefix)) ? errorMessagePrefix : "Field"; 358 String errorMsg = errorMessagePrefix + " with value '" + value + "' does not conform to standard validation for field type."; 359 LOG.debug("validateSimpleSearchFieldValue: " + errorMsg + " :: field type '" + attributeValue.getAttributeDataType() + "'"); 360 errors.add(RemotableAttributeError.Builder.create(fieldName, errorMsg).build()); 361 } else if (customValidator != null) { 362 errors.addAll(customValidator.apply(value)); 363 } 364 365 return Collections.unmodifiableCollection(errors); 366 } 367 368 /** 369 * Converts a searchable attribute field data type into a UI data type 370 * @param dataTypeValue the {@link SearchableAttributeValue} data type 371 * @return the corresponding {@link DataType} 372 */ 373 public static DataType convertValueToDataType(String dataTypeValue) { 374 if (StringUtils.isBlank(dataTypeValue)) { 375 return DataType.STRING; 376 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING.equals(dataTypeValue)) { 377 return DataType.STRING; 378 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE.equals(dataTypeValue)) { 379 return DataType.DATE; 380 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG.equals(dataTypeValue)) { 381 return DataType.LONG; 382 } else if (KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT.equals(dataTypeValue)) { 383 return DataType.FLOAT; 384 } 385 throw new IllegalArgumentException("Invalid dataTypeValue was given: " + dataTypeValue); 386 } 387 388}