View Javadoc

1   /**
2    * Copyright 2005-2011 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.impl.document.search;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.ArrayUtils;
20  import org.apache.commons.lang.BooleanUtils;
21  import org.apache.commons.lang.ObjectUtils;
22  import org.apache.commons.lang.StringUtils;
23  import org.kuali.rice.core.api.CoreApiServiceLocator;
24  import org.kuali.rice.core.api.config.property.Config;
25  import org.kuali.rice.core.api.config.property.ConfigContext;
26  import org.kuali.rice.core.api.search.SearchOperator;
27  import org.kuali.rice.core.api.uif.RemotableAttributeField;
28  import org.kuali.rice.core.api.util.KeyValue;
29  import org.kuali.rice.core.api.util.RiceKeyConstants;
30  import org.kuali.rice.core.framework.services.CoreFrameworkServiceLocator;
31  import org.kuali.rice.core.web.format.Formatter;
32  import org.kuali.rice.kew.api.KEWPropertyConstants;
33  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
34  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
35  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
36  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
37  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
38  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor;
39  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter;
40  import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
41  import org.kuali.rice.kew.doctype.bo.DocumentType;
42  import org.kuali.rice.kew.exception.WorkflowServiceError;
43  import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
44  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
45  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration;
46  import org.kuali.rice.kew.framework.document.search.StandardResultField;
47  import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaBo;
48  import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder;
49  import org.kuali.rice.kew.service.KEWServiceLocator;
50  import org.kuali.rice.kew.api.KewApiConstants;
51  import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
52  import org.kuali.rice.kns.lookup.HtmlData;
53  import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl;
54  import org.kuali.rice.kns.util.FieldUtils;
55  import org.kuali.rice.kns.web.struts.form.LookupForm;
56  import org.kuali.rice.kns.web.ui.Column;
57  import org.kuali.rice.kns.web.ui.Field;
58  import org.kuali.rice.kns.web.ui.ResultRow;
59  import org.kuali.rice.kns.web.ui.Row;
60  import org.kuali.rice.krad.bo.BusinessObject;
61  import org.kuali.rice.krad.exception.ValidationException;
62  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
63  import org.kuali.rice.krad.util.GlobalVariables;
64  import org.kuali.rice.krad.util.KRADConstants;
65  
66  import java.lang.reflect.InvocationTargetException;
67  import java.text.MessageFormat;
68  import java.util.ArrayList;
69  import java.util.Collection;
70  import java.util.Collections;
71  import java.util.HashMap;
72  import java.util.List;
73  import java.util.Map;
74  
75  /**
76   * Implementation of lookupable helper service which handles the complex lookup behavior required by the KEW
77   * document search screen.
78   *
79   * @author Kuali Rice Team (rice.collab@kuali.org)
80   */
81  public class DocumentSearchCriteriaBoLookupableHelperService extends KualiLookupableHelperServiceImpl {
82  
83      private static final String DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX = "documentAttribute.";
84  
85      static final String SAVED_SEARCH_NAME_PARAM = "savedSearchToLoadAndExecute";
86  
87      // warning message keys
88  
89      private static final String EXCEED_THRESHOLD_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThreshold";
90      private static final String SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.securityFiltered";
91      private static final String EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThresholdAndSecurityFiltered";
92  
93      private static final boolean DOCUMENT_HANDLER_POPUP_DEFAULT = true;
94      private static final boolean ROUTE_LOG_POPUP_DEFAULT = true;
95  
96      // injected services
97  
98      private DocumentSearchService documentSearchService;
99      private DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor;
100     private DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator;
101 
102     // unfortunately, lookup helpers are stateful, need to store these here for other methods to use
103     protected DocumentSearchResults searchResults = null;
104     protected DocumentSearchCriteria criteria = null;
105 
106     @Override
107     protected List<? extends BusinessObject> getSearchResultsHelper(Map<String, String> fieldValues,
108             boolean unbounded) {
109         criteria = loadCriteria(fieldValues);
110         searchResults = null;
111         try {
112             searchResults = KEWServiceLocator.getDocumentSearchService().lookupDocuments(
113                     GlobalVariables.getUserSession().getPrincipalId(), criteria);
114             if (searchResults.isCriteriaModified()) {
115                 criteria = searchResults.getCriteria();
116             }
117         } catch (WorkflowServiceErrorException wsee) {
118             for (WorkflowServiceError workflowServiceError : (List<WorkflowServiceError>) wsee.getServiceErrors()) {
119                 if (workflowServiceError.getMessageMap() != null && workflowServiceError.getMessageMap().hasErrors()) {
120                     // merge the message maps
121                     GlobalVariables.getMessageMap().merge(workflowServiceError.getMessageMap());
122                 } else {
123                     GlobalVariables.getMessageMap().putError(workflowServiceError.getMessage(),
124                             RiceKeyConstants.ERROR_CUSTOM, workflowServiceError.getMessage());
125                 }
126             }
127         }
128 
129         if (!GlobalVariables.getMessageMap().hasNoErrors() || searchResults == null) {
130             throw new ValidationException("error with doc search");
131         }
132 
133         populateResultWarningMessages(searchResults);
134 
135         List<DocumentSearchResult> individualSearchResults = searchResults.getSearchResults();
136 
137         setBackLocation(fieldValues.get(KRADConstants.BACK_LOCATION));
138         setDocFormKey(fieldValues.get(KRADConstants.DOC_FORM_KEY));
139 
140         applyCriteriaChangesToFields(criteria);
141 
142         return populateSearchResults(individualSearchResults);
143 
144     }
145 
146     /**
147      * Inspects the lookup results to determine if any warning messages should be published to the message map.
148      */
149     protected void populateResultWarningMessages(DocumentSearchResults searchResults) {
150         // check various warning conditions
151         boolean overThreshold = searchResults.isOverThreshold();
152         int numFiltered = searchResults.getNumberOfSecurityFilteredResults();
153         int numResults = searchResults.getSearchResults().size();
154         if (overThreshold && numFiltered > 0) {
155             GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
156                     EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY,
157                     String.valueOf(numResults),
158                     String.valueOf(numFiltered));
159         } else if (numFiltered > 0) {
160             GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
161                     SECURITY_FILTERED_MESSAGE_KEY,
162                     String.valueOf(numFiltered));
163         } else if (overThreshold) {
164             GlobalVariables.getMessageMap().putWarning(KRADConstants.GLOBAL_MESSAGES,
165                     EXCEED_THRESHOLD_MESSAGE_KEY,
166                     String.valueOf(numResults));
167         }
168     }
169 
170     /**
171      * Applies changes that might have happened to the criteria back to the fields so that they show up on the form.
172      * Namely, this handles populating the form with today's date if the create date was not filled in on the form.
173      */
174     protected void applyCriteriaChangesToFields(DocumentSearchCriteriaContract criteria) {
175         for (Field field: getFormFields()) {
176             if(StringUtils.equals(field.getPropertyName(), KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + "dateCreated") && StringUtils.isEmpty(field.getPropertyValue())) {
177                 if (criteria.getDateCreatedFrom() != null) {
178                     field.setPropertyValue(CoreApiServiceLocator.getDateTimeService().toDateString(criteria.getDateCreatedFrom().toDate()));
179                 }
180             }
181         }
182     }
183 
184     /**
185      * Cleans up various issues with fieldValues coming from the lookup form (namely, that they don't include
186      * multi-valued field values!). Handles these by adding them comma-separated.
187      */
188     protected Map<String, String> cleanupFieldValues(Map<String, String> fieldValues, Map<String, String[]> parameters) {
189         Map<String, String> cleanedUpFieldValues = new HashMap<String, String>(fieldValues);
190         if (ArrayUtils.isNotEmpty(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE))) {
191             cleanedUpFieldValues.put(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE,
192                     StringUtils.join(parameters.get(KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_STATUS_CODE), ","));
193         }
194         Map<String, String> documentAttributeFieldValues = new HashMap<String, String>();
195         for (String parameterName : parameters.keySet()) {
196             if (parameterName.contains(KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX)) {
197                 String[] value = parameters.get(parameterName);
198                 if (ArrayUtils.isNotEmpty(value)) {
199                     documentAttributeFieldValues.put(parameterName, StringUtils.join(value, " " + SearchOperator.OR.op() + " "));
200                 }
201             }
202         }
203         // if any of the document attributes are range values, process them
204         documentAttributeFieldValues.putAll(preProcessRangeFields(documentAttributeFieldValues));
205         cleanedUpFieldValues.putAll(documentAttributeFieldValues);
206         return cleanedUpFieldValues;
207     }
208 
209     /**
210      * Loads the document search criteria from the given map of field values as submitted from the search screen, and
211      * populates the current form Rows/Fields with the saved criteria fields
212      */
213     protected DocumentSearchCriteria loadCriteria(Map<String, String> fieldValues) {
214         fieldValues = cleanupFieldValues(fieldValues, getParameters());
215         String[] savedSearchToLoad = getParameters().get(SAVED_SEARCH_NAME_PARAM);
216         boolean savedSearch = savedSearchToLoad != null && savedSearchToLoad.length > 0 && StringUtils.isNotBlank(savedSearchToLoad[0]);
217         if (savedSearch) {
218             DocumentSearchCriteria criteria = getDocumentSearchService().getSavedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchToLoad[0]);
219             if (criteria != null) {
220                 mergeFieldValues(getDocumentSearchCriteriaTranslator().translateCriteriaToFields(criteria));
221                 return criteria;
222             }
223         }
224         // either it wasn't a saved search or the saved search failed to resolve
225         return getDocumentSearchCriteriaTranslator().translateFieldsToCriteria(fieldValues);
226     }
227 
228     protected List<DocumentSearchCriteriaBo> populateSearchResults(List<DocumentSearchResult> lookupResults) {
229         List<DocumentSearchCriteriaBo> searchResults = new ArrayList<DocumentSearchCriteriaBo>();
230         for (DocumentSearchResult searchResult : lookupResults) {
231             DocumentSearchCriteriaBo result = new DocumentSearchCriteriaBo();
232             result.populateFromDocumentSearchResult(searchResult);
233             searchResults.add(result);
234         }
235         return searchResults;
236     }
237 
238     @Override
239     public Collection performLookup(LookupForm lookupForm, Collection resultTable, boolean bounded) {
240         Collection lookupResult = super.performLookup(lookupForm, resultTable, bounded);
241         postProcessResults(resultTable, this.searchResults);
242         return lookupResult;
243     }
244 
245     /**
246      * Sets a Field value appropriately, depending on whether it is a "multi-value" field type
247      */
248     protected static void setFieldValue(Field field, String[] values) {
249         if(!Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
250             field.setPropertyValue(values[0]);
251         } else {
252             //multi value, set to values
253             field.setPropertyValues(values);
254         }
255     }
256 
257     /**
258      * Preserves Field values, saving single or array property value depending on field type; single property value is
259      * converted into String[1]
260      * This implementation makes the assumption that a Field can either represent a single property value, or an array
261      * of values but not both! (only one is preserved)
262      * @return a Map<String, String[]> containing field values depending on field type
263      */
264     protected Map<String, String[]> getFormFieldsValues() {
265         Map<String, String[]> values = new HashMap<String, String[]>();
266         for (Field field : getFormFields()) {
267             String[] value;
268             if(!Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
269                 value = new String[] { field.getPropertyValue() };
270             } else {
271                 //multi value, set to values
272                 value = field.getPropertyValues();
273             }
274             values.put(field.getPropertyName(), value);
275         }
276         return values;
277     }
278 
279     /**
280      * Overrides a Field value; sets a fallback/restored value if there is no new value
281      */
282     protected static void overrideFieldValue(Field field, Map<String, String[]> newValues, Map<String, String[]> oldValues) {
283         if (StringUtils.isNotBlank(field.getPropertyName())) {
284             if (newValues.get(field.getPropertyName()) != null) {
285                 setFieldValue(field, newValues.get(field.getPropertyName()));
286             } else if (oldValues.get(field.getPropertyName()) != null) {
287                 setFieldValue(field, oldValues.get(field.getPropertyName()));
288             }
289         }
290     }
291 
292     /**
293      * Overrides Row Field values with Map values
294      * @param values
295      */
296     protected void mergeFieldValues(Map<String, String[]> values) {
297         for (Field field: getFormFields()) {
298             if (StringUtils.isNotBlank(field.getPropertyName())) {
299                 if (values.get(field.getPropertyName()) != null) {
300                     setFieldValue(field, values.get(field.getPropertyName()));
301                 }
302             }
303         }
304     }
305 
306     /**
307      * Sets a single form field
308      */
309     protected void setFormField(String name, String value) {
310         for (Field field : getFormFields()) {
311             if(StringUtils.equals(field.getPropertyName(), name)) {
312                 setFieldValue(field, new String[] { value });
313                 break;
314             }
315         }
316     }
317 
318     /**
319      * Handles toggling between form views.
320      * Reads and sets the Rows state.
321      */
322     protected void toggleFormView() {
323         Map<String,String[]> fieldValues = new HashMap<String,String[]>();
324         Map<String, String[]> savedValues = getFormFieldsValues();
325 
326         // the original implementation saved the form values and then re-applied them
327         // we do the same here, however I suspect we may be able to avoid this re-application
328         // of existing values
329 
330         for (Field field: getFormFields()) {
331             overrideFieldValue(field, this.getParameters(), savedValues);
332             // if we are sure this does not depend on or cause side effects in other fields
333             // then this phase can be extracted and these loops simplified
334             applyFieldAuthorizationsFromNestedLookups(field);
335             fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() });
336         }
337 
338         // checkForAdditionalFields generates the form (setRows)
339         if (checkForAdditionalFields(fieldValues)) {
340             for (Field field: getFormFields()) {
341                 overrideFieldValue(field, this.getParameters(), savedValues);
342                 fieldValues.put(field.getPropertyName(), new String[] { field.getPropertyValue() });
343              }
344         }
345 
346         // unset the clear search param, since this is not really a state, but just an action
347         // it can never be toggled "off", just "on"
348         setFormField(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, "");
349     }
350 
351     /**
352      * Loads a saved search
353      * @return returns true on success to run the loaded search, false on error.
354      */
355     protected boolean loadSavedSearch(boolean ignoreErrors) {
356         Map<String,String[]> fieldValues = new HashMap<String,String[]>();
357 
358         String savedSearchName = getSavedSearchName();
359         if(StringUtils.isEmpty(savedSearchName) || "*ignore*".equals(savedSearchName)) {
360             if(!ignoreErrors) {
361                 GlobalVariables.getMessageMap().putError(SAVED_SEARCH_NAME_PARAM, RiceKeyConstants.ERROR_CUSTOM, "You must select a saved search");
362             } else {
363                 //if we're ignoring errors and we got an error just return, no reason to continue.  Also set false to indicate not to perform lookup
364                 return false;
365             }
366             setFormField(SAVED_SEARCH_NAME_PARAM, "");
367         }
368         if (!GlobalVariables.getMessageMap().hasNoErrors()) {
369             throw new ValidationException("errors in search criteria");
370         }
371 
372         DocumentSearchCriteria criteria = KEWServiceLocator.getDocumentSearchService().getSavedSearchCriteria(GlobalVariables.getUserSession().getPrincipalId(), savedSearchName);
373 
374         // get the document type
375         String docTypeName = criteria.getDocumentTypeName();
376         // and set the rows based on doc type
377         setRows(docTypeName);
378 
379         // clear the name of the search in the form
380         //fieldValues.put(SAVED_SEARCH_NAME_PARAM, new String[0]);
381 
382         // set the custom document attribute values on the search form
383         for (Map.Entry<String, List<String>> entry: criteria.getDocumentAttributeValues().entrySet()) {
384             fieldValues.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
385         }
386 
387         // sets the field values on the form, trying criteria object properties if a field value is not present in the map
388         for (Field field : getFormFields()) {
389             if (field.getPropertyName() != null && !field.getPropertyName().equals("")) {
390                 // UI Fields know whether they are single or multiple value
391                 // just set both so they can make the determination and render appropriately
392                 String[] values = null;
393                 if (fieldValues.get(field.getPropertyName()) != null) {
394                     values = fieldValues.get(field.getPropertyName());
395                 } else {
396                     //may be on the root of the criteria object, try looking there:
397                     try {
398                         values = new String[] { ObjectUtils.toString(PropertyUtils.getProperty(criteria, field.getPropertyName())) };
399                     } catch (IllegalAccessException e) {
400                         e.printStackTrace();
401                     } catch (InvocationTargetException e) {
402                         e.printStackTrace();
403                     } catch (NoSuchMethodException e) {
404                         // e.printStackTrace();
405                         //hmm what to do here, we should be able to find everything either in the search atts or at the base as far as I know.
406                     }
407                 }
408                 if (values != null) {
409                     setFieldValue(field, values);
410                 }
411             }
412         }
413 
414         return true;
415     }
416 
417     /**
418      * Performs custom document search/lookup actions.
419      * 1) switching between simple/detailed search
420      * 2) switching between non-superuser/superuser search
421      * 3) clearing saved search results
422      * 4) restoring a saved search and executing the search
423      * @param ignoreErrors
424      * @return whether to rerun the previous search; false in cases 1-3 because we are just updating the form
425      */
426     @Override
427     public boolean performCustomAction(boolean ignoreErrors) {
428         //boolean isConfigAction = isAdvancedSearch() || isSuperUserSearch() || isClearSavedSearch();
429         if (getSavedSearchName() != null) {
430             return loadSavedSearch(ignoreErrors);
431         } else {
432             if (isClearSavedSearch()) {
433                 KEWServiceLocator.getDocumentSearchService().clearNamedSearches(GlobalVariables.getUserSession().getPrincipalId());
434             } else {
435                 toggleFormView();
436             }
437             // Finally, return false to prevent the search from being performed and to skip the other custom processing below.
438             return false;
439         }
440     }
441 
442     /**
443      * Custom implementation of getInquiryUrl that sets up doc handler link.
444      */
445     @Override
446     public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) {
447         DocumentSearchCriteriaBo criteriaBo = (DocumentSearchCriteriaBo)bo;
448         if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOCUMENT_ID.equals(propertyName)) {
449             return generateDocumentHandlerUrl(criteriaBo.getDocumentId(), criteriaBo.getDocumentType(),
450                     isSuperUserSearch());
451         } else if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG.equals(propertyName)) {
452             return generateRouteLogUrl(criteriaBo.getDocumentId());
453         }
454         return super.getInquiryUrl(bo, propertyName);
455     }
456 
457     /**
458      * Generates the appropriate document handler url for the given document.  If superUserSearch is true then a super
459      * user doc handler link will be generated if the document type policy allows it.
460      */
461     protected HtmlData.AnchorHtmlData generateDocumentHandlerUrl(String documentId, DocumentType documentType, boolean superUserSearch) {
462         HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
463         link.setDisplayText(documentId);
464         if (isDocumentHandlerPopup()) {
465             link.setTarget("_blank");
466         }
467         String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/";
468         if (superUserSearch) {
469             if (documentType.getUseWorkflowSuperUserDocHandlerUrl().getPolicyValue().booleanValue()) {
470                 url += "SuperUser.do?methodToCall=displaySuperUserDocument&documentId=" + documentId;
471             } else {
472                 url = KewApiConstants.DOC_HANDLER_REDIRECT_PAGE
473                         + "?" + KewApiConstants.COMMAND_PARAMETER + "="
474                         + KewApiConstants.SUPERUSER_COMMAND + "&"
475                         + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
476                         + documentId;
477             }
478         } else {
479             url += KewApiConstants.DOC_HANDLER_REDIRECT_PAGE + "?"
480                     + KewApiConstants.COMMAND_PARAMETER + "="
481                     + KewApiConstants.DOCSEARCH_COMMAND + "&"
482                     + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
483                     + documentId;
484         }
485         link.setHref(url);
486         return link;
487     }
488 
489     protected HtmlData.AnchorHtmlData generateRouteLogUrl(String documentId) {
490         HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
491         if (isRouteLogPopup()) {
492             link.setTarget("_blank");
493         }
494         link.setDisplayText("Route Log for document " + documentId);
495         String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/" +
496                 "RouteLog.do?documentId=" + documentId;
497         link.setHref(url);
498         return link;
499     }
500 
501     /**
502      * Returns true if the document handler should open in a new window.
503      */
504     protected boolean isDocumentHandlerPopup() {
505         return BooleanUtils.toBooleanDefaultIfNull(
506                 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
507                     KewApiConstants.KEW_NAMESPACE,
508                     KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
509                     KewApiConstants.DOCUMENT_SEARCH_DOCUMENT_POPUP_IND),
510                 DOCUMENT_HANDLER_POPUP_DEFAULT);
511     }
512 
513     /**
514      * Returns true if the route log should open in a new window.
515      */
516     public boolean isRouteLogPopup() {
517         return BooleanUtils.toBooleanDefaultIfNull(
518                 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(KewApiConstants.KEW_NAMESPACE,
519                         KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
520                         KewApiConstants.DOCUMENT_SEARCH_ROUTE_LOG_POPUP_IND), ROUTE_LOG_POPUP_DEFAULT);
521     }
522 
523     /**
524      * Parses a boolean request parameter
525      */
526     protected boolean isFlagSet(String flagName) {
527         if(this.getParameters().containsKey(flagName)) {
528             String[] params = (String[])this.getParameters().get(flagName);
529             if (ArrayUtils.isNotEmpty(params)) {
530                 return "YES".equalsIgnoreCase(params[0]);
531             }
532         }
533         return false;
534     }
535 
536     /**
537      * Returns true if the current search being executed is a super user search.
538      */
539     protected boolean isSuperUserSearch() {
540         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD);
541     }
542 
543     /**
544      * Returns true if the current search being executed is an "advanced" search.
545      */
546     protected boolean isAdvancedSearch() {
547         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD);
548     }
549 
550     /**
551      * Returns true if the current "search" being executed is an "clear" search.
552      */
553     protected boolean isClearSavedSearch() {
554         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD);
555     }
556 
557     protected String getSavedSearchName() {
558         String[] savedSearchName = getParameters().get(SAVED_SEARCH_NAME_PARAM);
559         if (savedSearchName != null && savedSearchName.length > 0) {
560             return savedSearchName[0];
561         }
562         return null;
563     }
564 
565     /**
566      * Override setRows in order to post-process and add documenttype-dependent fields
567      */
568     @Override
569     protected void setRows() {
570         this.setRows(null);
571     }
572 
573     /**
574      * Returns an iterable of current form fields
575      */
576     protected Iterable<Field> getFormFields() {
577         return getFields(this.getRows());
578     }
579 
580     /**
581      * Sets the rows for the search criteria.  This method will delegate to the DocumentSearchCriteriaProcessor
582      * in order to pull in fields for custom search attributes.
583      *
584      * @param documentTypeName the name of the document type currently entered on the form, if this is a valid document
585      * type then it may have search attribute fields that need to be displayed; documentType name may also be loaded
586      * via a saved search
587      */
588     protected void setRows(String documentTypeName) {
589         if (getRows() == null) {
590             super.setRows();
591         }
592         List<Row> lookupRows = new ArrayList<Row>();
593         //copy the current rows
594         for (Row row : getRows()) {
595             lookupRows.add(row);
596         }
597         //clear out
598         getRows().clear();
599 
600         DocumentType docType = getValidDocumentType(documentTypeName);
601 
602         boolean advancedSearch = isAdvancedSearch();
603         boolean superUserSearch = isSuperUserSearch();
604 
605         //call get rows
606         List<Row> rows = getDocumentSearchCriteriaProcessor().getRows(docType,lookupRows, advancedSearch, superUserSearch);
607 
608         BusinessObjectEntry boe = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(this.getBusinessObjectClass().getName());
609         int numCols = boe.getLookupDefinition().getNumOfColumns();
610         if(numCols == 0) {
611             numCols = KRADConstants.DEFAULT_NUM_OF_COLUMNS;
612         }
613 
614         super.getRows().addAll(FieldUtils.wrapFields(this.getFields(rows), numCols));
615 
616     }
617 
618     private static List<Field> getFields(Collection<? extends Row> rows) {
619         List<Field> rList = new ArrayList<Field>();
620         for (Row r : rows) {
621             for (Field f : r.getFields()) {
622                 rList.add(f);
623             }
624         }
625         return rList;
626     }
627 
628     /**
629      * Checks for a valid document type with the given name in a case-sensitive manner.
630      *
631      * @return the DocumentType which matches the given name or null if no valid document type could be found
632      */
633     private DocumentType getValidDocumentType(String documentTypeName) {
634         if (StringUtils.isNotEmpty(documentTypeName)) {
635             DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName.trim());
636             if (documentType != null && documentType.isActive()) {
637                 return documentType;
638             }
639         }
640         return null;
641     }
642 
643     private static String TOGGLE_BUTTON = "<input type='image' name=''{0}'' id=''{0}'' class='tinybutton' src=''..{1}/images/tinybutton-{2}search.gif'' alt=''{3} search'' title=''{3} search''/>";
644 
645     @Override
646     public String getSupplementalMenuBar() {
647         boolean advancedSearch = isAdvancedSearch();
648         boolean superUserSearch = isSuperUserSearch();
649         StringBuilder suppMenuBar = new StringBuilder();
650 
651         // Add the detailed-search-toggling button.
652         // to mimic previous behavior, basic search button is shown both when currently rendering detailed search AND super user search
653         // as super user search is essentially a detailed search
654         String type = advancedSearch ? "basic" : "detailed";
655         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleAdvancedSearch", KewApiConstants.WEBAPP_DIRECTORY, type, type));
656 
657         // Add the superuser-search-toggling button.
658         suppMenuBar.append("&nbsp;");
659         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleSuperUserSearch", KewApiConstants.WEBAPP_DIRECTORY, superUserSearch ? "nonsupu" : "superuser", superUserSearch ? "non-superuser" : "superuser"));
660 
661         // Add the "clear saved searches" button.
662         suppMenuBar.append("&nbsp;");
663         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, KewApiConstants.WEBAPP_DIRECTORY, "clearsaved", "clear saved searches"));
664 
665         return suppMenuBar.toString();
666     }
667 
668     @Override
669     public boolean shouldDisplayHeaderNonMaintActions() {
670         return true;
671     }
672 
673     @Override
674     public boolean shouldDisplayLookupCriteria() {
675         return true;
676     }
677 
678     /**
679      * Determines if there should be more search fields rendered based on already entered search criteria, and
680      * generates additional form rows.
681      */
682     @Override
683     public boolean checkForAdditionalFields(Map fieldValues) {
684         // The given map is a Map<String, String>
685         Object val = fieldValues.get("documentTypeName");
686         String documentTypeName;
687         if (val instanceof String[]) {
688             documentTypeName = ((String[]) val)[0];
689         } else {
690             documentTypeName = (String) val;
691         }
692         if (StringUtils.isNotBlank(documentTypeName)) {
693             setRows(documentTypeName);
694         }
695         return true;
696     }
697 
698     @Override
699     public Field getExtraField() {
700         SavedSearchValuesFinder savedSearchValuesFinder = new SavedSearchValuesFinder();
701         List<KeyValue> savedSearchValues = savedSearchValuesFinder.getKeyValues();
702         Field savedSearch = new Field();
703         savedSearch.setPropertyName(SAVED_SEARCH_NAME_PARAM);
704         savedSearch.setFieldType(Field.DROPDOWN_SCRIPT);
705         savedSearch.setScript("customLookupChanged()");
706         savedSearch.setFieldValidValues(savedSearchValues);
707         savedSearch.setFieldLabel("Saved Searches");
708         return savedSearch;
709     }
710 
711     @Override
712     public void performClear(LookupForm lookupForm) {
713         DocumentSearchCriteria criteria = loadCriteria(lookupForm.getFields());
714         super.performClear(lookupForm);
715         repopulateSearchTypeFlags();
716         DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
717         if (documentType != null) {
718             DocumentSearchCriteria clearedCriteria = documentSearchService.clearCriteria(documentType, criteria);
719             applyCriteriaChangesToFields(DocumentSearchCriteria.Builder.create(clearedCriteria));
720         }
721     }
722 
723     /**
724      * Repopulate the fields indicating advanced/superuser search type.
725      */
726     protected void repopulateSearchTypeFlags() {
727         boolean advancedSearch = isAdvancedSearch();
728         boolean superUserSearch = isSuperUserSearch();
729         int fieldsRepopulated = 0;
730         for (Field field: getFields(super.getRows())) {
731             if (fieldsRepopulated >= 2) {
732                 break;
733             }
734             if (DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD.equals(field.getPropertyName())) {
735                 field.setPropertyValue(advancedSearch ? "YES" : "NO");
736                 fieldsRepopulated++;
737             } else if (DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD.equals(field.getPropertyName())) {
738                 field.setPropertyValue(advancedSearch ? "YES" : "NO");
739                 fieldsRepopulated++;
740             }
741         }
742 
743     }
744 
745     /**
746      * Takes a collection of result rows and does final processing on them.
747      */
748     protected void postProcessResults(Collection<ResultRow> resultRows, DocumentSearchResults searchResults) {
749         if (resultRows.size() != searchResults.getSearchResults().size()) {
750             throw new IllegalStateException("Encountered a mismatch between ResultRow items and document search results "
751                     + resultRows.size() + " != " + searchResults.getSearchResults().size());
752         }
753         DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
754         DocumentSearchResultSetConfiguration resultSetConfiguration = null;
755         DocumentSearchCriteriaConfiguration criteriaConfiguration = null;
756         if (documentType != null) {
757             resultSetConfiguration =
758                 KEWServiceLocator.getDocumentSearchCustomizationMediator().customizeResultSetConfiguration(
759                         documentType, criteria);
760             criteriaConfiguration =
761                     KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(
762                             documentType);
763 
764         }
765         int index = 0;
766         for (ResultRow resultRow : resultRows) {
767             DocumentSearchResult searchResult = searchResults.getSearchResults().get(index);
768             executeColumnCustomization(resultRow, searchResult, resultSetConfiguration, criteriaConfiguration);
769             index++;
770         }
771     }
772 
773     /**
774      * Executes customization of columns, could include removing certain columns or adding additional columns to the
775      * result row (in cases where columns are added by document search customization, such as searchable attributes).
776      */
777     protected void executeColumnCustomization(ResultRow resultRow, DocumentSearchResult searchResult,
778             DocumentSearchResultSetConfiguration resultSetConfiguration,
779             DocumentSearchCriteriaConfiguration criteriaConfiguration) {
780         if (resultSetConfiguration == null) {
781             resultSetConfiguration = DocumentSearchResultSetConfiguration.Builder.create().build();
782         }
783         if (criteriaConfiguration == null) {
784             criteriaConfiguration = DocumentSearchCriteriaConfiguration.Builder.create().build();
785         }
786         List<StandardResultField> standardFieldsToRemove = resultSetConfiguration.getStandardResultFieldsToRemove();
787         if (standardFieldsToRemove == null) {
788             standardFieldsToRemove = Collections.emptyList();
789         }
790         List<Column> newColumns = new ArrayList<Column>();
791         for (Column standardColumn : resultRow.getColumns()) {
792             if (!standardFieldsToRemove.contains(StandardResultField.fromFieldName(standardColumn.getPropertyName()))) {
793                 newColumns.add(standardColumn);
794                 // modify the route log column so that xml values are not escaped (allows for the route log <img ...> to be
795                 // rendered properly)
796                 if (standardColumn.getPropertyName().equals(
797                         KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG)) {
798                     standardColumn.setEscapeXMLValue(false);
799                 }
800             }
801         }
802 
803         // determine which document attribute fields should be added
804         List<RemotableAttributeField> searchAttributeFields = criteriaConfiguration.getFlattenedSearchAttributeFields();
805         List<String> additionalFieldNamesToInclude = new ArrayList<String>();
806         if (!resultSetConfiguration.isOverrideSearchableAttributes()) {
807             for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
808                 // TODO - KULRICE-5738 - add check here to make sure the searchable attribute should be displayed in result set
809                 // right now this is default always including all searchable attributes!
810                 additionalFieldNamesToInclude.add(searchAttributeField.getName());
811             }
812         }
813         if (resultSetConfiguration.getCustomFieldNamesToAdd() != null) {
814             additionalFieldNamesToInclude.addAll(resultSetConfiguration.getCustomFieldNamesToAdd());
815         }
816 
817         // now assemble the custom columns
818         List<Column> customColumns = new ArrayList<Column>();
819         List<Column> additionalAttributeColumns = FieldUtils.constructColumnsFromAttributeFields(
820                 resultSetConfiguration.getAdditionalAttributeFields());
821 
822         outer:for (String additionalFieldNameToInclude : additionalFieldNamesToInclude) {
823             // search the search attribute fields
824             for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
825                 if (additionalFieldNameToInclude.equals(searchAttributeField.getName())) {
826                     Column searchAttributeColumn = FieldUtils.constructColumnFromAttributeField(searchAttributeField);
827                     wrapDocumentAttributeColumnName(searchAttributeColumn);
828                     customColumns.add(searchAttributeColumn);
829                     continue outer;
830                 }
831             }
832             for (Column additionalAttributeColumn : additionalAttributeColumns) {
833                 if (additionalFieldNameToInclude.equals(additionalAttributeColumn.getPropertyName())) {
834                     wrapDocumentAttributeColumnName(additionalAttributeColumn);
835                     customColumns.add(additionalAttributeColumn);
836                     continue outer;
837                 }
838             }
839             LOG.warn("Failed to locate a proper column definition for requested additional field to include in"
840                     + "result set with name '"
841                     + additionalFieldNameToInclude
842                     + "'");
843         }
844         populateCustomColumns(customColumns, searchResult);
845 
846         // now merge the custom columns into the standard columns right before the route log (if the route log column wasn't removed!)
847         if (newColumns.isEmpty() || !StandardResultField.ROUTE_LOG.isFieldNameValid(newColumns.get(newColumns.size() - 1).getPropertyName())) {
848             newColumns.addAll(customColumns);
849         } else {
850             newColumns.addAll(newColumns.size() - 1, customColumns);
851         }
852         resultRow.setColumns(newColumns);
853     }
854 
855     protected void populateCustomColumns(List<Column> customColumns, DocumentSearchResult searchResult) {
856         for (Column customColumn : customColumns) {
857             DocumentAttribute documentAttribute =
858                     searchResult.getSingleDocumentAttributeByName(customColumn.getPropertyName());
859             if (documentAttribute != null && documentAttribute.getValue() != null) {
860                 wrapDocumentAttributeColumnName(customColumn);
861                 // list moving forward if the attribute has more than one value
862                 Formatter formatter = customColumn.getFormatter();
863                 customColumn.setPropertyValue(formatter.format(documentAttribute.getValue()).toString());
864             }
865         }
866     }
867 
868     private void wrapDocumentAttributeColumnName(Column column) {
869         // TODO - comment out for now, not sure we really want to do this...
870         //column.setPropertyName(DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX + column.getPropertyName());
871     }
872 
873     public void setDocumentSearchService(DocumentSearchService documentSearchService) {
874         this.documentSearchService = documentSearchService;
875     }
876 
877     public DocumentSearchService getDocumentSearchService() {
878         return documentSearchService;
879     }
880 
881     public DocumentSearchCriteriaProcessor getDocumentSearchCriteriaProcessor() {
882         return documentSearchCriteriaProcessor;
883     }
884 
885     public void setDocumentSearchCriteriaProcessor(DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor) {
886         this.documentSearchCriteriaProcessor = documentSearchCriteriaProcessor;
887     }
888 
889     public DocumentSearchCriteriaTranslator getDocumentSearchCriteriaTranslator() {
890         return documentSearchCriteriaTranslator;
891     }
892 
893     public void setDocumentSearchCriteriaTranslator(DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator) {
894         this.documentSearchCriteriaTranslator = documentSearchCriteriaTranslator;
895     }
896 
897 }