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