001/**
002 * Copyright 2005-2014 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.impl.document.search;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.ObjectUtils;
020import org.apache.commons.lang.StringUtils;
021import org.apache.log4j.Logger;
022import org.joda.time.DateTime;
023import org.kuali.rice.core.api.search.Range;
024import org.kuali.rice.core.api.search.SearchExpressionUtils;
025import org.kuali.rice.core.api.uif.AttributeLookupSettings;
026import org.kuali.rice.core.api.uif.RemotableAttributeField;
027import org.kuali.rice.kew.api.KEWPropertyConstants;
028import org.kuali.rice.kew.api.KewApiConstants;
029import org.kuali.rice.kew.api.document.DocumentStatus;
030import org.kuali.rice.kew.api.document.DocumentStatusCategory;
031import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
032import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
033import org.kuali.rice.kew.api.document.search.RouteNodeLookupLogic;
034import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
035import org.kuali.rice.kew.doctype.bo.DocumentType;
036import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
037import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils;
038import org.kuali.rice.kew.service.KEWServiceLocator;
039import org.kuali.rice.krad.util.KRADConstants;
040
041import java.lang.reflect.InvocationTargetException;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collection;
045import java.util.HashMap;
046import java.util.HashSet;
047import java.util.LinkedHashMap;
048import java.util.List;
049import java.util.Map;
050import java.util.Set;
051
052/**
053 * Reference implementation of {@code DocumentSearchCriteriaTranslator}.
054 *
055 * @author Kuali Rice Team (rice.collab@kuali.org)
056 */
057public class DocumentSearchCriteriaTranslatorImpl implements DocumentSearchCriteriaTranslator {
058
059    private static final Logger LOG = Logger.getLogger(DocumentSearchCriteriaTranslatorImpl.class);
060
061    private static final String DOCUMENT_STATUSES = "documentStatuses";
062    private static final String ROUTE_NODE_LOOKUP_LOGIC = "routeNodeLookupLogic";
063
064    /**
065     * Fields which translate directory from criteria strings to properties on the DocumentSearchCriteria.
066     */
067    private static final String[] DIRECT_TRANSLATE_FIELD_NAMES = {
068            "documentId",
069            "applicationDocumentId",
070            "applicationDocumentStatus",
071            "initiatorPrincipalName",
072            "initiatorPrincipalId",
073            "viewerPrincipalName",
074            "viewerPrincipalId",
075            "groupViewerId",
076            "approverPrincipalName",
077            "approverPrincipalId",
078            "routeNodeName",
079            "documentTypeName",
080            "saveName",
081            "title",
082            "isAdvancedSearch"
083    };
084    private static final Set<String> DIRECT_TRANSLATE_FIELD_NAMES_SET =
085            new HashSet<String>(Arrays.asList(DIRECT_TRANSLATE_FIELD_NAMES));
086
087    private static final String[] DATE_RANGE_TRANSLATE_FIELD_NAMES = {
088            "dateCreated",
089            "dateLastModified",
090            "dateApproved",
091            "dateFinalized"
092    };
093    private static final Set<String> DATE_RANGE_TRANSLATE_FIELD_NAMES_SET =
094            new HashSet<String>(Arrays.asList(DATE_RANGE_TRANSLATE_FIELD_NAMES));
095
096    @Override
097    public DocumentSearchCriteria translateFieldsToCriteria(Map<String, String> fieldValues) {
098
099        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
100        List<String> documentAttributeFields = new ArrayList<String>();
101        for (Map.Entry<String, String> field : fieldValues.entrySet()) {
102            try {
103                if (StringUtils.isNotBlank(field.getValue())) {
104                    if (DIRECT_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) {
105                        PropertyUtils.setNestedProperty(criteria, field.getKey(), field.getValue());
106                    } else if (DATE_RANGE_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) {
107                        applyDateRangeField(criteria, field.getKey(), field.getValue());
108                    } else if (field.getKey().startsWith(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) {
109                        documentAttributeFields.add(field.getKey());
110                    }
111
112                }
113            } catch (Exception e) {
114                throw new IllegalStateException("Failed to set document search criteria field: " + field.getKey(), e);
115            }
116        }
117
118        if (!documentAttributeFields.isEmpty()) {
119            translateDocumentAttributeFieldsToCriteria(fieldValues, documentAttributeFields, criteria);
120        }
121
122        String routeNodeLookupLogic = fieldValues.get(ROUTE_NODE_LOOKUP_LOGIC);
123        if (StringUtils.isNotBlank(routeNodeLookupLogic)) {
124            criteria.setRouteNodeLookupLogic(RouteNodeLookupLogic.valueOf(routeNodeLookupLogic));
125        }
126
127        String documentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE);
128        if (StringUtils.isNotBlank(documentStatusesValue)) {
129            String[] documentStatuses = documentStatusesValue.split(",");
130            for (String documentStatus : documentStatuses) {
131                if (documentStatus.startsWith("category:")) {
132                    String categoryCode = StringUtils.remove(documentStatus, "category:");
133                    criteria.getDocumentStatusCategories().add(DocumentStatusCategory.fromCode(categoryCode));
134                } else {
135                    criteria.getDocumentStatuses().add(DocumentStatus.fromCode(documentStatus));
136                }
137            }
138        }
139
140        LinkedHashMap<String, List<String>> applicationDocumentStatusGroupings =
141                ApplicationDocumentStatusUtils.getApplicationDocumentStatusCategories(criteria.getDocumentTypeName());
142
143        String applicationDocumentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS);
144        if (StringUtils.isNotBlank(applicationDocumentStatusesValue)) {
145            String[] applicationDocumentStatuses = applicationDocumentStatusesValue.split(",");
146            for (String applicationDocumentStatus : applicationDocumentStatuses) {
147                // KULRICE-7786: support for groups (categories) of application document statuses
148                if (applicationDocumentStatus.startsWith("category:")) {
149                    String categoryCode = StringUtils.remove(applicationDocumentStatus, "category:");
150                    if (applicationDocumentStatusGroupings.containsKey(categoryCode)) {
151                        criteria.getApplicationDocumentStatuses().addAll(applicationDocumentStatusGroupings.get(categoryCode));
152                    }
153                } else {
154                    criteria.getApplicationDocumentStatuses().add(applicationDocumentStatus);
155                }
156            }
157        }
158
159        // blank the deprecated field out, it's not needed.
160        criteria.setApplicationDocumentStatus(null);
161
162        return criteria.build();
163    }
164
165    /**
166     * Converts the DocumentSearchCriteria to a Map of values that can be applied to the Lookup form fields.
167     * @param criteria the criteria to translate
168     * @return a Map of values that can be applied to the Lookup form fields.
169     */
170    public Map<String, String[]> translateCriteriaToFields(DocumentSearchCriteria criteria) {
171        Map<String, String[]> values = new HashMap<String, String[]>();
172
173        for (String property: DIRECT_TRANSLATE_FIELD_NAMES) {
174            convertCriteriaPropertyToField(criteria, property, values);
175        }
176
177        for (String property: DATE_RANGE_TRANSLATE_FIELD_NAMES) {
178            convertCriteriaRangeField(criteria, property, values);
179        }
180
181        Map<String, List<String>> docAttrValues = criteria.getDocumentAttributeValues();
182        if (!docAttrValues.isEmpty()) {
183            Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria);
184            for (Map.Entry<String, List<String>> entry: docAttrValues.entrySet()) {
185                AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(entry.getKey());
186                if (lookupSettings != null && lookupSettings.isRanged()) {
187                    convertAttributeRangeField(entry.getKey(), entry.getValue(), values);
188                } else {
189                    values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + entry.getKey(), entry.getValue().toArray(new String[0]));
190                }
191            }
192        }
193
194        RouteNodeLookupLogic lookupLogic = criteria.getRouteNodeLookupLogic();
195        if (lookupLogic != null) {
196            values.put(ROUTE_NODE_LOOKUP_LOGIC, new String[]{lookupLogic.name()});
197        }
198
199        Collection<String> statuses = new ArrayList<String>();
200        for (DocumentStatus status: criteria.getDocumentStatuses()) {
201            statuses.add(status.getCode());
202        }
203        for (DocumentStatusCategory category: criteria.getDocumentStatusCategories()) {
204            statuses.add("category:" + category.getCode());
205        }
206        values.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE, statuses.toArray(new String[0]));
207
208        return values;
209    }
210
211    /**
212     * Convert a ranged document search attribute field into a form field.
213     * This means:
214     * 0) the attribute field has been identified as a ranged attribute
215     * 1) we need to parse the attribute search expression to find upper and lower bounds
216     * 2) set upper and lower bounds in distinct form fields
217     * @param attrKey the attribute key
218     * @param attrValues the attribute value
219     */
220    protected static void convertAttributeRangeField(String attrKey, List<String> attrValues, Map<String, String[]> values) {
221        String value = "";
222        if (attrValues != null && !attrValues.isEmpty()) {
223            value = attrValues.get(0);
224            // can ranged attributes be multi-valued?
225            if (attrValues.size() > 1) {
226                LOG.warn("Encountered multi-valued ranged document search attribute '" + attrKey + "': " + attrValues);
227            }
228        }
229        Range range = SearchExpressionUtils.parseRange(value);
230        String lower;
231        String upper;
232        if (range != null) {
233            lower = range.getLowerBoundValue();
234            upper = range.getUpperBoundValue();
235        } else {
236            lower = null;
237            upper = value;
238        }
239        values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attrKey, new String[] { lower });
240        values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + attrKey, new String[] { upper });
241    }
242
243    /**
244     * Convenience method for converting a set of doc search criteria range fields into form fields
245     * @param criteria the dsc
246     * @param property the abstract property name
247     * @param values the form field values
248     */
249    protected static void convertCriteriaRangeField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) {
250        convertCriteriaPropertyToField(criteria, property + "From", KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property, values);
251        convertCriteriaPropertyToField(criteria, property + "To", property, values);
252    }
253
254    /**
255     * Looks up a property on the criteria object and sets it as a key/value pair in the values Map
256     * @param criteria the DocumentSearchCriteria
257     * @param property the DocumentSearchCriteria property name and destination field name
258     * @param values the map of values to update
259     */
260    protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) {
261        convertCriteriaPropertyToField(criteria, property, property, values);
262    }
263
264    /**
265     * Looks up a property on the criteria object and sets it as a key/value pair in the values Map
266     * @param criteria the DocumentSearchCriteria
267     * @param property the DocumentSearchCriteria property name
268     * @param fieldName the destination field name
269     * @param values the map of values to update
270     */
271    protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, String fieldName, Map<String, String[]> values) {
272        try {
273            Object val = PropertyUtils.getProperty(criteria, property);
274            if (val != null) {
275                values.put(fieldName, new String[] { ObjectUtils.toString(val) });
276            }
277        } catch (NoSuchMethodException nsme) {
278            LOG.error("Error reading property '" + property + "' of criteria", nsme);
279        } catch (InvocationTargetException ite) {
280            LOG.error("Error reading property '" + property + "' of criteria", ite);
281        } catch (IllegalAccessException iae) {
282            LOG.error("Error reading property '" + property + "' of criteria", iae);
283
284        }
285    }
286
287    protected void applyDateRangeField(DocumentSearchCriteria.Builder criteria, String fieldName, String fieldValue) throws Exception {
288        DateTime lowerDateTime = DocumentSearchInternalUtils.getLowerDateTimeBound(fieldValue);
289        DateTime upperDateTime = DocumentSearchInternalUtils.getUpperDateTimeBound(fieldValue);
290        if (lowerDateTime != null) {
291            PropertyUtils.setNestedProperty(criteria, fieldName + "From", lowerDateTime);
292        }
293        if (upperDateTime != null) {
294            PropertyUtils.setNestedProperty(criteria, fieldName + "To", upperDateTime);
295        }
296    }
297
298    /**
299     * Returns a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria
300     * @param criteria the doc search criteria
301     * @return a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria, empty otherwise
302     */
303    protected Map<String, AttributeLookupSettings> getAttributeLookupSettings(DocumentSearchCriteriaContract criteria) {
304        String documentTypeName = criteria.getDocumentTypeName();
305        Map<String, AttributeLookupSettings> attributeLookupSettingsMap = new HashMap<java.lang.String, AttributeLookupSettings>();
306
307        if (StringUtils.isNotEmpty(documentTypeName)) {
308            DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName);
309            if (documentType != null) {
310                DocumentSearchCriteriaConfiguration configuration = KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(
311                        documentType);
312                if (configuration != null) {
313                    List<RemotableAttributeField> remotableAttributeFields = configuration.getFlattenedSearchAttributeFields();
314                    for (RemotableAttributeField raf: remotableAttributeFields) {
315                        attributeLookupSettingsMap.put(raf.getName(), raf.getAttributeLookupSettings());
316                    }
317                }
318            } else {
319                LOG.error("Searching against unknown document type '" + documentTypeName + "'; searchable attribute ranges will not work.");
320            }
321        }
322
323        return attributeLookupSettingsMap;
324    }
325
326    protected String translateRangePropertyToExpression(Map<String, String> fieldValues, String property, String prefix, AttributeLookupSettings settings) {
327        String lowerBoundValue = fieldValues.get(prefix + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property);
328        String upperBoundValue = fieldValues.get(prefix + property);
329
330        Range range = new Range();
331        // defaults for general lookup/search
332        range.setLowerBoundInclusive(settings.isLowerBoundInclusive());
333        range.setUpperBoundInclusive(settings.isUpperBoundInclusive());
334        range.setLowerBoundValue(lowerBoundValue);
335        range.setUpperBoundValue(upperBoundValue);
336
337        String expr = range.toString();
338        if (StringUtils.isEmpty(expr)) {
339            expr = upperBoundValue;
340        }
341        return expr;
342    }
343
344    protected void translateDocumentAttributeFieldsToCriteria(Map<String, String> fieldValues, List<String> fields, DocumentSearchCriteria.Builder criteria) {
345        Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria);
346        for (String field: fields) {
347            String documentAttributeName = field.substring(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX.length());
348            // omit the synthetic lower bound field is there is an upper bound field, don't set back into doc attrib values
349            if (documentAttributeName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
350                String tempDocumentAttributeName = StringUtils.substringAfter(documentAttributeName,KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX) ;
351                String tempField = fieldValues.get(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + tempDocumentAttributeName);
352                if (StringUtils.isEmpty(tempField)) {
353                    documentAttributeName =  tempDocumentAttributeName;
354                } else {
355                    continue;
356                }
357            }
358            String value = fieldValues.get(field);
359            AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(documentAttributeName);
360            if (lookupSettings != null && lookupSettings.isRanged()) {
361                value = translateRangePropertyToExpression(fieldValues, documentAttributeName, KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX, lookupSettings);
362            }
363            applyDocumentAttribute(criteria, documentAttributeName, value);
364        }
365    }
366
367    protected void applyDocumentAttribute(DocumentSearchCriteria.Builder criteria, String documentAttributeName, String attributeValue) {
368        criteria.addDocumentAttributeValue(documentAttributeName, attributeValue);
369    }
370
371}