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.coreservice.framework.CoreFrameworkServiceLocator;
24  import org.kuali.rice.core.api.CoreApiServiceLocator;
25  import org.kuali.rice.core.api.config.property.Config;
26  import org.kuali.rice.core.api.config.property.ConfigContext;
27  import org.kuali.rice.core.api.search.SearchOperator;
28  import org.kuali.rice.core.api.uif.RemotableAttributeField;
29  import org.kuali.rice.core.api.util.KeyValue;
30  import org.kuali.rice.core.api.util.RiceKeyConstants;
31  import org.kuali.rice.core.web.format.Formatter;
32  import org.kuali.rice.kew.api.KEWPropertyConstants;
33  import org.kuali.rice.kew.api.KewApiConstants;
34  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
35  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
36  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
37  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
38  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
39  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor;
40  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter;
41  import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
42  import org.kuali.rice.kew.doctype.bo.DocumentType;
43  import org.kuali.rice.kew.exception.WorkflowServiceError;
44  import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
45  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
46  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration;
47  import org.kuali.rice.kew.framework.document.search.StandardResultField;
48  import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder;
49  import org.kuali.rice.kew.service.KEWServiceLocator;
50  import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
51  import org.kuali.rice.kns.lookup.HtmlData;
52  import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl;
53  import org.kuali.rice.kns.lookup.LookupUtils;
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(LookupUtils.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 
377         // update the parameters to include whether or not this is an advanced search
378         if(this.getParameters().containsKey(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD)) {
379             Map<String, String[]> parameters = this.getParameters();
380             String[] params = (String[])parameters.get(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD);
381             if (ArrayUtils.isNotEmpty(params)) {
382                 params[0] = criteria.getIsAdvancedSearch();
383                 this.setParameters(parameters);
384             }
385         }
386 
387         // and set the rows based on doc type
388         setRows(docTypeName);
389 
390         // clear the name of the search in the form
391         //fieldValues.put(SAVED_SEARCH_NAME_PARAM, new String[0]);
392 
393         // set the custom document attribute values on the search form
394         for (Map.Entry<String, List<String>> entry: criteria.getDocumentAttributeValues().entrySet()) {
395             fieldValues.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
396         }
397 
398         // sets the field values on the form, trying criteria object properties if a field value is not present in the map
399         for (Field field : getFormFields()) {
400             if (field.getPropertyName() != null && !field.getPropertyName().equals("")) {
401                 // UI Fields know whether they are single or multiple value
402                 // just set both so they can make the determination and render appropriately
403                 String[] values = null;
404                 if (fieldValues.get(field.getPropertyName()) != null) {
405                     values = fieldValues.get(field.getPropertyName());
406                 } else {
407                     //may be on the root of the criteria object, try looking there:
408                     try {
409                         values = new String[] { ObjectUtils.toString(PropertyUtils.getProperty(criteria, field.getPropertyName())) };
410                     } catch (IllegalAccessException e) {
411                         e.printStackTrace();
412                     } catch (InvocationTargetException e) {
413                         e.printStackTrace();
414                     } catch (NoSuchMethodException e) {
415                         // e.printStackTrace();
416                         //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.
417                     }
418                 }
419                 if (values != null) {
420                     setFieldValue(field, values);
421                 }
422             }
423         }
424 
425         return true;
426     }
427 
428     /**
429      * Performs custom document search/lookup actions.
430      * 1) switching between simple/detailed search
431      * 2) switching between non-superuser/superuser search
432      * 3) clearing saved search results
433      * 4) restoring a saved search and executing the search
434      * @param ignoreErrors
435      * @return whether to rerun the previous search; false in cases 1-3 because we are just updating the form
436      */
437     @Override
438     public boolean performCustomAction(boolean ignoreErrors) {
439         //boolean isConfigAction = isAdvancedSearch() || isSuperUserSearch() || isClearSavedSearch();
440         if (isClearSavedSearch()) {
441             KEWServiceLocator.getDocumentSearchService().clearNamedSearches(GlobalVariables.getUserSession().getPrincipalId());
442             return false;
443         }
444         else if (getSavedSearchName() != null) {
445             return loadSavedSearch(ignoreErrors);
446         } else {
447             toggleFormView();
448             // Finally, return false to prevent the search from being performed and to skip the other custom processing below.
449             return false;
450         }
451     }
452 
453     /**
454      * Custom implementation of getInquiryUrl that sets up doc handler link.
455      */
456     @Override
457     public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) {
458         DocumentSearchCriteriaBo criteriaBo = (DocumentSearchCriteriaBo)bo;
459         if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_DOCUMENT_ID.equals(propertyName)) {
460             return generateDocumentHandlerUrl(criteriaBo.getDocumentId(), criteriaBo.getDocumentType(),
461                     isSuperUserSearch());
462         } else if (KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG.equals(propertyName)) {
463             return generateRouteLogUrl(criteriaBo.getDocumentId());
464         }
465         return super.getInquiryUrl(bo, propertyName);
466     }
467 
468     /**
469      * Generates the appropriate document handler url for the given document.  If superUserSearch is true then a super
470      * user doc handler link will be generated if the document type policy allows it.
471      */
472     protected HtmlData.AnchorHtmlData generateDocumentHandlerUrl(String documentId, DocumentType documentType, boolean superUserSearch) {
473         HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
474         link.setDisplayText(documentId);
475         if (isDocumentHandlerPopup()) {
476             link.setTarget("_blank");
477         }
478         String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/";
479         if (superUserSearch) {
480             if (documentType.getUseWorkflowSuperUserDocHandlerUrl().getPolicyValue().booleanValue()) {
481                 url += "SuperUser.do?methodToCall=displaySuperUserDocument&documentId=" + documentId;
482             } else {
483                 url = KewApiConstants.DOC_HANDLER_REDIRECT_PAGE
484                         + "?" + KewApiConstants.COMMAND_PARAMETER + "="
485                         + KewApiConstants.SUPERUSER_COMMAND + "&"
486                         + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
487                         + documentId;
488             }
489         } else {
490             url += KewApiConstants.DOC_HANDLER_REDIRECT_PAGE + "?"
491                     + KewApiConstants.COMMAND_PARAMETER + "="
492                     + KewApiConstants.DOCSEARCH_COMMAND + "&"
493                     + KewApiConstants.DOCUMENT_ID_PARAMETER + "="
494                     + documentId;
495         }
496         link.setHref(url);
497         return link;
498     }
499 
500     protected HtmlData.AnchorHtmlData generateRouteLogUrl(String documentId) {
501         HtmlData.AnchorHtmlData link = new HtmlData.AnchorHtmlData();
502         if (isRouteLogPopup()) {
503             link.setTarget("_blank");
504         }
505         link.setDisplayText("Route Log for document " + documentId);
506         String url = ConfigContext.getCurrentContextConfig().getProperty(Config.KEW_URL) + "/" +
507                 "RouteLog.do?documentId=" + documentId;
508         link.setHref(url);
509         return link;
510     }
511 
512     /**
513      * Returns true if the document handler should open in a new window.
514      */
515     protected boolean isDocumentHandlerPopup() {
516         return BooleanUtils.toBooleanDefaultIfNull(
517                 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
518                     KewApiConstants.KEW_NAMESPACE,
519                     KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
520                     KewApiConstants.DOCUMENT_SEARCH_DOCUMENT_POPUP_IND),
521                 DOCUMENT_HANDLER_POPUP_DEFAULT);
522     }
523 
524     /**
525      * Returns true if the route log should open in a new window.
526      */
527     public boolean isRouteLogPopup() {
528         return BooleanUtils.toBooleanDefaultIfNull(
529                 CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(KewApiConstants.KEW_NAMESPACE,
530                         KRADConstants.DetailTypes.DOCUMENT_SEARCH_DETAIL_TYPE,
531                         KewApiConstants.DOCUMENT_SEARCH_ROUTE_LOG_POPUP_IND), ROUTE_LOG_POPUP_DEFAULT);
532     }
533 
534     /**
535      * Parses a boolean request parameter
536      */
537     protected boolean isFlagSet(String flagName) {
538         if(this.getParameters().containsKey(flagName)) {
539             String[] params = (String[])this.getParameters().get(flagName);
540             if (ArrayUtils.isNotEmpty(params)) {
541                 return "YES".equalsIgnoreCase(params[0]);
542             }
543         }
544         return false;
545     }
546 
547     /**
548      * Returns true if the current search being executed is a super user search.
549      */
550     protected boolean isSuperUserSearch() {
551         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD);
552     }
553 
554     /**
555      * Returns true if the current search being executed is an "advanced" search.
556      */
557     protected boolean isAdvancedSearch() {
558         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD);
559     }
560 
561     /**
562      * Returns true if the current "search" being executed is an "clear" search.
563      */
564     protected boolean isClearSavedSearch() {
565         return isFlagSet(DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD);
566     }
567 
568     protected String getSavedSearchName() {
569         String[] savedSearchName = getParameters().get(SAVED_SEARCH_NAME_PARAM);
570         if (savedSearchName != null && savedSearchName.length > 0) {
571             return savedSearchName[0];
572         }
573         return null;
574     }
575 
576     /**
577      * Override setRows in order to post-process and add documenttype-dependent fields
578      */
579     @Override
580     protected void setRows() {
581         this.setRows(null);
582     }
583 
584     /**
585      * Returns an iterable of current form fields
586      */
587     protected Iterable<Field> getFormFields() {
588         return getFields(this.getRows());
589     }
590 
591     /**
592      * Sets the rows for the search criteria.  This method will delegate to the DocumentSearchCriteriaProcessor
593      * in order to pull in fields for custom search attributes.
594      *
595      * @param documentTypeName the name of the document type currently entered on the form, if this is a valid document
596      * type then it may have search attribute fields that need to be displayed; documentType name may also be loaded
597      * via a saved search
598      */
599     protected void setRows(String documentTypeName) {
600         // Always call superclass to regenerate the rows since state may have changed (namely, documentTypeName parsed from params)
601         super.setRows();
602 
603         List<Row> lookupRows = new ArrayList<Row>();
604         //copy the current rows
605         for (Row row : getRows()) {
606             lookupRows.add(row);
607         }
608         //clear out
609         getRows().clear();
610 
611         DocumentType docType = getValidDocumentType(documentTypeName);
612 
613         boolean advancedSearch = isAdvancedSearch();
614         boolean superUserSearch = isSuperUserSearch();
615 
616         //call get rows
617         List<Row> rows = getDocumentSearchCriteriaProcessor().getRows(docType,lookupRows, advancedSearch, superUserSearch);
618 
619         BusinessObjectEntry boe = (BusinessObjectEntry) KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(this.getBusinessObjectClass().getName());
620         int numCols = boe.getLookupDefinition().getNumOfColumns();
621         if(numCols == 0) {
622             numCols = KRADConstants.DEFAULT_NUM_OF_COLUMNS;
623         }
624 
625         super.getRows().addAll(FieldUtils.wrapFields(this.getFields(rows), numCols));
626 
627     }
628 
629     private static List<Field> getFields(Collection<? extends Row> rows) {
630         List<Field> rList = new ArrayList<Field>();
631         for (Row r : rows) {
632             for (Field f : r.getFields()) {
633                 rList.add(f);
634             }
635         }
636         return rList;
637     }
638 
639     /**
640      * Checks for a valid document type with the given name in a case-sensitive manner.
641      *
642      * @return the DocumentType which matches the given name or null if no valid document type could be found
643      */
644     private DocumentType getValidDocumentType(String documentTypeName) {
645         if (StringUtils.isNotEmpty(documentTypeName)) {
646             DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByNameCaseInsensitive(documentTypeName.trim());
647             if (documentType != null && documentType.isActive()) {
648                 return documentType;
649             }
650         }
651         return null;
652     }
653 
654     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''/>";
655 
656     @Override
657     public String getSupplementalMenuBar() {
658         boolean advancedSearch = isAdvancedSearch();
659         boolean superUserSearch = isSuperUserSearch();
660         StringBuilder suppMenuBar = new StringBuilder();
661 
662         // Add the detailed-search-toggling button.
663         // to mimic previous behavior, basic search button is shown both when currently rendering detailed search AND super user search
664         // as super user search is essentially a detailed search
665         String type = advancedSearch ? "basic" : "detailed";
666         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleAdvancedSearch", KewApiConstants.WEBAPP_DIRECTORY, type, type));
667 
668         // Add the superuser-search-toggling button.
669         suppMenuBar.append("&nbsp;");
670         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, "toggleSuperUserSearch", KewApiConstants.WEBAPP_DIRECTORY, superUserSearch ? "nonsupu" : "superuser", superUserSearch ? "non-superuser" : "superuser"));
671 
672         // Add the "clear saved searches" button.
673         suppMenuBar.append("&nbsp;");
674         suppMenuBar.append(MessageFormat.format(TOGGLE_BUTTON, DocumentSearchCriteriaProcessorKEWAdapter.CLEARSAVED_SEARCH_FIELD, KewApiConstants.WEBAPP_DIRECTORY, "clearsaved", "clear saved searches"));
675 
676         // Wire up the onblur for document type name
677         suppMenuBar.append("<script type=\"text/javascript\">"
678                 + " jQuery(document).ready(function () {"
679                 + " jQuery(\"#documentTypeName\").blur(function () { validateDocTypeAndRefresh( this ); });"
680                 + "});</script>");
681 
682         return suppMenuBar.toString();
683     }
684 
685     @Override
686     public boolean shouldDisplayHeaderNonMaintActions() {
687         return true;
688     }
689 
690     @Override
691     public boolean shouldDisplayLookupCriteria() {
692         return true;
693     }
694 
695     /**
696      * Determines if there should be more search fields rendered based on already entered search criteria, and
697      * generates additional form rows.
698      */
699     @Override
700     public boolean checkForAdditionalFields(Map fieldValues) {
701         // The given map is a Map<String, String>
702         Object val = fieldValues.get("documentTypeName");
703         String documentTypeName;
704         if (val instanceof String[]) {
705             documentTypeName = ((String[]) val)[0];
706         } else {
707             documentTypeName = (String) val;
708         }
709         if (StringUtils.isNotBlank(documentTypeName)) {
710             setRows(documentTypeName);
711         }
712         return true;
713     }
714 
715     @Override
716     public Field getExtraField() {
717         SavedSearchValuesFinder savedSearchValuesFinder = new SavedSearchValuesFinder();
718         List<KeyValue> savedSearchValues = savedSearchValuesFinder.getKeyValues();
719         Field savedSearch = new Field();
720         savedSearch.setPropertyName(SAVED_SEARCH_NAME_PARAM);
721         savedSearch.setFieldType(Field.DROPDOWN_SCRIPT);
722         savedSearch.setScript("customLookupChanged()");
723         savedSearch.setFieldValidValues(savedSearchValues);
724         savedSearch.setFieldLabel("Saved Searches");
725         return savedSearch;
726     }
727 
728     @Override
729     public void performClear(LookupForm lookupForm) {
730         DocumentSearchCriteria criteria = loadCriteria(lookupForm.getFields());
731         super.performClear(lookupForm);
732         repopulateSearchTypeFlags();
733         DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
734         if (documentType != null) {
735             DocumentSearchCriteria clearedCriteria = documentSearchService.clearCriteria(documentType, criteria);
736             applyCriteriaChangesToFields(DocumentSearchCriteria.Builder.create(clearedCriteria));
737         }
738     }
739 
740     /**
741      * Repopulate the fields indicating advanced/superuser search type.
742      */
743     protected void repopulateSearchTypeFlags() {
744         boolean advancedSearch = isAdvancedSearch();
745         boolean superUserSearch = isSuperUserSearch();
746         int fieldsRepopulated = 0;
747         for (Field field: getFields(super.getRows())) {
748             if (fieldsRepopulated >= 2) {
749                 break;
750             }
751             if (DocumentSearchCriteriaProcessorKEWAdapter.ADVANCED_SEARCH_FIELD.equals(field.getPropertyName())) {
752                 field.setPropertyValue(advancedSearch ? "YES" : "NO");
753                 fieldsRepopulated++;
754             } else if (DocumentSearchCriteriaProcessorKEWAdapter.SUPERUSER_SEARCH_FIELD.equals(field.getPropertyName())) {
755                 field.setPropertyValue(advancedSearch ? "YES" : "NO");
756                 fieldsRepopulated++;
757             }
758         }
759 
760     }
761 
762     /**
763      * Takes a collection of result rows and does final processing on them.
764      */
765     protected void postProcessResults(Collection<ResultRow> resultRows, DocumentSearchResults searchResults) {
766         if (resultRows.size() != searchResults.getSearchResults().size()) {
767             throw new IllegalStateException("Encountered a mismatch between ResultRow items and document search results "
768                     + resultRows.size() + " != " + searchResults.getSearchResults().size());
769         }
770         DocumentType documentType = getValidDocumentType(criteria.getDocumentTypeName());
771         DocumentSearchResultSetConfiguration resultSetConfiguration = null;
772         DocumentSearchCriteriaConfiguration criteriaConfiguration = null;
773         if (documentType != null) {
774             resultSetConfiguration =
775                 KEWServiceLocator.getDocumentSearchCustomizationMediator().customizeResultSetConfiguration(
776                         documentType, criteria);
777             criteriaConfiguration =
778                     KEWServiceLocator.getDocumentSearchCustomizationMediator().getDocumentSearchCriteriaConfiguration(
779                             documentType);
780 
781         }
782         int index = 0;
783         for (ResultRow resultRow : resultRows) {
784             DocumentSearchResult searchResult = searchResults.getSearchResults().get(index);
785             executeColumnCustomization(resultRow, searchResult, resultSetConfiguration, criteriaConfiguration);
786             index++;
787         }
788     }
789 
790     /**
791      * Executes customization of columns, could include removing certain columns or adding additional columns to the
792      * result row (in cases where columns are added by document search customization, such as searchable attributes).
793      */
794     protected void executeColumnCustomization(ResultRow resultRow, DocumentSearchResult searchResult,
795             DocumentSearchResultSetConfiguration resultSetConfiguration,
796             DocumentSearchCriteriaConfiguration criteriaConfiguration) {
797         if (resultSetConfiguration == null) {
798             resultSetConfiguration = DocumentSearchResultSetConfiguration.Builder.create().build();
799         }
800         if (criteriaConfiguration == null) {
801             criteriaConfiguration = DocumentSearchCriteriaConfiguration.Builder.create().build();
802         }
803         List<StandardResultField> standardFieldsToRemove = resultSetConfiguration.getStandardResultFieldsToRemove();
804         if (standardFieldsToRemove == null) {
805             standardFieldsToRemove = Collections.emptyList();
806         }
807         List<Column> newColumns = new ArrayList<Column>();
808         for (Column standardColumn : resultRow.getColumns()) {
809             if (!standardFieldsToRemove.contains(StandardResultField.fromFieldName(standardColumn.getPropertyName()))) {
810                 newColumns.add(standardColumn);
811                 // modify the route log column so that xml values are not escaped (allows for the route log <img ...> to be
812                 // rendered properly)
813                 if (standardColumn.getPropertyName().equals(
814                         KEWPropertyConstants.DOC_SEARCH_RESULT_PROPERTY_NAME_ROUTE_LOG)) {
815                     standardColumn.setEscapeXMLValue(false);
816                 }
817             }
818         }
819 
820         // determine which document attribute fields should be added
821         List<RemotableAttributeField> searchAttributeFields = criteriaConfiguration.getFlattenedSearchAttributeFields();
822         List<String> additionalFieldNamesToInclude = new ArrayList<String>();
823         if (!resultSetConfiguration.isOverrideSearchableAttributes()) {
824             for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
825                 // TODO - KULRICE-5738 - add check here to make sure the searchable attribute should be displayed in result set
826                 // right now this is default always including all searchable attributes!
827                 additionalFieldNamesToInclude.add(searchAttributeField.getName());
828             }
829         }
830         if (resultSetConfiguration.getCustomFieldNamesToAdd() != null) {
831             additionalFieldNamesToInclude.addAll(resultSetConfiguration.getCustomFieldNamesToAdd());
832         }
833 
834         // now assemble the custom columns
835         List<Column> customColumns = new ArrayList<Column>();
836         List<Column> additionalAttributeColumns = FieldUtils.constructColumnsFromAttributeFields(
837                 resultSetConfiguration.getAdditionalAttributeFields());
838 
839         outer:for (String additionalFieldNameToInclude : additionalFieldNamesToInclude) {
840             // search the search attribute fields
841             for (RemotableAttributeField searchAttributeField : searchAttributeFields) {
842                 if (additionalFieldNameToInclude.equals(searchAttributeField.getName())) {
843                     Column searchAttributeColumn = FieldUtils.constructColumnFromAttributeField(searchAttributeField);
844                     wrapDocumentAttributeColumnName(searchAttributeColumn);
845                     customColumns.add(searchAttributeColumn);
846                     continue outer;
847                 }
848             }
849             for (Column additionalAttributeColumn : additionalAttributeColumns) {
850                 if (additionalFieldNameToInclude.equals(additionalAttributeColumn.getPropertyName())) {
851                     wrapDocumentAttributeColumnName(additionalAttributeColumn);
852                     customColumns.add(additionalAttributeColumn);
853                     continue outer;
854                 }
855             }
856             LOG.warn("Failed to locate a proper column definition for requested additional field to include in"
857                     + "result set with name '"
858                     + additionalFieldNameToInclude
859                     + "'");
860         }
861         populateCustomColumns(customColumns, searchResult);
862 
863         // now merge the custom columns into the standard columns right before the route log (if the route log column wasn't removed!)
864         if (newColumns.isEmpty() || !StandardResultField.ROUTE_LOG.isFieldNameValid(newColumns.get(newColumns.size() - 1).getPropertyName())) {
865             newColumns.addAll(customColumns);
866         } else {
867             newColumns.addAll(newColumns.size() - 1, customColumns);
868         }
869         resultRow.setColumns(newColumns);
870     }
871 
872     protected void populateCustomColumns(List<Column> customColumns, DocumentSearchResult searchResult) {
873         for (Column customColumn : customColumns) {
874             DocumentAttribute documentAttribute =
875                     searchResult.getSingleDocumentAttributeByName(customColumn.getPropertyName());
876             if (documentAttribute != null && documentAttribute.getValue() != null) {
877                 wrapDocumentAttributeColumnName(customColumn);
878                 // list moving forward if the attribute has more than one value
879                 Formatter formatter = customColumn.getFormatter();
880                 customColumn.setPropertyValue(formatter.format(documentAttribute.getValue()).toString());
881             }
882         }
883     }
884 
885     private void wrapDocumentAttributeColumnName(Column column) {
886         // TODO - comment out for now, not sure we really want to do this...
887         //column.setPropertyName(DOCUMENT_ATTRIBUTE_PROPERTY_NAME_PREFIX + column.getPropertyName());
888     }
889 
890     public void setDocumentSearchService(DocumentSearchService documentSearchService) {
891         this.documentSearchService = documentSearchService;
892     }
893 
894     public DocumentSearchService getDocumentSearchService() {
895         return documentSearchService;
896     }
897 
898     public DocumentSearchCriteriaProcessor getDocumentSearchCriteriaProcessor() {
899         return documentSearchCriteriaProcessor;
900     }
901 
902     public void setDocumentSearchCriteriaProcessor(DocumentSearchCriteriaProcessor documentSearchCriteriaProcessor) {
903         this.documentSearchCriteriaProcessor = documentSearchCriteriaProcessor;
904     }
905 
906     public DocumentSearchCriteriaTranslator getDocumentSearchCriteriaTranslator() {
907         return documentSearchCriteriaTranslator;
908     }
909 
910     public void setDocumentSearchCriteriaTranslator(DocumentSearchCriteriaTranslator documentSearchCriteriaTranslator) {
911         this.documentSearchCriteriaTranslator = documentSearchCriteriaTranslator;
912     }
913 
914 }