View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.docsearch;
17  
18  import com.google.common.base.Function;
19  import org.apache.commons.collections.CollectionUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.log4j.Logger;
22  import org.codehaus.jackson.map.ObjectMapper;
23  import org.codehaus.jackson.map.annotate.JsonSerialize;
24  import org.joda.time.DateTime;
25  import org.joda.time.MutableDateTime;
26  import org.kuali.rice.core.api.CoreApiServiceLocator;
27  import org.kuali.rice.core.api.reflect.ObjectDefinition;
28  import org.kuali.rice.core.api.search.Range;
29  import org.kuali.rice.core.api.search.SearchExpressionUtils;
30  import org.kuali.rice.core.api.uif.AttributeLookupSettings;
31  import org.kuali.rice.core.api.uif.DataType;
32  import org.kuali.rice.core.api.uif.RemotableAttributeError;
33  import org.kuali.rice.core.api.uif.RemotableAttributeField;
34  import org.kuali.rice.core.api.util.ClassLoaderUtils;
35  import org.kuali.rice.core.api.util.RiceConstants;
36  import org.kuali.rice.core.api.util.RiceKeyConstants;
37  import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
38  import org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver;
39  import org.kuali.rice.kew.api.KewApiConstants;
40  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
41  import org.kuali.rice.krad.util.GlobalVariables;
42  
43  import java.io.IOException;
44  import java.sql.Date;
45  import java.sql.Timestamp;
46  import java.text.ParseException;
47  import java.util.ArrayList;
48  import java.util.Collection;
49  import java.util.Collections;
50  import java.util.EnumSet;
51  import java.util.List;
52  
53  /**
54   * Defines various utilities for internal use in the reference implementation of the document search functionality.
55   *
56   * @author Kuali Rice Team (rice.collab@kuali.org)
57   */
58  public class DocumentSearchInternalUtils {
59  
60      private static final Logger LOG = Logger.getLogger(DocumentSearchInternalUtils.class);
61  
62      private static final boolean CASE_SENSITIVE_DEFAULT = false;
63  
64      private static final String STRING_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_T";
65      private static final String DATE_TIME_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_DT_T";
66      private static final String DECIMAL_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_FLT_T";
67      private static final String INTEGER_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_LONG_T";
68  
69      private static final List<SearchableAttributeConfiguration> CONFIGURATIONS =
70              new ArrayList<SearchableAttributeConfiguration>();
71      public static final List<Class<? extends SearchableAttributeValue>> SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST =
72              new ArrayList<Class<? extends SearchableAttributeValue>>();
73  
74      static {
75          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeStringValue.class);
76          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeFloatValue.class);
77          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeLongValue.class);
78          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeDateTimeValue.class);
79      }
80  
81      static {
82  
83          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
84                  STRING_ATTRIBUTE_TABLE_NAME,
85                  EnumSet.of(DataType.BOOLEAN, DataType.STRING, DataType.MARKUP),
86                  String.class));
87  
88          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
89                  DATE_TIME_ATTRIBUTE_TABLE_NAME,
90                  EnumSet.of(DataType.DATE, DataType.TRUNCATED_DATE, DataType.DATETIME),
91                  Timestamp.class));
92  
93          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
94                  DECIMAL_ATTRIBUTE_TABLE_NAME,
95                  EnumSet.of(DataType.FLOAT, DataType.DOUBLE, DataType.CURRENCY),
96                  Float.TYPE));
97  
98          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
99                  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 }