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     * 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}