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