View Javadoc

1   /**
2    * Copyright 2005-2013 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.docsearch;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.uif.RemotableAttributeField;
20  import org.kuali.rice.core.api.util.ConcreteKeyValue;
21  import org.kuali.rice.core.api.util.KeyValue;
22  import org.kuali.rice.kew.api.KewApiConstants;
23  import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
24  import org.kuali.rice.kew.doctype.bo.DocumentType;
25  import org.kuali.rice.kew.engine.node.RouteNode;
26  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
27  import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils;
28  import org.kuali.rice.kew.service.KEWServiceLocator;
29  import org.kuali.rice.kns.util.FieldUtils;
30  import org.kuali.rice.kns.web.ui.Field;
31  import org.kuali.rice.kns.web.ui.Row;
32  import org.kuali.rice.krad.util.KRADConstants;
33  
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collection;
37  import java.util.LinkedHashMap;
38  import java.util.LinkedHashSet;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Set;
42  
43  /**
44   * This class adapts the RemotableAttributeField instances from the various attributes
45   * associated with a document type and combines with the "default" rows for the search,
46   * returning the final List of Row objects to render for the document search.
47   *
48   * <p>Implementation note:</p>
49   * <p>
50   * This implementation relies on applicationDocumentStatus, and dateApplicationDocumentStatusChanged conditional fields
51   * being defined in the DD for basic display purposes.  These fields are conditionally shown depending on whether
52   * a document supporting application document statuses has been specified.  Further, the applicationDocumentStatus field
53   * is dynamically switched to a multiselect when the document specifies an explicit enumeration of valid statuses (this
54   * control switching is something that is not possible via declarative DD, at the time of this writing).
55   * </p>
56   * <p>
57   * In addition the routeNodeName field is dynamically populated with the list of route nodes for the specified document
58   * type.
59   * <p>
60   * Note: an alternative to programmatically providing dynamic select values is to define a value finder declaratively in
61   * DD.  KeyValueFinder however does not have access to request state, including the required document type, which would mean
62   * resorting to GlobalVariables inspection.  In reluctance to add yet another dependency on this external state, the fixups
63   * are done programmatically in this class. (see {@link #applyApplicationDocumentStatusCustomizations(org.kuali.rice.kns.web.ui.Field, org.kuali.rice.kew.doctype.bo.DocumentType)},
64   * {@link #applyRouteNodeNameCustomizations(org.kuali.rice.kns.web.ui.Field, org.kuali.rice.kew.doctype.bo.DocumentType)}).
65   * </p>
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   *
68   */
69  public class DocumentSearchCriteriaProcessorKEWAdapter implements DocumentSearchCriteriaProcessor {
70      /**
71       * Name if the hidden input field containing non-superuser/superuser search toggle state
72       */
73      public static final String SUPERUSER_SEARCH_FIELD = "superUserSearch";
74      /**
75       * Name if the hidden input field containing the clear saved search flag
76       */
77      public static final String CLEARSAVED_SEARCH_FIELD = "resetSavedSearch";
78  
79      /**
80       * Indicates where document attributes should be placed inside search criteria
81       */
82      private static final String DOCUMENT_ATTRIBUTE_FIELD_MARKER = "DOCUMENT_ATTRIBUTE_FIELD_MARKER";
83  
84      private static final String APPLICATION_DOCUMENT_STATUS = "applicationDocumentStatus";
85      private static final String DATE_APP_DOC_STATUS_CHANGED_FROM = "rangeLowerBoundKeyPrefix_dateApplicationDocumentStatusChanged";
86      private static final String DATE_APP_DOC_STATUS_CHANGED = "dateApplicationDocumentStatusChanged";
87      private static final String ROUTE_NODE_NAME = "routeNodeName";
88      private static final String ROUTE_NODE_LOGIC = "routeNodeLogic";
89  
90      private static final String[] BASIC_FIELD_NAMES = {
91              "documentTypeName",
92              "initiatorPrincipalName",
93              "documentId",
94              APPLICATION_DOCUMENT_STATUS,
95              "dateCreated",
96              DOCUMENT_ATTRIBUTE_FIELD_MARKER,
97              "saveName"
98      };
99  
100     private static final String[] ADVANCED_FIELD_NAMES = {
101             "documentTypeName",
102             "initiatorPrincipalName",
103             "approverPrincipalName",
104             "viewerPrincipalName",
105             "groupViewerName",
106             "groupViewerId",
107             "documentId",
108             "applicationDocumentId",
109             "statusCode",
110             APPLICATION_DOCUMENT_STATUS,
111             DATE_APP_DOC_STATUS_CHANGED,
112             ROUTE_NODE_NAME,
113             ROUTE_NODE_LOGIC,
114             "dateCreated",
115             "dateApproved",
116             "dateLastModified",
117             "dateFinalized",
118             "title",
119             DOCUMENT_ATTRIBUTE_FIELD_MARKER,
120             "saveName"
121     };
122 
123     /**
124      * Fields that are only applicable if a document type has been specified
125      */
126     private static final Collection<String> DOCUMENTTYPE_DEPENDENT_FIELDS = Arrays.asList(new String[] { DOCUMENT_ATTRIBUTE_FIELD_MARKER, APPLICATION_DOCUMENT_STATUS, DATE_APP_DOC_STATUS_CHANGED_FROM, DATE_APP_DOC_STATUS_CHANGED, ROUTE_NODE_NAME, ROUTE_NODE_LOGIC });
127     /**
128      * Fields that are only applicable if application document status is in use (assumes documenttype dependency)
129      */
130     private static final Collection<String> DOCSTATUS_DEPENDENT_FIELDS = Arrays.asList(new String[] { APPLICATION_DOCUMENT_STATUS, DATE_APP_DOC_STATUS_CHANGED_FROM, DATE_APP_DOC_STATUS_CHANGED });
131 
132 
133     @Override
134     public List<Row> getRows(DocumentType documentType, List<Row> defaultRows, boolean advancedSearch, boolean superUserSearch) {
135         List<Row> rows = null;
136         if(advancedSearch) {
137             rows = loadRowsForAdvancedSearch(defaultRows, documentType);
138         } else {
139             rows = loadRowsForBasicSearch(defaultRows, documentType);
140         }
141         addHiddenFields(rows, advancedSearch, superUserSearch);
142         return rows;
143     }
144 
145     protected List<Row> loadRowsForAdvancedSearch(List<Row> defaultRows, DocumentType documentType) {
146         List<Row> rows = new ArrayList<Row>();
147         loadRowsWithFields(rows, defaultRows, ADVANCED_FIELD_NAMES, documentType);
148         return rows;
149     }
150 
151     protected List<Row> loadRowsForBasicSearch(List<Row> defaultRows, DocumentType documentType) {
152         List<Row> rows = new ArrayList<Row>();
153         loadRowsWithFields(rows, defaultRows, BASIC_FIELD_NAMES, documentType);
154         return rows;
155     }
156 
157     /**
158      * Generates the document search form fields given the DataDictionary-defined fields, the DocumentType,
159      * and whether basic, detailed, or superuser search is being rendered.
160      * If the document type policy DOCUMENT_STATUS_POLICY is set to "app", or "both"
161      * Then display the doc search criteria fields.
162      * If the documentType.validApplicationStatuses are defined, then the criteria field is a drop down.
163      * If the validApplication statuses are NOT defined, then the criteria field is a text input.
164      * @param rowsToLoad the list of rows to update
165      * @param defaultRows the DataDictionary-derived default form rows
166      * @param fieldNames a list of field names corresponding to the fields to render according to the current document search state
167      * @param documentType the document type, if specified in the search form
168      */
169     protected void loadRowsWithFields(List<Row> rowsToLoad, List<Row> defaultRows, String[] fieldNames,
170             DocumentType documentType) {
171 
172         for (String fieldName : fieldNames) {
173             if (DOCUMENTTYPE_DEPENDENT_FIELDS.contains(fieldName) && documentType == null) {
174                 continue;
175             }
176             // assuming DOCSTATUS_DEPENDENT_FIELDS are also documentType-dependent, this block is only executed when documentType is present
177             if (DOCSTATUS_DEPENDENT_FIELDS.contains(fieldName) && !documentType.isAppDocStatusInUse()) {
178                 continue;
179             }
180             if (fieldName.equals(DOCUMENT_ATTRIBUTE_FIELD_MARKER)) {
181                 rowsToLoad.addAll(getDocumentAttributeRows(documentType));
182                 continue;
183             }
184             // now add all matching rows given
185             // 1) the field is doc type and doc status independent
186             // 2) the field is doc type dependent and the doctype is specified
187             // 3) the field is doc status dependent and the doctype is specified and doc status is in use
188             for (Row row : defaultRows) {
189                 boolean matched = false;
190                 // we must iterate over each field without short-circuiting to make sure to inspect the
191                 // APPLICATION_DOCUMENT_STATUS field, which needs customizations
192                 for (Field field : row.getFields()) {
193                     // dp "endsWith" here because lower bounds properties come
194                     // across like "rangeLowerBoundKeyPrefix_dateCreated"
195                     if (field.getPropertyName().equals(fieldName) || field.getPropertyName().endsWith("_" + fieldName)) {
196                         matched = true;
197                         if (APPLICATION_DOCUMENT_STATUS.equals(field.getPropertyName())) {
198                             // If Application Document Status policy is in effect for this document type,
199                             // add search attributes for document status, and transition dates.
200                             // Note: document status field is a multiselect if valid statuses are defined, a text input field otherwise.
201                             applyApplicationDocumentStatusCustomizations(field, documentType);
202                             break;
203                         } else if (ROUTE_NODE_NAME.equals(field.getPropertyName())) {
204                             // populates routenodename dropdown with documenttype nodes
205                             applyRouteNodeNameCustomizations(field, documentType);
206                         }
207                     }
208                 }
209                 if (matched) {
210                     rowsToLoad.add(row);
211                 }
212             }
213         }
214     }
215 
216     /**
217      * Returns fields for the search attributes defined on the document
218      */
219     protected List<Row> getDocumentAttributeRows(DocumentType documentType) {
220         List<Row> documentAttributeRows = new ArrayList<Row>();
221         DocumentSearchCriteriaConfiguration configuration =
222                 KEWServiceLocator.getDocumentSearchCustomizationMediator().
223                         getDocumentSearchCriteriaConfiguration(documentType);
224         if (configuration != null) {
225             List<RemotableAttributeField> remotableAttributeFields = configuration.getFlattenedSearchAttributeFields();
226             if (remotableAttributeFields != null && !remotableAttributeFields.isEmpty()) {
227                 documentAttributeRows.addAll(FieldUtils.convertRemotableAttributeFields(remotableAttributeFields));
228             }
229         }
230         List<Row> fixedDocumentAttributeRows = new ArrayList<Row>();
231         for (Row row : documentAttributeRows) {
232             List<Field> fields = row.getFields();
233             for (Field field : fields) {
234                 //force the max length for now if not set
235                 if(field.getMaxLength() == 0) {
236                     field.setMaxLength(100);
237                 }
238                 // prepend all document attribute field names with "documentAttribute."
239                 field.setPropertyName(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + field.getPropertyName());
240                 if (StringUtils.isNotBlank(field.getLookupParameters())) {
241                     field.setLookupParameters(prefixLookupParameters(field.getLookupParameters()));
242                 }
243                 if (StringUtils.isNotBlank(field.getFieldConversions())) {
244                     field.setFieldConversions(prefixFieldConversions(field.getFieldConversions()));
245                 }
246             }
247             fixedDocumentAttributeRows.add(row);
248         }
249 
250         return fixedDocumentAttributeRows;
251     }
252 
253     /**
254      * Modifies the DataDictionary-defined applicationDocumentStatus field control to reflect whether the DocumentType
255      * has specified a list of valid application document statuses (in which case a select control is rendered), or whether
256      * it is free form (in which case a text control is rendered)
257      *
258      * @param field the applicationDocumentStatus field
259      * @param documentType the document type
260      */
261     protected void applyApplicationDocumentStatusCustomizations(Field field, DocumentType documentType) {
262 
263         if (documentType.getValidApplicationStatuses() == null || documentType.getValidApplicationStatuses().size() == 0){
264             // use a text input field
265             // StandardSearchCriteriaField(String fieldKey, String propertyName, String fieldType, String datePickerKey, String labelMessageKey, String helpMessageKeyArgument, boolean hidden, String displayOnlyPropertyName, String lookupableImplServiceName, boolean lookupTypeRequired)
266             // new StandardSearchCriteriaField(DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS,"criteria.appDocStatus",StandardSearchCriteriaField.TEXT,null,null,"DocSearchApplicationDocStatus",false,null,null,false));
267             // String fieldKey DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS
268             field.setFieldType(Field.TEXT);
269         } else {
270             // multiselect
271             // String fieldKey DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS + "_VALUES"
272             field.setFieldType(Field.MULTISELECT);
273             List<KeyValue> validValues = new ArrayList<KeyValue>();
274 
275             // add to set for quick membership check and removal.  LinkedHashSet to preserve order
276             Set<String> statusesToDisplay = new LinkedHashSet<String>();
277             for (ApplicationDocumentStatus status: documentType.getValidApplicationStatuses()) {
278                 statusesToDisplay.add(status.getStatusName());
279             }
280 
281             // KULRICE-7786: support for groups (categories) of application document statuses
282 
283             LinkedHashMap<String, List<String>> appDocStatusCategories =
284                     ApplicationDocumentStatusUtils.getApplicationDocumentStatusCategories(documentType.getName());
285 
286             if (!appDocStatusCategories.isEmpty()) {
287                 for (Map.Entry<String,List<String>> group : appDocStatusCategories.entrySet()) {
288                     boolean addedCategoryHeading = false; // only add category if it has valid members
289                     for (String member : group.getValue()) {
290                         if (statusesToDisplay.remove(member)) { // remove them from the set as we display them
291                             if (!addedCategoryHeading) {
292                                 addedCategoryHeading = true;
293                                 validValues.add(new ConcreteKeyValue("category:" + group.getKey(), group.getKey()));
294                             }
295                             validValues.add(new ConcreteKeyValue(member, "- " + member));
296                         }
297                     }
298                 }
299             }
300 
301             // add remaining statuses, if any.
302             for (String member : statusesToDisplay) {
303                 validValues.add(new ConcreteKeyValue(member, member));
304             }
305 
306             field.setFieldValidValues(validValues);
307 
308             // size the multiselect as appropriate
309             if (validValues.size() > 5) {
310                 field.setSize(5);
311             } else {
312                 field.setSize(validValues.size());
313             }
314 
315             //dropDown.setOptionsCollectionProperty("validApplicationStatuses");
316             //dropDown.setCollectionKeyProperty("statusName");
317             //dropDown.setCollectionLabelProperty("statusName");
318             //dropDown.setEmptyCollectionMessage("Select a document status.");
319         }
320     }
321 
322     protected void applyRouteNodeNameCustomizations(Field field, DocumentType documentType) {
323         List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(documentType, true);
324         List<KeyValue> values = new ArrayList<KeyValue>(nodes.size());
325         for (RouteNode node: nodes) {
326             values.add(new ConcreteKeyValue(node.getName(), node.getName()));
327         }
328         field.setFieldValidValues(values);
329     }
330 
331     protected void addHiddenFields(List<Row> rows, boolean advancedSearch, boolean superUserSearch) {
332         Row hiddenRow = new Row();
333         hiddenRow.setHidden(true);
334 
335         Field detailedField = new Field();
336         detailedField.setPropertyName(KRADConstants.ADVANCED_SEARCH_FIELD);
337         detailedField.setPropertyValue(advancedSearch ? "YES" : "NO");
338         detailedField.setFieldType(Field.HIDDEN);
339 
340         Field superUserSearchField = new Field();
341         superUserSearchField.setPropertyName(SUPERUSER_SEARCH_FIELD);
342         superUserSearchField.setPropertyValue(superUserSearch ? "YES" : "NO");
343         superUserSearchField.setFieldType(Field.HIDDEN);
344 
345         Field clearSavedSearchField = new Field();
346         clearSavedSearchField .setPropertyName(CLEARSAVED_SEARCH_FIELD);
347         clearSavedSearchField .setPropertyValue(superUserSearch ? "YES" : "NO");
348         clearSavedSearchField .setFieldType(Field.HIDDEN);
349 
350         List<Field> hiddenFields = new ArrayList<Field>();
351         hiddenFields.add(detailedField);
352         hiddenFields.add(superUserSearchField);
353         hiddenFields.add(clearSavedSearchField);
354         hiddenRow.setFields(hiddenFields);
355         rows.add(hiddenRow);
356 
357     }
358     
359     private String prefixLookupParameters(String lookupParameters) {
360         StringBuilder newLookupParameters = new StringBuilder(KRADConstants.EMPTY_STRING);
361         String[] conversions = StringUtils.split(lookupParameters, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
362 
363         for (int m = 0; m < conversions.length; m++) {
364             String conversion = conversions[m];
365             String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
366             String conversionFrom = conversionPair[0];
367             String conversionTo = conversionPair[1];
368             conversionFrom = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + conversionFrom;
369             newLookupParameters.append(conversionFrom)
370                     .append(KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR)
371                     .append(conversionTo);
372 
373             if (m < conversions.length) {
374                 newLookupParameters.append(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
375             }
376         }
377         return newLookupParameters.toString();
378     }
379     
380     private String prefixFieldConversions(String fieldConversions) {
381         StringBuilder newFieldConversions = new StringBuilder(KRADConstants.EMPTY_STRING);
382         String[] conversions = StringUtils.split(fieldConversions, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
383 
384         for (int l = 0; l < conversions.length; l++) {
385             String conversion = conversions[l];
386             //String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
387             String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
388             String conversionFrom = conversionPair[0];
389             String conversionTo = conversionPair[1];
390             conversionTo = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + conversionTo;
391             newFieldConversions.append(conversionFrom)
392                     .append(KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR)
393                     .append(conversionTo);
394 
395             if (l < conversions.length) {
396                 newFieldConversions.append(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
397             }
398         }
399 
400         return newFieldConversions.toString();
401     }
402 
403 }