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