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