001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.docsearch;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.uif.RemotableAttributeField;
020import org.kuali.rice.core.api.util.ConcreteKeyValue;
021import org.kuali.rice.core.api.util.KeyValue;
022import org.kuali.rice.kew.api.KewApiConstants;
023import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
024import org.kuali.rice.kew.doctype.bo.DocumentType;
025import org.kuali.rice.kew.engine.node.RouteNode;
026import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
027import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils;
028import org.kuali.rice.kew.service.KEWServiceLocator;
029import org.kuali.rice.kns.util.FieldUtils;
030import org.kuali.rice.kns.web.ui.Field;
031import org.kuali.rice.kns.web.ui.Row;
032import org.kuali.rice.krad.util.KRADConstants;
033
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.LinkedHashMap;
038import java.util.LinkedHashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043/**
044 * This class adapts the RemotableAttributeField instances from the various attributes
045 * associated with a document type and combines with the "default" rows for the search,
046 * returning the final List of Row objects to render for the document search.
047 *
048 * <p>Implementation note:</p>
049 * <p>
050 * This implementation relies on applicationDocumentStatus, and dateApplicationDocumentStatusChanged conditional fields
051 * being defined in the DD for basic display purposes.  These fields are conditionally shown depending on whether
052 * a document supporting application document statuses has been specified.  Further, the applicationDocumentStatus field
053 * is dynamically switched to a multiselect when the document specifies an explicit enumeration of valid statuses (this
054 * control switching is something that is not possible via declarative DD, at the time of this writing).
055 * </p>
056 * <p>
057 * In addition the routeNodeName field is dynamically populated with the list of route nodes for the specified document
058 * type.
059 * <p>
060 * Note: an alternative to programmatically providing dynamic select values is to define a value finder declaratively in
061 * DD.  KeyValueFinder however does not have access to request state, including the required document type, which would mean
062 * resorting to GlobalVariables inspection.  In reluctance to add yet another dependency on this external state, the fixups
063 * are done programmatically in this class. (see {@link #applyApplicationDocumentStatusCustomizations(org.kuali.rice.kns.web.ui.Field, org.kuali.rice.kew.doctype.bo.DocumentType)},
064 * {@link #applyRouteNodeNameCustomizations(org.kuali.rice.kns.web.ui.Field, org.kuali.rice.kew.doctype.bo.DocumentType)}).
065 * </p>
066 * @author Kuali Rice Team (rice.collab@kuali.org)
067 *
068 */
069public class DocumentSearchCriteriaProcessorKEWAdapter implements DocumentSearchCriteriaProcessor {
070    /**
071     * Name if the hidden input field containing non-superuser/superuser search toggle state
072     */
073    public static final String SUPERUSER_SEARCH_FIELD = "superUserSearch";
074    /**
075     * Name if the hidden input field containing the clear saved search flag
076     */
077    public static final String CLEARSAVED_SEARCH_FIELD = "resetSavedSearch";
078
079    /**
080     * Indicates where document attributes should be placed inside search criteria
081     */
082    private static final String DOCUMENT_ATTRIBUTE_FIELD_MARKER = "DOCUMENT_ATTRIBUTE_FIELD_MARKER";
083
084    private static final String APPLICATION_DOCUMENT_STATUS = "applicationDocumentStatus";
085    private static final String DATE_APP_DOC_STATUS_CHANGED_FROM = "rangeLowerBoundKeyPrefix_dateApplicationDocumentStatusChanged";
086    private static final String DATE_APP_DOC_STATUS_CHANGED = "dateApplicationDocumentStatusChanged";
087    private static final String ROUTE_NODE_NAME = "routeNodeName";
088    private static final String ROUTE_NODE_LOGIC = "routeNodeLogic";
089
090    private static final String[] BASIC_FIELD_NAMES = {
091            "documentTypeName",
092            "initiatorPrincipalName",
093            "documentId",
094            APPLICATION_DOCUMENT_STATUS,
095            "dateCreated",
096            DOCUMENT_ATTRIBUTE_FIELD_MARKER,
097            "saveName"
098    };
099
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}