View Javadoc

1   /**
2    * Copyright 2005-2012 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              "viewerPrincipalName",
73              "groupViewerId",
74              "approverPrincipalName",
75              "routeNodeName",
76              "documentTypeName",
77              "saveName",
78              "title",
79              "isAdvancedSearch"
80      };
81      private static final Set<String> DIRECT_TRANSLATE_FIELD_NAMES_SET =
82              new HashSet<String>(Arrays.asList(DIRECT_TRANSLATE_FIELD_NAMES));
83  
84      private static final String[] DATE_RANGE_TRANSLATE_FIELD_NAMES = {
85              "dateCreated",
86              "dateLastModified",
87              "dateApproved",
88              "dateFinalized"
89      };
90      private static final Set<String> DATE_RANGE_TRANSLATE_FIELD_NAMES_SET =
91              new HashSet<String>(Arrays.asList(DATE_RANGE_TRANSLATE_FIELD_NAMES));
92  
93      @Override
94      public DocumentSearchCriteria translateFieldsToCriteria(Map<String, String> fieldValues) {
95  
96          DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
97          List<String> documentAttributeFields = new ArrayList<String>();
98          for (Map.Entry<String, String> field : fieldValues.entrySet()) {
99              try {
100                 if (StringUtils.isNotBlank(field.getValue())) {
101                     if (DIRECT_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) {
102                         PropertyUtils.setNestedProperty(criteria, field.getKey(), field.getValue());
103                     } else if (DATE_RANGE_TRANSLATE_FIELD_NAMES_SET.contains(field.getKey())) {
104                         applyDateRangeField(criteria, field.getKey(), field.getValue());
105                     } else if (field.getKey().startsWith(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) {
106                         documentAttributeFields.add(field.getKey());
107                     }
108 
109                 }
110             } catch (Exception e) {
111                 throw new IllegalStateException("Failed to set document search criteria field: " + field.getKey(), e);
112             }
113         }
114 
115         if (!documentAttributeFields.isEmpty()) {
116             translateDocumentAttributeFieldsToCriteria(fieldValues, documentAttributeFields, criteria);
117         }
118 
119         String routeNodeLookupLogic = fieldValues.get(ROUTE_NODE_LOOKUP_LOGIC);
120         if (StringUtils.isNotBlank(routeNodeLookupLogic)) {
121             criteria.setRouteNodeLookupLogic(RouteNodeLookupLogic.valueOf(routeNodeLookupLogic));
122         }
123 
124         String documentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE);
125         if (StringUtils.isNotBlank(documentStatusesValue)) {
126             String[] documentStatuses = documentStatusesValue.split(",");
127             for (String documentStatus : documentStatuses) {
128                 if (documentStatus.startsWith("category:")) {
129                     String categoryCode = StringUtils.remove(documentStatus, "category:");
130                     criteria.getDocumentStatusCategories().add(DocumentStatusCategory.fromCode(categoryCode));
131                 } else {
132                     criteria.getDocumentStatuses().add(DocumentStatus.fromCode(documentStatus));
133                 }
134             }
135         }
136 
137         LinkedHashMap<String, List<String>> applicationDocumentStatusGroupings =
138                 ApplicationDocumentStatusUtils.getApplicationDocumentStatusCategories(criteria.getDocumentTypeName());
139 
140         String applicationDocumentStatusesValue = fieldValues.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOC_STATUS);
141         if (StringUtils.isNotBlank(applicationDocumentStatusesValue)) {
142             String[] applicationDocumentStatuses = applicationDocumentStatusesValue.split(",");
143             for (String applicationDocumentStatus : applicationDocumentStatuses) {
144                 // KULRICE-7786: support for groups (categories) of application document statuses
145                 if (applicationDocumentStatus.startsWith("category:")) {
146                     String categoryCode = StringUtils.remove(applicationDocumentStatus, "category:");
147                     if (applicationDocumentStatusGroupings.containsKey(categoryCode)) {
148                         criteria.getApplicationDocumentStatuses().addAll(applicationDocumentStatusGroupings.get(categoryCode));
149                     }
150                 } else {
151                     criteria.getApplicationDocumentStatuses().add(applicationDocumentStatus);
152                 }
153             }
154         }
155 
156         // blank the deprecated field out, it's not needed.
157         criteria.setApplicationDocumentStatus(null);
158 
159         return criteria.build();
160     }
161 
162     /**
163      * Converts the DocumentSearchCriteria to a Map of values that can be applied to the Lookup form fields.
164      * @param criteria the criteria to translate
165      * @return a Map of values that can be applied to the Lookup form fields.
166      */
167     public Map<String, String[]> translateCriteriaToFields(DocumentSearchCriteria criteria) {
168         Map<String, String[]> values = new HashMap<String, String[]>();
169 
170         for (String property: DIRECT_TRANSLATE_FIELD_NAMES) {
171             convertCriteriaPropertyToField(criteria, property, values);
172         }
173 
174         for (String property: DATE_RANGE_TRANSLATE_FIELD_NAMES) {
175             convertCriteriaRangeField(criteria, property, values);
176         }
177 
178         Map<String, List<String>> docAttrValues = criteria.getDocumentAttributeValues();
179         if (!docAttrValues.isEmpty()) {
180             Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria);
181             for (Map.Entry<String, List<String>> entry: docAttrValues.entrySet()) {
182                 AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(entry.getKey());
183                 if (lookupSettings != null && lookupSettings.isRanged()) {
184                     convertAttributeRangeField(entry.getKey(), entry.getValue(), values);
185                 } else {
186                     values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + entry.getKey(), entry.getValue().toArray(new String[0]));
187                 }
188             }
189         }
190 
191         RouteNodeLookupLogic lookupLogic = criteria.getRouteNodeLookupLogic();
192         if (lookupLogic != null) {
193             values.put(ROUTE_NODE_LOOKUP_LOGIC, new String[]{lookupLogic.name()});
194         }
195 
196         Collection<String> statuses = new ArrayList<String>();
197         for (DocumentStatus status: criteria.getDocumentStatuses()) {
198             statuses.add(status.getCode());
199         }
200         for (DocumentStatusCategory category: criteria.getDocumentStatusCategories()) {
201             statuses.add("category:" + category.getCode());
202         }
203         values.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE, statuses.toArray(new String[0]));
204 
205         return values;
206     }
207 
208     /**
209      * Convert a ranged document search attribute field into a form field.
210      * This means:
211      * 0) the attribute field has been identified as a ranged attribute
212      * 1) we need to parse the attribute search expression to find upper and lower bounds
213      * 2) set upper and lower bounds in distinct form fields
214      * @param attrKey the attribute key
215      * @param attrValues the attribute value
216      */
217     protected static void convertAttributeRangeField(String attrKey, List<String> attrValues, Map<String, String[]> values) {
218         String value = "";
219         if (attrValues != null && !attrValues.isEmpty()) {
220             value = attrValues.get(0);
221             // can ranged attributes be multi-valued?
222             if (attrValues.size() > 1) {
223                 LOG.warn("Encountered multi-valued ranged document search attribute '" + attrKey + "': " + attrValues);
224             }
225         }
226         Range range = SearchExpressionUtils.parseRange(value);
227         String lower;
228         String upper;
229         if (range != null) {
230             lower = range.getLowerBoundValue();
231             upper = range.getUpperBoundValue();
232         } else {
233             lower = null;
234             upper = value;
235         }
236         values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attrKey, new String[] { lower });
237         values.put(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + attrKey, new String[] { upper });
238     }
239 
240     /**
241      * Convenience method for converting a set of doc search criteria range fields into form fields
242      * @param criteria the dsc
243      * @param property the abstract property name
244      * @param values the form field values
245      */
246     protected static void convertCriteriaRangeField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) {
247         convertCriteriaPropertyToField(criteria, property + "From", KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property, values);
248         convertCriteriaPropertyToField(criteria, property + "To", property, values);
249     }
250 
251     /**
252      * Looks up a property on the criteria object and sets it as a key/value pair in the values Map
253      * @param criteria the DocumentSearchCriteria
254      * @param property the DocumentSearchCriteria property name and destination field name
255      * @param values the map of values to update
256      */
257     protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, Map<String, String[]> values) {
258         convertCriteriaPropertyToField(criteria, property, property, values);
259     }
260 
261     /**
262      * Looks up a property on the criteria object and sets it as a key/value pair in the values Map
263      * @param criteria the DocumentSearchCriteria
264      * @param property the DocumentSearchCriteria property name
265      * @param fieldName the destination field name
266      * @param values the map of values to update
267      */
268     protected static void convertCriteriaPropertyToField(DocumentSearchCriteria criteria, String property, String fieldName, Map<String, String[]> values) {
269         try {
270             Object val = PropertyUtils.getProperty(criteria, property);
271             if (val != null) {
272                 values.put(fieldName, new String[] { ObjectUtils.toString(val) });
273             }
274         } catch (NoSuchMethodException nsme) {
275             LOG.error("Error reading property '" + property + "' of criteria", nsme);
276         } catch (InvocationTargetException ite) {
277             LOG.error("Error reading property '" + property + "' of criteria", ite);
278         } catch (IllegalAccessException iae) {
279             LOG.error("Error reading property '" + property + "' of criteria", iae);
280 
281         }
282     }
283 
284     protected void applyDateRangeField(DocumentSearchCriteria.Builder criteria, String fieldName, String fieldValue) throws Exception {
285         DateTime lowerDateTime = DocumentSearchInternalUtils.getLowerDateTimeBound(fieldValue);
286         DateTime upperDateTime = DocumentSearchInternalUtils.getUpperDateTimeBound(fieldValue);
287         if (lowerDateTime != null) {
288             PropertyUtils.setNestedProperty(criteria, fieldName + "From", lowerDateTime);
289         }
290         if (upperDateTime != null) {
291             PropertyUtils.setNestedProperty(criteria, fieldName + "To", upperDateTime);
292         }
293     }
294 
295     /**
296      * Returns a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria
297      * @param criteria the doc search criteria
298      * @return a map of attributelookupsettings for the custom search attributes of the document if specified in the criteria, empty otherwise
299      */
300     protected Map<String, AttributeLookupSettings> getAttributeLookupSettings(DocumentSearchCriteriaContract criteria) {
301         String documentTypeName = criteria.getDocumentTypeName();
302         Map<String, AttributeLookupSettings> attributeLookupSettingsMap = new HashMap<java.lang.String, AttributeLookupSettings>();
303 
304         if (StringUtils.isNotEmpty(documentTypeName)) {
305             DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName);
306             if (documentType != null) {
307                 DocumentSearchCriteriaConfiguration configuration = KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(
308                         documentType);
309                 if (configuration != null) {
310                     List<RemotableAttributeField> remotableAttributeFields = configuration.getFlattenedSearchAttributeFields();
311                     for (RemotableAttributeField raf: remotableAttributeFields) {
312                         attributeLookupSettingsMap.put(raf.getName(), raf.getAttributeLookupSettings());
313                     }
314                 }
315             } else {
316                 LOG.error("Searching against unknown document type '" + documentTypeName + "'; searchable attribute ranges will not work.");
317             }
318         }
319 
320         return attributeLookupSettingsMap;
321     }
322 
323     protected String translateRangePropertyToExpression(Map<String, String> fieldValues, String property, String prefix, AttributeLookupSettings settings) {
324         String lowerBoundValue = fieldValues.get(prefix + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + property);
325         String upperBoundValue = fieldValues.get(prefix + property);
326 
327         Range range = new Range();
328         // defaults for general lookup/search
329         range.setLowerBoundInclusive(settings.isLowerBoundInclusive());
330         range.setUpperBoundInclusive(settings.isUpperBoundInclusive());
331         range.setLowerBoundValue(lowerBoundValue);
332         range.setUpperBoundValue(upperBoundValue);
333 
334         String expr = range.toString();
335         if (StringUtils.isEmpty(expr)) {
336             expr = upperBoundValue;
337         }
338         return expr;
339     }
340 
341     protected void translateDocumentAttributeFieldsToCriteria(Map<String, String> fieldValues, List<String> fields, DocumentSearchCriteria.Builder criteria) {
342         Map<String, AttributeLookupSettings> attributeLookupSettingsMap = getAttributeLookupSettings(criteria);
343         for (String field: fields) {
344             String documentAttributeName = field.substring(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX.length());
345             // omit the synthetic lower bound field, don't set back into doc attrib values
346             if (documentAttributeName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
347                 continue;
348             }
349             String value = fieldValues.get(field);
350             AttributeLookupSettings lookupSettings = attributeLookupSettingsMap.get(documentAttributeName);
351             if (lookupSettings != null && lookupSettings.isRanged()) {
352                 value = translateRangePropertyToExpression(fieldValues, documentAttributeName, KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX, lookupSettings);
353             }
354             applyDocumentAttribute(criteria, documentAttributeName, value);
355         }
356     }
357 
358     protected void applyDocumentAttribute(DocumentSearchCriteria.Builder criteria, String documentAttributeName, String attributeValue) {
359         criteria.addDocumentAttributeValue(documentAttributeName, attributeValue);
360     }
361 
362 }