001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kew.docsearch;
017
018import com.google.common.base.Function;
019import org.apache.commons.collections.CollectionUtils;
020import org.apache.commons.lang.StringUtils;
021import org.apache.log4j.Logger;
022import org.codehaus.jackson.map.ObjectMapper;
023import org.codehaus.jackson.map.annotate.JsonSerialize;
024import org.joda.time.DateTime;
025import org.joda.time.MutableDateTime;
026import org.kuali.rice.core.api.CoreApiServiceLocator;
027import org.kuali.rice.core.api.reflect.ObjectDefinition;
028import org.kuali.rice.core.api.search.Range;
029import org.kuali.rice.core.api.search.SearchExpressionUtils;
030import org.kuali.rice.core.api.uif.AttributeLookupSettings;
031import org.kuali.rice.core.api.uif.DataType;
032import org.kuali.rice.core.api.uif.RemotableAttributeError;
033import org.kuali.rice.core.api.uif.RemotableAttributeField;
034import org.kuali.rice.core.api.util.ClassLoaderUtils;
035import org.kuali.rice.core.api.util.RiceConstants;
036import org.kuali.rice.core.api.util.RiceKeyConstants;
037import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
038import org.kuali.rice.core.framework.resourceloader.ObjectDefinitionResolver;
039import org.kuali.rice.kew.api.KewApiConstants;
040import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
041import org.kuali.rice.krad.util.GlobalVariables;
042
043import java.io.IOException;
044import java.sql.Date;
045import java.sql.Timestamp;
046import java.text.ParseException;
047import java.util.ArrayList;
048import java.util.Collection;
049import java.util.Collections;
050import java.util.EnumSet;
051import 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 */
058public 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}