View Javadoc
1   /**
2    * Copyright 2005-2015 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      * Fields that are only applicable if document contains route nodes
133      */
134     private static final Collection<String> ROUTE_NODE_DEPENDENT_FIELDS = Arrays.asList(new String[] { ROUTE_NODE_NAME, ROUTE_NODE_LOGIC });
135 
136 
137     @Override
138     public List<Row> getRows(DocumentType documentType, List<Row> defaultRows, boolean advancedSearch, boolean superUserSearch) {
139         List<Row> rows = null;
140         if(advancedSearch) {
141             rows = loadRowsForAdvancedSearch(defaultRows, documentType);
142         } else {
143             rows = loadRowsForBasicSearch(defaultRows, documentType);
144         }
145         addHiddenFields(rows, advancedSearch, superUserSearch);
146         return rows;
147     }
148 
149     protected List<Row> loadRowsForAdvancedSearch(List<Row> defaultRows, DocumentType documentType) {
150         List<Row> rows = new ArrayList<Row>();
151         loadRowsWithFields(rows, defaultRows, ADVANCED_FIELD_NAMES, documentType);
152         return rows;
153     }
154 
155     protected List<Row> loadRowsForBasicSearch(List<Row> defaultRows, DocumentType documentType) {
156         List<Row> rows = new ArrayList<Row>();
157         loadRowsWithFields(rows, defaultRows, BASIC_FIELD_NAMES, documentType);
158         return rows;
159     }
160 
161     /**
162      * Generates the document search form fields given the DataDictionary-defined fields, the DocumentType,
163      * and whether basic, detailed, or superuser search is being rendered.
164      * If the document type policy DOCUMENT_STATUS_POLICY is set to "app", or "both"
165      * Then display the doc search criteria fields.
166      * If the documentType.validApplicationStatuses are defined, then the criteria field is a drop down.
167      * If the validApplication statuses are NOT defined, then the criteria field is a text input.
168      * @param rowsToLoad the list of rows to update
169      * @param defaultRows the DataDictionary-derived default form rows
170      * @param fieldNames a list of field names corresponding to the fields to render according to the current document search state
171      * @param documentType the document type, if specified in the search form
172      */
173     protected void loadRowsWithFields(List<Row> rowsToLoad, List<Row> defaultRows, String[] fieldNames,
174             DocumentType documentType) {
175 
176         for (String fieldName : fieldNames) {
177             if (DOCUMENTTYPE_DEPENDENT_FIELDS.contains(fieldName) && documentType == null) {
178                 continue;
179             }
180             // assuming DOCSTATUS_DEPENDENT_FIELDS are also documentType-dependent, this block is only executed when documentType is present
181             if (DOCSTATUS_DEPENDENT_FIELDS.contains(fieldName) && !documentType.isAppDocStatusInUse()) {
182                 continue;
183             }
184 
185             // assuming ROUTE_NODE_DEPENDENT_FIELDS are also documentType-dependent, this block is only executed when documentType is present
186             if (ROUTE_NODE_DEPENDENT_FIELDS.contains(fieldName) &&
187                     getRouteNodesByDocumentType(documentType, false).size() == 0) {
188                 continue;
189             }
190 
191             if (fieldName.equals(DOCUMENT_ATTRIBUTE_FIELD_MARKER)) {
192                 rowsToLoad.addAll(getDocumentAttributeRows(documentType));
193                 continue;
194             }
195             // now add all matching rows given
196             // 1) the field is doc type and doc status independent
197             // 2) the field is doc type dependent and the doctype is specified
198             // 3) the field is doc status dependent and the doctype is specified and doc status is in use
199             for (Row row : defaultRows) {
200                 boolean matched = false;
201                 // we must iterate over each field without short-circuiting to make sure to inspect the
202                 // APPLICATION_DOCUMENT_STATUS field, which needs customizations
203                 for (Field field : row.getFields()) {
204                     // dp "endsWith" here because lower bounds properties come
205                     // across like "rangeLowerBoundKeyPrefix_dateCreated"
206                     if (field.getPropertyName().equals(fieldName) || field.getPropertyName().endsWith("_" + fieldName)) {
207                         matched = true;
208                         if (APPLICATION_DOCUMENT_STATUS.equals(field.getPropertyName())) {
209                             // If Application Document Status policy is in effect for this document type,
210                             // add search attributes for document status, and transition dates.
211                             // Note: document status field is a multiselect if valid statuses are defined, a text input field otherwise.
212                             applyApplicationDocumentStatusCustomizations(field, documentType);
213                             break;
214                         } else if (ROUTE_NODE_NAME.equals(field.getPropertyName())) {
215                             // populates routenodename dropdown with documenttype nodes
216                             applyRouteNodeNameCustomizations(field, documentType);
217                         }
218                     }
219                 }
220                 if (matched) {
221                     rowsToLoad.add(row);
222                 }
223             }
224         }
225     }
226 
227     /**
228      * Returns fields for the search attributes defined on the document
229      */
230     protected List<Row> getDocumentAttributeRows(DocumentType documentType) {
231         List<Row> documentAttributeRows = new ArrayList<Row>();
232         DocumentSearchCriteriaConfiguration configuration =
233                 KEWServiceLocator.getDocumentSearchCustomizationMediator().
234                         getDocumentSearchCriteriaConfiguration(documentType);
235         if (configuration != null) {
236             List<RemotableAttributeField> remotableAttributeFields = configuration.getFlattenedSearchAttributeFields();
237             if (remotableAttributeFields != null && !remotableAttributeFields.isEmpty()) {
238                 documentAttributeRows.addAll(FieldUtils.convertRemotableAttributeFields(remotableAttributeFields));
239             }
240         }
241         List<Row> fixedDocumentAttributeRows = new ArrayList<Row>();
242         for (Row row : documentAttributeRows) {
243             List<Field> fields = row.getFields();
244             for (Field field : fields) {
245                 //force the max length for now if not set
246                 if(field.getMaxLength() == 0) {
247                     field.setMaxLength(100);
248                 }
249                 // prepend all document attribute field names with "documentAttribute."
250                 field.setPropertyName(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + field.getPropertyName());
251                 if (StringUtils.isNotBlank(field.getLookupParameters())) {
252                     field.setLookupParameters(prefixLookupParameters(field.getLookupParameters()));
253                 }
254                 if (StringUtils.isNotBlank(field.getFieldConversions())) {
255                     field.setFieldConversions(prefixFieldConversions(field.getFieldConversions()));
256                 }
257             }
258             fixedDocumentAttributeRows.add(row);
259         }
260 
261         return fixedDocumentAttributeRows;
262     }
263 
264     /**
265      * Modifies the DataDictionary-defined applicationDocumentStatus field control to reflect whether the DocumentType
266      * has specified a list of valid application document statuses (in which case a select control is rendered), or whether
267      * it is free form (in which case a text control is rendered)
268      *
269      * @param field the applicationDocumentStatus field
270      * @param documentType the document type
271      */
272     protected void applyApplicationDocumentStatusCustomizations(Field field, DocumentType documentType) {
273 
274         if (documentType.getValidApplicationStatuses() == null || documentType.getValidApplicationStatuses().size() == 0){
275             // use a text input field
276             // StandardSearchCriteriaField(String fieldKey, String propertyName, String fieldType, String datePickerKey, String labelMessageKey, String helpMessageKeyArgument, boolean hidden, String displayOnlyPropertyName, String lookupableImplServiceName, boolean lookupTypeRequired)
277             // new StandardSearchCriteriaField(DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS,"criteria.appDocStatus",StandardSearchCriteriaField.TEXT,null,null,"DocSearchApplicationDocStatus",false,null,null,false));
278             // String fieldKey DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS
279             field.setFieldType(Field.TEXT);
280         } else {
281             // multiselect
282             // String fieldKey DocumentSearchCriteriaProcessor.CRITERIA_KEY_APP_DOC_STATUS + "_VALUES"
283             field.setFieldType(Field.MULTISELECT);
284             List<KeyValue> validValues = new ArrayList<KeyValue>();
285 
286             // add to set for quick membership check and removal.  LinkedHashSet to preserve order
287             Set<String> statusesToDisplay = new LinkedHashSet<String>();
288             for (ApplicationDocumentStatus status: documentType.getValidApplicationStatuses()) {
289                 statusesToDisplay.add(status.getStatusName());
290             }
291 
292             // KULRICE-7786: support for groups (categories) of application document statuses
293 
294             LinkedHashMap<String, List<String>> appDocStatusCategories =
295                     ApplicationDocumentStatusUtils.getApplicationDocumentStatusCategories(documentType.getName());
296 
297             if (!appDocStatusCategories.isEmpty()) {
298                 for (Map.Entry<String,List<String>> group : appDocStatusCategories.entrySet()) {
299                     boolean addedCategoryHeading = false; // only add category if it has valid members
300                     for (String member : group.getValue()) {
301                         if (statusesToDisplay.remove(member)) { // remove them from the set as we display them
302                             if (!addedCategoryHeading) {
303                                 addedCategoryHeading = true;
304                                 validValues.add(new ConcreteKeyValue("category:" + group.getKey(), group.getKey()));
305                             }
306                             validValues.add(new ConcreteKeyValue(member, "- " + member));
307                         }
308                     }
309                 }
310             }
311 
312             // add remaining statuses, if any.
313             for (String member : statusesToDisplay) {
314                 validValues.add(new ConcreteKeyValue(member, member));
315             }
316 
317             field.setFieldValidValues(validValues);
318 
319             // size the multiselect as appropriate
320             if (validValues.size() > 5) {
321                 field.setSize(5);
322             } else {
323                 field.setSize(validValues.size());
324             }
325 
326             //dropDown.setOptionsCollectionProperty("validApplicationStatuses");
327             //dropDown.setCollectionKeyProperty("statusName");
328             //dropDown.setCollectionLabelProperty("statusName");
329             //dropDown.setEmptyCollectionMessage("Select a document status.");
330         }
331     }
332 
333     /**
334      * Return route nodes based on the document
335      *
336      * @param documentType
337      * @return List
338      */
339     protected List<RouteNode> getRouteNodesByDocumentType(DocumentType documentType, boolean includeBlankNodes) {
340         List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(documentType, true);
341 
342         // Blank check can be removed if no longer included in RouteNodeService getFlattenedNodes
343         if(nodes.size() > 0 && !includeBlankNodes) {
344             List<RouteNode> namedNodes = new ArrayList<RouteNode>();
345             for (RouteNode node : nodes) {
346                 if (StringUtils.isNotBlank(node.getName())) {
347                     namedNodes.add(node);
348                 }
349             }
350             return namedNodes;
351         }
352 
353         return nodes;
354     }
355 
356 
357     protected void applyRouteNodeNameCustomizations(Field field, DocumentType documentType) {
358         List<RouteNode> nodes = getRouteNodesByDocumentType(documentType, false);
359         List<KeyValue> values = new ArrayList<KeyValue>(nodes.size());
360         for (RouteNode node: nodes) {
361             values.add(new ConcreteKeyValue(node.getName(), node.getName()));
362         }
363 
364         field.setFieldValidValues(values);
365     }
366 
367     protected void addHiddenFields(List<Row> rows, boolean advancedSearch, boolean superUserSearch) {
368         Row hiddenRow = new Row();
369         hiddenRow.setHidden(true);
370 
371         Field detailedField = new Field();
372         detailedField.setPropertyName(KRADConstants.ADVANCED_SEARCH_FIELD);
373         detailedField.setPropertyValue(advancedSearch ? "YES" : "NO");
374         detailedField.setFieldType(Field.HIDDEN);
375 
376         Field superUserSearchField = new Field();
377         superUserSearchField.setPropertyName(SUPERUSER_SEARCH_FIELD);
378         superUserSearchField.setPropertyValue(superUserSearch ? "YES" : "NO");
379         superUserSearchField.setFieldType(Field.HIDDEN);
380 
381         Field clearSavedSearchField = new Field();
382         clearSavedSearchField .setPropertyName(CLEARSAVED_SEARCH_FIELD);
383         clearSavedSearchField .setPropertyValue(superUserSearch ? "YES" : "NO");
384         clearSavedSearchField .setFieldType(Field.HIDDEN);
385 
386         List<Field> hiddenFields = new ArrayList<Field>();
387         hiddenFields.add(detailedField);
388         hiddenFields.add(superUserSearchField);
389         hiddenFields.add(clearSavedSearchField);
390         hiddenRow.setFields(hiddenFields);
391         rows.add(hiddenRow);
392 
393     }
394     
395     private String prefixLookupParameters(String lookupParameters) {
396         StringBuilder newLookupParameters = new StringBuilder(KRADConstants.EMPTY_STRING);
397         String[] conversions = StringUtils.split(lookupParameters, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
398 
399         for (int m = 0; m < conversions.length; m++) {
400             String conversion = conversions[m];
401             String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
402             String conversionFrom = conversionPair[0];
403             String conversionTo = conversionPair[1];
404             conversionFrom = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + conversionFrom;
405             newLookupParameters.append(conversionFrom)
406                     .append(KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR)
407                     .append(conversionTo);
408 
409             if (m < conversions.length) {
410                 newLookupParameters.append(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
411             }
412         }
413         return newLookupParameters.toString();
414     }
415     
416     private String prefixFieldConversions(String fieldConversions) {
417         StringBuilder newFieldConversions = new StringBuilder(KRADConstants.EMPTY_STRING);
418         String[] conversions = StringUtils.split(fieldConversions, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
419 
420         for (int l = 0; l < conversions.length; l++) {
421             String conversion = conversions[l];
422             //String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
423             String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
424             String conversionFrom = conversionPair[0];
425             String conversionTo = conversionPair[1];
426             conversionTo = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + conversionTo;
427             newFieldConversions.append(conversionFrom)
428                     .append(KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR)
429                     .append(conversionTo);
430 
431             if (l < conversions.length) {
432                 newFieldConversions.append(KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
433             }
434         }
435 
436         return newFieldConversions.toString();
437     }
438 
439 }