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