001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kew.impl.document.search;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.ArrayUtils;
020import org.apache.commons.lang.BooleanUtils;
021import org.apache.commons.lang.ObjectUtils;
022import org.apache.commons.lang.StringUtils;
023import org.joda.time.DateTime;
024import org.kuali.rice.core.api.CoreApiServiceLocator;
025import org.kuali.rice.core.api.config.property.Config;
026import org.kuali.rice.core.api.config.property.ConfigContext;
027import org.kuali.rice.core.api.search.SearchOperator;
028import org.kuali.rice.core.api.uif.RemotableAttributeField;
029import org.kuali.rice.core.api.util.KeyValue;
030import org.kuali.rice.core.api.util.RiceKeyConstants;
031import org.kuali.rice.core.api.util.type.KualiDecimal;
032import org.kuali.rice.core.api.util.type.KualiPercent;
033import org.kuali.rice.core.web.format.Formatter;
034import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
035import org.kuali.rice.kew.api.KEWPropertyConstants;
036import org.kuali.rice.kew.api.KewApiConstants;
037import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
038import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
039import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
040import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
041import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
042import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor;
043import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter;
044import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
045import org.kuali.rice.kew.doctype.bo.DocumentType;
046import org.kuali.rice.kew.exception.WorkflowServiceError;
047import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
048import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
049import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration;
050import org.kuali.rice.kew.framework.document.search.StandardResultField;
051import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaBo;
052import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaTranslator;
053import org.kuali.rice.kew.impl.document.search.FormFields;
054import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder;
055import org.kuali.rice.kew.service.KEWServiceLocator;
056import org.kuali.rice.kew.user.UserUtils;
057import org.kuali.rice.kim.api.identity.Person;
058import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
059import org.kuali.rice.kns.lookup.HtmlData;
060import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl;
061import org.kuali.rice.kns.lookup.LookupUtils;
062import org.kuali.rice.kns.util.FieldUtils;
063import org.kuali.rice.kns.web.struts.form.LookupForm;
064import org.kuali.rice.kns.web.ui.Column;
065import org.kuali.rice.kns.web.ui.Field;
066import org.kuali.rice.kns.web.ui.ResultRow;
067import org.kuali.rice.kns.web.ui.Row;
068import org.kuali.rice.krad.UserSession;
069import org.kuali.rice.krad.bo.BusinessObject;
070import org.kuali.rice.krad.exception.ValidationException;
071import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
072import org.kuali.rice.krad.util.GlobalVariables;
073import org.kuali.rice.krad.util.KRADConstants;
074
075import java.lang.reflect.InvocationTargetException;
076import java.math.BigDecimal;
077import java.text.MessageFormat;
078import java.util.ArrayList;
079import java.util.Collection;
080import java.util.Collections;
081import java.util.HashMap;
082import java.util.List;
083import java.util.Map;
084import java.util.regex.Matcher;
085import 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 */
093public 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}