001 /**
002 * Copyright 2005-2013 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 */
016 package org.kuali.rice.kew.docsearch;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.kuali.rice.core.api.uif.RemotableAttributeField;
020 import org.kuali.rice.core.api.util.ConcreteKeyValue;
021 import org.kuali.rice.core.api.util.KeyValue;
022 import org.kuali.rice.kew.api.KewApiConstants;
023 import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
024 import org.kuali.rice.kew.doctype.bo.DocumentType;
025 import org.kuali.rice.kew.engine.node.RouteNode;
026 import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
027 import org.kuali.rice.kew.impl.document.ApplicationDocumentStatusUtils;
028 import org.kuali.rice.kew.service.KEWServiceLocator;
029 import org.kuali.rice.kns.util.FieldUtils;
030 import org.kuali.rice.kns.web.ui.Field;
031 import org.kuali.rice.kns.web.ui.Row;
032 import org.kuali.rice.krad.util.KRADConstants;
033
034 import java.util.ArrayList;
035 import java.util.Arrays;
036 import java.util.Collection;
037 import java.util.LinkedHashMap;
038 import java.util.LinkedHashSet;
039 import java.util.List;
040 import java.util.Map;
041 import 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 */
069 public 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 }