View Javadoc
1   /**
2    * Copyright 2005-2014 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.impl.document.search;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.ObjectUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.log4j.Logger;
22  import org.joda.time.DateTime;
23  import org.kuali.rice.core.api.search.Range;
24  import org.kuali.rice.core.api.search.SearchExpressionUtils;
25  import org.kuali.rice.core.api.uif.AttributeLookupSettings;
26  import org.kuali.rice.core.api.uif.RemotableAttributeField;
27  import org.kuali.rice.kew.api.KEWPropertyConstants;
28  import org.kuali.rice.kew.api.KewApiConstants;
29  import org.kuali.rice.kew.api.document.DocumentStatus;
30  import org.kuali.rice.kew.api.document.DocumentStatusCategory;
31  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
32  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
33  import org.kuali.rice.kew.api.document.search.RouteNodeLookupLogic;
34  import org.kuali.rice.kew.docsearch.DocumentSearchInternalUtils;
35  import org.kuali.rice.kew.doctype.bo.DocumentType;
36  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
37  import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils;
38  import org.kuali.rice.kew.service.KEWServiceLocator;
39  import org.kuali.rice.krad.util.KRADConstants;
40  
41  import java.lang.reflect.InvocationTargetException;
42  import java.util.ArrayList;
43  import java.util.Arrays;
44  import java.util.Collection;
45  import java.util.HashMap;
46  import java.util.HashSet;
47  import java.util.LinkedHashMap;
48  import java.util.List;
49  import java.util.Map;
50  import java.util.Set;
51  
52  /**
53   * Reference implementation of {@code DocumentSearchCriteriaTranslator}.
54   *
55   * @author Kuali Rice Team (rice.collab@kuali.org)
56   */
57  public class DocumentSearchCriteriaTranslatorImpl implements DocumentSearchCriteriaTranslator {
58  
59      private static final Logger LOG = Logger.getLogger(DocumentSearchCriteriaTranslatorImpl.class);
60  
61      private static final String DOCUMENT_STATUSES = "documentStatuses";
62      private static final String ROUTE_NODE_LOOKUP_LOGIC = "routeNodeLookupLogic";
63  
64      /**
65       * Fields which translate directory from criteria strings to properties on the DocumentSearchCriteria.
66       */
67      private static final String[] DIRECT_TRANSLATE_FIELD_NAMES = {
68              "documentId",
69              "applicationDocumentId",
70              "applicationDocumentStatus",
71              "initiatorPrincipalName",
72              "initiatorPrincipalId",
73              "viewerPrincipalName",
74              "viewerPrincipalId",
75              "groupViewerId",
76              "approverPrincipalName",
77              "approverPrincipalId",
78              "routeNodeName",
79              "documentTypeName",
80              "saveName",
81              "title",
82              "isAdvancedSearch"
83      };
84      private static final Set<String> DIRECT_TRANSLATE_FIELD_NAMES_SET =
85              new HashSet<String>(Arrays.asList(DIRECT_TRANSLATE_FIELD_NAMES));
86  
87      private static final String[] DATE_RANGE_TRANSLATE_FIELD_NAMES = {
88              "dateCreated",
89              "dateLastModified",
90              "dateApproved",
91              "dateFinalized"
92      };
93      private static final Set<String> DATE_RANGE_TRANSLATE_FIELD_NAMES_SET =
94              new HashSet<String>(Arrays.asList(DATE_RANGE_TRANSLATE_FIELD_NAMES));
95  
96      @Override
97      public DocumentSearchCriteria translateFieldsToCriteria(Map<String, String> fieldValues) {
98  
99          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 }