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