View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.impl.document.search;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.ArrayUtils;
20  import org.apache.commons.lang.BooleanUtils;
21  import org.apache.commons.lang.ObjectUtils;
22  import org.apache.commons.lang.StringUtils;
23  import org.kuali.rice.core.api.CoreApiServiceLocator;
24  import org.kuali.rice.core.api.config.property.Config;
25  import org.kuali.rice.core.api.config.property.ConfigContext;
26  import org.kuali.rice.core.api.search.SearchOperator;
27  import org.kuali.rice.core.api.uif.RemotableAttributeField;
28  import org.kuali.rice.core.api.util.KeyValue;
29  import org.kuali.rice.core.api.util.RiceKeyConstants;
30  import org.kuali.rice.core.api.util.type.KualiDecimal;
31  import org.kuali.rice.core.api.util.type.KualiPercent;
32  import org.kuali.rice.core.web.format.Formatter;
33  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
34  import org.kuali.rice.kew.api.KEWPropertyConstants;
35  import org.kuali.rice.kew.api.KewApiConstants;
36  import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
37  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
38  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteriaContract;
39  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
40  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
41  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessor;
42  import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaProcessorKEWAdapter;
43  import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
44  import org.kuali.rice.kew.doctype.bo.DocumentType;
45  import org.kuali.rice.kew.exception.WorkflowServiceError;
46  import org.kuali.rice.kew.exception.WorkflowServiceErrorException;
47  import org.kuali.rice.kew.framework.document.search.AttributeFields;
48  import org.kuali.rice.kew.framework.document.search.DocumentSearchCriteriaConfiguration;
49  import org.kuali.rice.kew.framework.document.search.DocumentSearchResultSetConfiguration;
50  import org.kuali.rice.kew.framework.document.search.StandardResultField;
51  import org.kuali.rice.kew.lookup.valuefinder.SavedSearchValuesFinder;
52  import org.kuali.rice.kew.service.KEWServiceLocator;
53  import org.kuali.rice.kew.user.UserUtils;
54  import org.kuali.rice.kew.util.DocumentTypeWindowTargets;
55  import org.kuali.rice.kim.api.identity.Person;
56  import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
57  import org.kuali.rice.kns.lookup.HtmlData;
58  import org.kuali.rice.kns.lookup.KualiLookupableHelperServiceImpl;
59  import org.kuali.rice.kns.lookup.LookupUtils;
60  import org.kuali.rice.kns.util.FieldUtils;
61  import org.kuali.rice.kns.web.struts.form.LookupForm;
62  import org.kuali.rice.kns.web.ui.Column;
63  import org.kuali.rice.kns.web.ui.Field;
64  import org.kuali.rice.kns.web.ui.ResultRow;
65  import org.kuali.rice.kns.web.ui.Row;
66  import org.kuali.rice.krad.bo.BusinessObject;
67  import org.kuali.rice.krad.exception.ValidationException;
68  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
69  import org.kuali.rice.krad.util.GlobalVariables;
70  import org.kuali.rice.krad.util.KRADConstants;
71  
72  import java.lang.reflect.InvocationTargetException;
73  import java.math.BigDecimal;
74  import java.text.MessageFormat;
75  import java.util.*;
76  import java.util.regex.Matcher;
77  import java.util.regex.Pattern;
78  
79  /**
80   * Implementation of lookupable helper service which handles the complex lookup behavior required by the KEW
81   * document search screen.
82   *
83   * @author Kuali Rice Team (rice.collab@kuali.org)
84   */
85  public class DocumentSearchCriteriaBoLookupableHelperService extends KualiLookupableHelperServiceImpl {
86  
87      static final String SAVED_SEARCH_NAME_PARAM = "savedSearchToLoadAndExecute";
88      static final String DOCUMENT_TYPE_NAME_PARAM = "documentTypeName";
89      static final String TARGET_SPEC_PARAM = "targetSpec";
90      static final String DOCUMENT_TARGET_SPEC_PARAM = "documentTargetSpec";
91      static final String ROUTE_LOG_TARGET_SPEC_PARAM = "routeLogTargetSpec";
92      static final String SHOW_SUPER_USER_BUTTON_PARAM = "showSuperUserButton";
93  
94      // warning message keys
95  
96      private static final String EXCEED_THRESHOLD_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThreshold";
97      private static final String SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.securityFiltered";
98      private static final String EXCEED_THRESHOLD_AND_SECURITY_FILTERED_MESSAGE_KEY = "docsearch.DocumentSearchService.exceededThresholdAndSecurityFiltered";
99  
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 }