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}