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