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