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;
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),
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 }