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