View Javadoc

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