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