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    }