View Javadoc
1   /**
2    * Copyright 2005-2015 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 java.io.IOException;
19  import java.sql.Date;
20  import java.sql.Timestamp;
21  import java.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.EnumSet;
26  import java.util.List;
27  
28  import org.apache.commons.collections.CollectionUtils;
29  import org.apache.commons.lang.StringUtils;
30  import org.apache.log4j.Logger;
31  import org.codehaus.jackson.map.ObjectMapper;
32  import org.codehaus.jackson.map.annotate.JsonSerialize;
33  import org.joda.time.DateTime;
34  import org.joda.time.MutableDateTime;
35  import org.kuali.rice.core.api.CoreApiServiceLocator;
36  import org.kuali.rice.core.api.data.DataType;
37  import org.kuali.rice.core.api.reflect.ObjectDefinition;
38  import org.kuali.rice.core.api.search.Range;
39  import org.kuali.rice.core.api.search.SearchExpressionUtils;
40  import org.kuali.rice.core.api.uif.AttributeLookupSettings;
41  import org.kuali.rice.core.api.uif.RemotableAttributeError;
42  import org.kuali.rice.core.api.uif.RemotableAttributeField;
43  import org.kuali.rice.core.api.util.ClassLoaderUtils;
44  import org.kuali.rice.core.api.util.RiceConstants;
45  import org.kuali.rice.core.api.util.RiceKeyConstants;
46  import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
47  import org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver;
48  import org.kuali.rice.kew.api.KewApiConstants;
49  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
50  import org.kuali.rice.krad.util.GlobalVariables;
51  
52  import com.google.common.base.Function;
53  
54  /**
55   * Defines various utilities for internal use in the reference implementation of the document search functionality.
56   *
57   * @author Kuali Rice Team (rice.collab@kuali.org)
58   */
59  public class DocumentSearchInternalUtils {
60  
61      private static final Logger LOG = Logger.getLogger(DocumentSearchInternalUtils.class);
62  
63      private static final boolean CASE_SENSITIVE_DEFAULT = false;
64  
65      private static final String STRING_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_T";
66      private static final String DATE_TIME_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_DT_T";
67      private static final String DECIMAL_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_FLT_T";
68      private static final String INTEGER_ATTRIBUTE_TABLE_NAME = "KREW_DOC_HDR_EXT_LONG_T";
69  
70      private static final List<SearchableAttributeConfiguration> CONFIGURATIONS =
71              new ArrayList<SearchableAttributeConfiguration>();
72      public static final List<Class<? extends SearchableAttributeValue>> SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST =
73              new ArrayList<Class<? extends SearchableAttributeValue>>();
74  
75      static {
76          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeStringValue.class);
77          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeFloatValue.class);
78          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeLongValue.class);
79          SEARCHABLE_ATTRIBUTE_BASE_CLASS_LIST.add(SearchableAttributeDateTimeValue.class);
80      }
81  
82      static {
83  
84          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
85                  STRING_ATTRIBUTE_TABLE_NAME,
86                  EnumSet.of(DataType.BOOLEAN, DataType.STRING, DataType.MARKUP),
87                  String.class));
88  
89          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
90                  DATE_TIME_ATTRIBUTE_TABLE_NAME,
91                  EnumSet.of(DataType.DATE, DataType.TRUNCATED_DATE, DataType.DATETIME),
92                  Timestamp.class));
93  
94          CONFIGURATIONS.add(new SearchableAttributeConfiguration(
95                  DECIMAL_ATTRIBUTE_TABLE_NAME,
96                  EnumSet.of(DataType.FLOAT, DataType.DOUBLE, DataType.CURRENCY),
97                  Float.TYPE));
98  
99          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 }