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.krad.lookup;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.CoreApiServiceLocator;
20  import org.kuali.rice.core.api.encryption.EncryptionService;
21  import org.kuali.rice.core.api.search.SearchOperator;
22  import org.kuali.rice.core.api.util.RiceKeyConstants;
23  import org.kuali.rice.core.api.util.type.TypeUtils;
24  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
25  import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
26  import org.kuali.rice.krad.service.DataObjectAuthorizationService;
27  import org.kuali.rice.krad.service.DocumentDictionaryService;
28  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
29  import org.kuali.rice.krad.service.LookupService;
30  import org.kuali.rice.krad.service.ModuleService;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.UifParameters;
33  import org.kuali.rice.krad.uif.UifPropertyPaths;
34  import org.kuali.rice.krad.uif.control.Control;
35  import org.kuali.rice.krad.uif.control.FilterableLookupCriteriaControl;
36  import org.kuali.rice.krad.uif.control.FilterableLookupCriteriaControlPostData;
37  import org.kuali.rice.krad.uif.control.HiddenControl;
38  import org.kuali.rice.krad.uif.control.ValueConfiguredControl;
39  import org.kuali.rice.krad.uif.element.Link;
40  import org.kuali.rice.krad.uif.field.InputField;
41  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
42  import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
43  import org.kuali.rice.krad.uif.util.ComponentUtils;
44  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
45  import org.kuali.rice.krad.uif.util.ScriptUtils;
46  import org.kuali.rice.krad.util.BeanPropertyComparator;
47  import org.kuali.rice.krad.util.GlobalVariables;
48  import org.kuali.rice.krad.util.KRADConstants;
49  import org.kuali.rice.krad.util.KRADUtils;
50  import org.kuali.rice.krad.util.MessageMap;
51  import org.kuali.rice.krad.util.UrlFactory;
52  
53  import java.security.GeneralSecurityException;
54  import java.util.ArrayList;
55  import java.util.Collection;
56  import java.util.Collections;
57  import java.util.HashMap;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.Properties;
61  
62  /**
63   * View helper service that implements {@link Lookupable} and executes a search using the
64   * {@link org.kuali.rice.krad.service.LookupService}.
65   *
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   * @see LookupForm
68   * @see LookupView
69   * @see org.kuali.rice.krad.service.LookupService
70   */
71  public class LookupableImpl extends ViewHelperServiceImpl implements Lookupable {
72      private static final long serialVersionUID = 1885161468871327740L;
73      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupableImpl.class);
74  
75      private Class<?> dataObjectClass;
76  
77      private transient DataObjectAuthorizationService dataObjectAuthorizationService;
78      private transient DocumentDictionaryService documentDictionaryService;
79      private transient LookupService lookupService;
80      private transient EncryptionService encryptionService;
81  
82      /**
83       * {@inheritDoc}
84       */
85      @Override
86      public Collection<?> performSearch(LookupForm form, Map<String, String> searchCriteria, boolean bounded) {
87          // removed blank search values and decrypt any encrypted search values
88          Map<String, String> adjustedSearchCriteria = processSearchCriteria(form, searchCriteria);
89  
90          boolean isValidCriteria = validateSearchParameters(form, adjustedSearchCriteria);
91          if (!isValidCriteria) {
92              return new ArrayList<Object>();
93          }
94  
95          List<String> wildcardAsLiteralSearchCriteria = identifyWildcardDisabledFields(form, adjustedSearchCriteria);
96  
97          // return empty search results (none found) when the search doesn't have any adjustedSearchCriteria although
98          // a filtered search criteria is specified
99          if (adjustedSearchCriteria == null) {
100             MessageMap messageMap = GlobalVariables.getMessageMap();
101             messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
102                     RiceKeyConstants.INFO_LOOKUP_RESULTS_NONE_FOUND);
103             return new ArrayList<Object>();
104         }
105 
106         // if this class is an EBO, just call the module service to get the results
107         if (ExternalizableBusinessObject.class.isAssignableFrom(getDataObjectClass())) {
108             return getSearchResultsForEBO(adjustedSearchCriteria, !bounded);
109         }
110 
111         // if any of the properties refer to an embedded EBO, call the EBO lookups first and apply to the local lookup
112         try {
113             if (LookupUtils.hasExternalBusinessObjectProperty(getDataObjectClass(), adjustedSearchCriteria)) {
114                 adjustedSearchCriteria = LookupUtils.adjustCriteriaForNestedEBOs(getDataObjectClass(),
115                         adjustedSearchCriteria, !bounded);
116 
117                 if (LOG.isDebugEnabled()) {
118                     LOG.debug("Passing these results into the lookup service: " + adjustedSearchCriteria);
119                 }
120             }
121         } catch (IllegalAccessException e) {
122             throw new RuntimeException("Error trying to check for nested external business objects", e);
123         } catch (InstantiationException e1) {
124             throw new RuntimeException("Error trying to check for nested external business objects", e1);
125         }
126 
127         Integer searchResultsLimit = null;
128         if (bounded) {
129             searchResultsLimit = LookupUtils.getSearchResultsLimit(getDataObjectClass(), form);
130         }
131 
132         // invoke the lookup search to carry out the search
133         Collection<?> searchResults = executeSearch(adjustedSearchCriteria, wildcardAsLiteralSearchCriteria, bounded,
134                 searchResultsLimit);
135 
136         generateLookupResultsMessages(adjustedSearchCriteria, searchResults, bounded, searchResultsLimit);
137 
138         Collection<?> sortedResults;
139         if (searchResults != null) {
140             sortedResults = new ArrayList<Object>(searchResults);
141 
142             sortSearchResults(form, (List<?>) sortedResults);
143         } else {
144             sortedResults = new ArrayList<Object>();
145         }
146 
147         return sortedResults;
148     }
149 
150     /**
151      * Invoked to execute the search with the given criteria and restrictions.
152      *
153      * @param adjustedSearchCriteria map of criteria that has been adjusted (encyrption, ebos, etc)
154      * @param wildcardAsLiteralSearchCriteria map of criteria to treat as literals (wildcards disabled)
155      * @param bounded indicates whether the search should be bounded
156      * @param searchResultsLimit for bounded searches, the result limit
157      * @return Collection<?> collection of data object instances from the search results
158      */
159     protected Collection<?> executeSearch(Map<String, String> adjustedSearchCriteria,
160             List<String> wildcardAsLiteralSearchCriteria, boolean bounded, Integer searchResultsLimit) {
161         return getLookupService().findCollectionBySearchHelper(getDataObjectClass(),
162                 adjustedSearchCriteria, wildcardAsLiteralSearchCriteria, !bounded, searchResultsLimit);
163     }
164 
165     /**
166      * Filters the search criteria to be used with the lookup.
167      *
168      * <p>Processing entails primarily of the removal of filtered and unused/blank search criteria.  Encrypted field
169      * values are decrypted, and date range fields are combined into a single criteria entry.</p>
170      *
171      *  <p>In special cases additional non-valid criteria may be included. E.g. with the KIM User Control as a criteria
172      *  the principal name may be passed so that it is displayed on the control.  The filtering removes these values
173      *  based on the viewPostMetadata.  When calling the search directly (methodToCall=search) the viewPostMetadata is
174      *  not set before filtering therefore non-valid criteria are not supported in these cases.</p>
175      *
176      * @param lookupForm lookup form instance containing the lookup data
177      * @param searchCriteria map of criteria to process
178      * @return map of processed criteria
179      */
180     protected Map<String, String> processSearchCriteria(LookupForm lookupForm, Map<String, String> searchCriteria) {
181         Map<String, InputField> criteriaFields = new HashMap<String, InputField>();
182         if (lookupForm.getView() != null) {
183             criteriaFields = getCriteriaFieldsForValidation((LookupView) lookupForm.getView(), lookupForm);
184         }
185 
186         // combine date range criteria
187         Map<String, String> filteredSearchCriteria = LookupUtils.preprocessDateFields(searchCriteria);
188 
189         // allow lookup inputs to filter the criteria
190         for (String fieldName : searchCriteria.keySet()) {
191             InputField inputField = criteriaFields.get(fieldName);
192 
193             if ((inputField == null) || !(inputField instanceof LookupInputField)) {
194                 continue;
195             }
196 
197             LookupInputField lookupInputField = (LookupInputField) inputField;
198             String propertyName = lookupInputField.getPropertyName();
199 
200             // get the post data for the filterable controls
201             ViewPostMetadata viewPostMetadata = lookupForm.getViewPostMetadata();
202             if (viewPostMetadata != null) {
203                 Object componentPostData = viewPostMetadata.getComponentPostData(lookupForm.getViewId(),
204                         UifConstants.PostMetadata.FILTERABLE_LOOKUP_CRITERIA);
205                 Map<String, FilterableLookupCriteriaControlPostData> filterableLookupCriteria =
206                         (Map<String, FilterableLookupCriteriaControlPostData>) componentPostData;
207 
208                 // first filter the results using the filter on the control
209                 if (filterableLookupCriteria != null && filterableLookupCriteria.containsKey(propertyName)) {
210                     FilterableLookupCriteriaControlPostData postData = filterableLookupCriteria.get(propertyName);
211                     Class<? extends FilterableLookupCriteriaControl> controlClass = postData.getControlClass();
212                     FilterableLookupCriteriaControl control = KRADUtils.createNewObjectFromClass(controlClass);
213 
214                     filteredSearchCriteria = control.filterSearchCriteria(propertyName, filteredSearchCriteria,
215                             postData);
216                 }
217 
218                 // second filter the results using the filter in the input field
219                 filteredSearchCriteria = lookupInputField.filterSearchCriteria(filteredSearchCriteria);
220 
221                 // early return if we have no results
222                 if (filteredSearchCriteria == null) {
223                     return null;
224                 }
225             }
226         }
227 
228         // decryption any encrypted search values
229         Map<String, String> processedSearchCriteria = new HashMap<String, String>();
230         for (String fieldName : filteredSearchCriteria.keySet()) {
231             String fieldValue = filteredSearchCriteria.get(fieldName);
232 
233             // do not add hidden or blank criteria
234             InputField inputField = criteriaFields.get(fieldName);
235             if (((inputField != null) && (inputField.getControl() instanceof HiddenControl)) || StringUtils.isBlank(
236                     fieldValue)) {
237                 continue;
238             }
239 
240             if (fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
241                 String encryptedValue = StringUtils.removeEnd(fieldValue, EncryptionService.ENCRYPTION_POST_PREFIX);
242                 try {
243                     fieldValue = getEncryptionService().decrypt(encryptedValue);
244                 } catch (GeneralSecurityException e) {
245                     throw new RuntimeException("Error decrypting value for business object class " +
246                             getDataObjectClass() + " attribute " + fieldName, e);
247                 }
248             }
249 
250             processedSearchCriteria.put(fieldName, fieldValue);
251         }
252 
253         return processedSearchCriteria;
254     }
255 
256     /**
257      * Determines which searchCriteria have been configured with wildcard characters disabled.
258      *
259      * @param lookupForm form used to collect search criteria
260      * @param searchCriteria Map of property names and values to use as search parameters
261      * @return List of property names which have wildcard characters disabled
262      */
263     protected List<String> identifyWildcardDisabledFields(LookupForm lookupForm, Map<String, String> searchCriteria) {
264         List<String> wildcardAsLiteralPropertyNames = new ArrayList<String>();
265 
266         if (searchCriteria != null) {
267             Map<String, InputField> criteriaFields = new HashMap<String, InputField>();
268             if (lookupForm.getView() != null) {
269                 criteriaFields = getCriteriaFieldsForValidation((LookupView) lookupForm.getView(), lookupForm);
270             }
271 
272             for (String fieldName : searchCriteria.keySet()) {
273                 InputField inputField = criteriaFields.get(fieldName);
274                 if ((inputField == null) || !(inputField instanceof LookupInputField)) {
275                     continue;
276                 }
277 
278                 if ((LookupInputField.class.isAssignableFrom(inputField.getClass())) && (((LookupInputField) inputField)
279                         .isDisableWildcardsAndOperators())) {
280                     wildcardAsLiteralPropertyNames.add(fieldName);
281                 }
282             }
283         }
284 
285         return wildcardAsLiteralPropertyNames;
286     }
287 
288     /**
289      * Invoked to perform validation on the search criteria before the search is performed.
290      *
291      * @param form lookup form instance containing the lookup data
292      * @param searchCriteria map of criteria where key is search property name and value is
293      * search value (which can include wildcards)
294      * @return boolean true if validation was successful, false if there were errors and the search
295      *         should not be performed
296      */
297     protected boolean validateSearchParameters(LookupForm form, Map<String, String> searchCriteria) {
298         boolean valid = true;
299 
300         if (searchCriteria == null) {
301             return valid;
302         }
303 
304         Map<String, InputField> criteriaFields = getCriteriaFieldsForValidation((LookupView) form.getView(),
305                 form);
306 
307         // TODO: this should be an error condition but we have an issue when the search is performed from
308         // the initial request and there is not a posted view
309         if ((criteriaFields == null) || criteriaFields.isEmpty()) {
310             return valid;
311         }
312 
313         // build list of hidden properties configured with criteria fields so they are excluded from validation
314         List<String> hiddenCriteria = new ArrayList<String>();
315         for (InputField field : criteriaFields.values()) {
316             if (field.getAdditionalHiddenPropertyNames() != null) {
317                 hiddenCriteria.addAll(field.getAdditionalHiddenPropertyNames());
318             }
319         }
320 
321         for (Map.Entry<String, String> searchKeyValue : searchCriteria.entrySet()) {
322             String searchPropertyName = searchKeyValue.getKey();
323             String searchPropertyValue = searchKeyValue.getValue();
324 
325             InputField inputField = criteriaFields.get(searchPropertyName);
326 
327             String adjustedSearchPropertyPath = UifPropertyPaths.LOOKUP_CRITERIA + "[" + searchPropertyName + "]";
328             //TODO: Currently the validation is called before the performLifeCycle is completed on the view and
329             // hence any additionalHiddenPropertyNames will not be set on the InputFields. For now if the
330             // inputField is not found for the criteria, it is treated as "Valid"
331             if (inputField == null || hiddenCriteria.contains(adjustedSearchPropertyPath)) {
332                 return valid;
333             }
334 
335             //   if (inputField == null) {
336             //       throw new RuntimeException("Invalid search value sent for property name: " + searchPropertyName);
337             //   }
338 
339             if (StringUtils.isBlank(searchPropertyValue) && inputField.getRequired()) {
340                 GlobalVariables.getMessageMap().putError(inputField.getPropertyName(), RiceKeyConstants.ERROR_REQUIRED,
341                         inputField.getLabel());
342             }
343 
344             validateSearchParameterWildcardAndOperators(inputField, searchPropertyValue);
345         }
346 
347         if (GlobalVariables.getMessageMap().hasErrors()) {
348             valid = false;
349         }
350 
351         return valid;
352     }
353 
354     /**
355      * Validates that any wildcards contained within the search value are valid wildcards and allowed for the
356      * property type for which the field is searching.
357      *
358      * @param inputField attribute field instance for the field that is being searched
359      * @param searchPropertyValue value given for field to search for
360      */
361     protected void validateSearchParameterWildcardAndOperators(InputField inputField, String searchPropertyValue) {
362         if (StringUtils.isBlank(searchPropertyValue)) {
363             return;
364         }
365 
366         // make sure a wildcard/operator is in the value
367         boolean found = false;
368         for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
369             String queryCharacter = op.op();
370 
371             if (searchPropertyValue.contains(queryCharacter)) {
372                 found = true;
373             }
374         }
375 
376         // no query characters to validate
377         if (!found) {
378             return;
379         }
380 
381         String attributeLabel = inputField.getLabel();
382         if ((LookupInputField.class.isAssignableFrom(inputField.getClass())) && (((LookupInputField) inputField)
383                 .isDisableWildcardsAndOperators())) {
384             Class<?> propertyType = ObjectPropertyUtils.getPropertyType(getDataObjectClass(),
385                     inputField.getPropertyName());
386 
387             if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) ||
388                     TypeUtils.isTemporalClass(propertyType)) {
389                 GlobalVariables.getMessageMap().putError(inputField.getPropertyName(),
390                         RiceKeyConstants.ERROR_WILDCARDS_AND_OPERATORS_NOT_ALLOWED_ON_FIELD, attributeLabel);
391             } else if (TypeUtils.isStringClass(propertyType)) {
392                 GlobalVariables.getMessageMap().putInfo(inputField.getPropertyName(),
393                         RiceKeyConstants.INFO_WILDCARDS_AND_OPERATORS_TREATED_LITERALLY, attributeLabel);
394             }
395         } else if (inputField.hasSecureValue()) {
396             GlobalVariables.getMessageMap().putError(inputField.getPropertyName(), RiceKeyConstants.ERROR_SECURE_FIELD,
397                     attributeLabel);
398         }
399     }
400 
401     /**
402      * Generates messages for the user based on the search results.
403      *
404      * <p>Messages are generated for the number of results, if the results exceed the result limit, and if the
405      * search was done using the primary keys for the data object.</p>
406      *
407      * @param searchCriteria map of search criteria that was used for the search
408      * @param searchResults list of result data objects from the search
409      * @param bounded whether the search was bounded
410      * @param searchResultsLimit maximum number of search results to return
411      */
412     protected void generateLookupResultsMessages(Map<String, String> searchCriteria, Collection<?> searchResults,
413             boolean bounded, Integer searchResultsLimit) {
414         MessageMap messageMap = GlobalVariables.getMessageMap();
415 
416         Long searchResultsSize = Long.valueOf(0);
417         if (searchResults instanceof CollectionIncomplete
418                 && ((CollectionIncomplete<?>) searchResults).getActualSizeIfTruncated() > 0) {
419             searchResultsSize = ((CollectionIncomplete<?>) searchResults).getActualSizeIfTruncated();
420         } else if (searchResults != null) {
421             searchResultsSize = Long.valueOf(searchResults.size());
422         }
423 
424         if (searchResultsSize == 0) {
425             messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
426                     RiceKeyConstants.INFO_LOOKUP_RESULTS_NONE_FOUND);
427         } else if (searchResultsSize == 1) {
428             messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
429                     RiceKeyConstants.INFO_LOOKUP_RESULTS_DISPLAY_ONE);
430         } else if (searchResultsSize > 1) {
431             boolean resultsExceedsLimit =
432                     bounded && (searchResultsLimit != null) && (searchResultsSize > searchResultsLimit);
433 
434             if (resultsExceedsLimit) {
435                 messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
436                         RiceKeyConstants.INFO_LOOKUP_RESULTS_EXCEEDS_LIMIT, searchResultsSize.toString(),
437                         searchResultsLimit.toString());
438             } else {
439                 messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
440                         RiceKeyConstants.INFO_LOOKUP_RESULTS_DISPLAY_ALL, searchResultsSize.toString());
441             }
442         }
443 
444         Boolean usingPrimaryKey = getLookupService().allPrimaryKeyValuesPresentAndNotWildcard(getDataObjectClass(),
445                 searchCriteria);
446 
447         if (usingPrimaryKey) {
448             List<String> pkNames = getLegacyDataAdapter().listPrimaryKeyFieldNames(getDataObjectClass());
449 
450             List<String> pkLabels = new ArrayList<String>();
451             for (String pkName : pkNames) {
452                 pkLabels.add(getDataDictionaryService().getAttributeLabel(getDataObjectClass(), pkName));
453             }
454 
455             messageMap.putInfoForSectionId(UifConstants.MessageKeys.LOOKUP_RESULT_MESSAGES,
456                     RiceKeyConstants.INFO_LOOKUP_RESULTS_USING_PRIMARY_KEY, StringUtils.join(pkLabels, ","));
457         }
458     }
459 
460     /**
461      * Sorts the given list of search results based on the lookup view's configured sort attributes.
462      *
463      * <p>First if the posted view exists we grab the sort attributes from it. This will take into account expressions
464      * that might have been configured on the sort attributes. If the posted view does not exist (because we did a
465      * search from a get request or form session storage is off), we get the sort attributes from the view that we
466      * will be rendered (and was initialized before controller call). However, expressions will not be evaluated yet,
467      * thus if expressions were configured we don't know the results and can not sort the list</p>
468      *
469      * @param form lookup form instance containing view information
470      * @param searchResults list of search results to sort
471      * @TODO: revisit this when we have a solution for the posted view problem
472      */
473     protected void sortSearchResults(LookupForm form, List<?> searchResults) {
474         List<String> defaultSortColumns = null;
475         boolean defaultSortAscending = true;
476 
477         if (form.getView() != null) {
478             defaultSortColumns = ((LookupView) form.getView()).getDefaultSortAttributeNames();
479             defaultSortAscending = ((LookupView) form.getView()).isDefaultSortAscending();
480         }
481 
482         boolean hasExpression = false;
483         if (defaultSortColumns != null) {
484             for (String sortColumn : defaultSortColumns) {
485                 if (sortColumn == null) {
486                     hasExpression = true;
487                 }
488             }
489         }
490 
491         if (hasExpression) {
492             defaultSortColumns = null;
493         }
494 
495         if ((defaultSortColumns != null) && (!defaultSortColumns.isEmpty())) {
496             BeanPropertyComparator comparator = new BeanPropertyComparator(defaultSortColumns, true);
497             if (defaultSortAscending) {
498                 Collections.sort(searchResults, comparator);
499             } else {
500                 Collections.sort(searchResults, Collections.reverseOrder(comparator));
501             }
502         }
503     }
504 
505     /**
506      * Performs a search against an {@link org.kuali.rice.krad.bo.ExternalizableBusinessObject} by invoking the
507      * module service
508      *
509      * @param searchCriteria map of criteria currently set
510      * @param unbounded indicates whether the complete result should be returned.  When set to false the result is
511      * limited (if necessary) to the max search result limit configured.
512      * @return list of result objects, possibly bounded
513      */
514     protected List<?> getSearchResultsForEBO(Map<String, String> searchCriteria, boolean unbounded) {
515         ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
516                 getDataObjectClass());
517 
518         BusinessObjectEntry ddEntry = eboModuleService.getExternalizableBusinessObjectDictionaryEntry(
519                 getDataObjectClass());
520 
521         Map<String, String> filteredFieldValues = new HashMap<String, String>();
522         for (String fieldName : searchCriteria.keySet()) {
523             if (ddEntry.getAttributeNames().contains(fieldName)) {
524                 filteredFieldValues.put(fieldName, searchCriteria.get(fieldName));
525             }
526         }
527 
528         Map<String, Object> translatedValues  = KRADUtils.coerceRequestParameterTypes(
529                 (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), filteredFieldValues);
530 
531         List<?> searchResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
532                 (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), (Map) translatedValues,
533                 unbounded);
534 
535         return searchResults;
536     }
537 
538     /**
539      * {@inheritDoc}
540      */
541     @Override
542     public Map<String, String> performClear(LookupForm form, Map<String, String> lookupCriteria) {
543         Map<String, String> clearedLookupCriteria = new HashMap<String, String>();
544 
545         Map<String, InputField> criteriaFieldMap = new HashMap<String, InputField>();
546         if (form.getView() != null) {
547             criteriaFieldMap = getCriteriaFieldsForValidation((LookupView) form.getView(), form);
548         }
549 
550         // fields marked as read only through the initial request should not be cleared
551         List<String> readOnlyFieldsList = form.getReadOnlyFieldsList();
552 
553         for (Map.Entry<String, String> searchKeyValue : lookupCriteria.entrySet()) {
554             String searchPropertyName = searchKeyValue.getKey();
555             String searchPropertyValue = searchKeyValue.getValue();
556 
557             InputField inputField = criteriaFieldMap.get(searchPropertyName);
558 
559             if (readOnlyFieldsList == null || !readOnlyFieldsList.contains(searchPropertyName)) {
560                 if (inputField != null && inputField.getDefaultValue() != null  ) {
561                     searchPropertyValue = inputField.getDefaultValue().toString();
562                 } else {
563                     searchPropertyValue = "";
564                 }
565             }
566 
567             clearedLookupCriteria.put(searchPropertyName, searchPropertyValue);
568         }
569 
570         return clearedLookupCriteria;
571     }
572 
573     /**
574      * {@inheritDoc}
575      */
576     @Override
577     public void buildReturnUrlForResult(Link returnLink, Object model) {
578         LookupForm lookupForm = (LookupForm) model;
579 
580         Map<String, Object> returnLinkContext = returnLink.getContext();
581         LookupView lookupView = returnLinkContext == null ? null : (LookupView) returnLinkContext
582                 .get(UifConstants.ContextVariableNames.VIEW);
583         Object dataObject = returnLinkContext == null ? null : returnLinkContext
584                 .get(UifConstants.ContextVariableNames.LINE);
585 
586         // don't render return link if the object is null or if the row is not returnable
587         if ((dataObject == null) || (!isResultReturnable(dataObject))) {
588             returnLink.setRender(false);
589 
590             return;
591         }
592 
593         String dataReturnValue = "true";
594         if (lookupForm.isReturnByScript()) {
595             Map<String, String> translatedKeyValues = getTranslatedReturnKeyValues(lookupView, lookupForm, dataObject);
596 
597             dataReturnValue = ScriptUtils.translateValue(translatedKeyValues);
598 
599             returnLink.setHref("#");
600         } else if (StringUtils.isBlank(returnLink.getHref())) {
601             String href = getReturnUrl(lookupView, lookupForm, dataObject);
602 
603             if (StringUtils.isNotBlank(href)) {
604                 returnLink.setHref(href);
605             } else {
606                 returnLink.setRender(false);
607                 return;
608             }
609 
610             String target = lookupForm.getReturnTarget();
611 
612             if (StringUtils.isNotBlank(target)) {
613                 returnLink.setTarget(target);
614             }
615         }
616 
617         // add data attribute for attaching event handlers on the return links
618         returnLink.addDataAttribute(UifConstants.DataAttributes.RETURN, dataReturnValue);
619 
620         // build return link title if not already set
621         if (StringUtils.isBlank(returnLink.getTitle())) {
622             String linkLabel = getConfigurationService().getPropertyValueAsString(
623                     KRADConstants.Lookup.TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
624 
625             Map<String, String> returnKeyValues = getReturnKeyValues(lookupView, lookupForm, dataObject);
626 
627             String title = KRADUtils.buildAttributeTitleString(linkLabel, getDataObjectClass(), returnKeyValues);
628             returnLink.setTitle(title);
629         }
630     }
631 
632     /**
633      * Determines whether a given data object that's returned as one of the lookup's results is considered returnable,
634      * which means that for single-value lookups, a "return value" link may be rendered, and for multiple
635      * value lookups, a checkbox is rendered.
636      *
637      * <p>Note that this can be part of an authorization mechanism, but not the complete authorization mechanism. The
638      * component that invoked the lookup/ lookup caller (e.g. document, nesting lookup, etc.) needs to check
639      * that the object that was passed to it was returnable as well because there are ways around this method
640      * (e.g. crafting a custom return URL).</p>
641      *
642      * @param dataObject an object from the search result set
643      * @return true if the row is returnable and false if it is not
644      */
645     protected boolean isResultReturnable(Object dataObject) {
646         return true;
647     }
648 
649     /**
650      * Builds the URL for returning the given data object result row.
651      *
652      * <p>Note return URL will only be built if a return location is specified on the lookup form</p>
653      *
654      * @param lookupView lookup view instance containing lookup configuration
655      * @param lookupForm lookup form instance containing the data
656      * @param dataObject data object instance for the current line and for which the return URL is being built
657      * @return String return URL or blank if URL cannot be built
658      */
659     protected String getReturnUrl(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
660         Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
661 
662         String href = "";
663         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
664             href = UrlFactory.parameterizeUrl(lookupForm.getReturnLocation(), props);
665         }
666 
667         return href;
668     }
669 
670     /**
671      * Builds up a {@code Properties} object that will be used to provide the request parameters for the
672      * return URL link
673      *
674      * @param lookupView lookup view instance containing lookup configuration
675      * @param lookupForm lookup form instance containing the data
676      * @param dataObject data object instance for the current line and for which the return URL is being built
677      * @return Properties instance containing request parameters for return URL
678      */
679     protected Properties getReturnUrlParameters(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
680         Properties props = new Properties();
681         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, KRADConstants.RETURN_METHOD_TO_CALL);
682 
683         if (StringUtils.isNotBlank(lookupForm.getReturnFormKey())) {
684             props.put(UifParameters.FORM_KEY, lookupForm.getReturnFormKey());
685         }
686 
687         props.put(KRADConstants.REFRESH_CALLER, lookupView.getId());
688         props.put(KRADConstants.REFRESH_DATA_OBJECT_CLASS, getDataObjectClass().getName());
689 
690         if (StringUtils.isNotBlank(lookupForm.getReferencesToRefresh())) {
691             props.put(UifParameters.REFERENCES_TO_REFRESH, lookupForm.getReferencesToRefresh());
692         }
693 
694         if (StringUtils.isNotBlank(lookupForm.getQuickfinderId())) {
695             props.put(UifParameters.QUICKFINDER_ID, lookupForm.getQuickfinderId());
696         }
697 
698         Map<String, String> returnKeyValues = getTranslatedReturnKeyValues(lookupView, lookupForm, dataObject);
699         props.putAll(returnKeyValues);
700 
701         return props;
702     }
703 
704     /**
705      * Returns a map of the configured return keys translated to their corresponding field conversion with
706      * the associated values.
707      *
708      * @param lookupView lookup view instance containing lookup configuration
709      * @param lookupForm lookup form instance containing the data
710      * @param dataObject data object instance
711      * @return Map<String, String> map of translated return key/value pairs
712      */
713     protected Map<String, String> getTranslatedReturnKeyValues(LookupView lookupView, LookupForm lookupForm,
714             Object dataObject) {
715         Map<String, String> translatedKeyValues = new HashMap<String, String>();
716 
717         Map<String, String> returnKeyValues = getReturnKeyValues(lookupView, lookupForm, dataObject);
718 
719         for (String returnKey : returnKeyValues.keySet()) {
720             String returnValue = returnKeyValues.get(returnKey);
721 
722             // get name of the property on the calling view to pass back the parameter value as
723             if (lookupForm.getFieldConversions().containsKey(returnKey)) {
724                 returnKey = lookupForm.getFieldConversions().get(returnKey);
725             }
726 
727             translatedKeyValues.put(returnKey, returnValue);
728         }
729 
730         return translatedKeyValues;
731     }
732 
733     /**
734      * Returns a map of the configured return keys with their selected values.
735      *
736      * @param lookupView lookup view instance containing lookup configuration
737      * @param lookupForm lookup form instance containing the data
738      * @param dataObject data object instance
739      * @return Map<String, String> map of return key/value pairs
740      */
741     protected Map<String, String> getReturnKeyValues(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
742         List<String> returnKeys;
743 
744         if (lookupForm.getFieldConversions() != null && !lookupForm.getFieldConversions().isEmpty()) {
745             returnKeys = new ArrayList<String>(lookupForm.getFieldConversions().keySet());
746         } else {
747             returnKeys = getLegacyDataAdapter().listPrimaryKeyFieldNames(getDataObjectClass());
748         }
749 
750         List<String> secureReturnKeys = lookupView.getAdditionalSecurePropertyNames();
751 
752         return KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, secureReturnKeys, dataObject);
753     }
754 
755     /**
756      * {@inheritDoc}
757      */
758     @Override
759     public void buildMaintenanceActionLink(Link actionLink, Object model, String maintenanceMethodToCall) {
760         LookupForm lookupForm = (LookupForm) model;
761 
762         Map<String, Object> actionLinkContext = actionLink.getContext();
763         Object dataObject = actionLinkContext == null ? null : actionLinkContext
764                 .get(UifConstants.ContextVariableNames.LINE);
765 
766         List<String> pkNames = getLegacyDataAdapter().listPrimaryKeyFieldNames(getDataObjectClass());
767 
768         // build maintenance link href if needed
769         if (StringUtils.isBlank(actionLink.getHref())) {
770             String href = getMaintenanceActionUrl(lookupForm, dataObject, maintenanceMethodToCall, pkNames);
771             if (StringUtils.isBlank(href)) {
772                 actionLink.setRender(false);
773 
774                 return;
775             }
776 
777             actionLink.setHref(href);
778         }
779 
780         // build action title if not set
781         if (StringUtils.isBlank(actionLink.getTitle())) {
782             String prependTitleText = actionLink.getLinkText() + " " +
783                     getDataDictionaryService().getDataDictionary().getDataObjectEntry(getDataObjectClass().getName())
784                             .getObjectLabel() + " " +
785                     getConfigurationService().getPropertyValueAsString(
786                             KRADConstants.Lookup.TITLE_ACTION_URL_PREPENDTEXT_PROPERTY);
787 
788             Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
789             String title = KRADUtils.buildAttributeTitleString(prependTitleText, getDataObjectClass(),
790                     primaryKeyValues);
791 
792             actionLink.setTitle(title);
793         }
794     }
795 
796     /**
797      * Generates a URL to perform a maintenance action on the given result data object.
798      *
799      * <p>Will build a URL containing keys of the data object to invoke the given maintenance action method
800      * within the maintenance controller</p>
801      * 
802      * @param lookupForm lookup form
803      * @param dataObject data object instance for the line to build the maintenance action link for
804      * @param methodToCall method name on the maintenance controller that should be invoked
805      * @param pkNames list of primary key field names for the data object whose key/value pairs will be added to
806      * the maintenance link
807      *
808      * @return String URL link for the maintenance action
809      */
810     protected String getMaintenanceActionUrl(LookupForm lookupForm, Object dataObject, String methodToCall,
811             List<String> pkNames) {
812         LookupView lookupView = (LookupView) lookupForm.getView();
813 
814         Properties props = new Properties();
815         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
816 
817         Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
818         for (String primaryKey : primaryKeyValues.keySet()) {
819             String primaryKeyValue = primaryKeyValues.get(primaryKey);
820 
821             props.put(primaryKey, primaryKeyValue);
822         }
823 
824         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
825             props.put(KRADConstants.RETURN_LOCATION_PARAMETER, lookupForm.getReturnLocation());
826         }
827 
828         props.put(UifParameters.DATA_OBJECT_CLASS_NAME, lookupForm.getDataObjectClassName());
829         props.put(UifParameters.VIEW_TYPE_NAME, UifConstants.ViewType.MAINTENANCE.name());
830 
831         String maintenanceMapping = KRADConstants.Maintenance.REQUEST_MAPPING_MAINTENANCE;
832         if (lookupView != null && StringUtils.isNotBlank(lookupView.getMaintenanceUrlMapping())) {
833             maintenanceMapping = lookupView.getMaintenanceUrlMapping();
834         }
835 
836         return UrlFactory.parameterizeUrl(maintenanceMapping, props);
837     }
838 
839     /**
840      * {@inheritDoc}
841      */
842     @Override
843     public void buildMultiValueSelectField(InputField selectField, Object model) {
844         LookupForm lookupForm = (LookupForm) model;
845 
846         Map<String, Object> selectFieldContext = selectField.getContext();
847         Object lineDataObject = selectFieldContext == null ? null : selectFieldContext
848                 .get(UifConstants.ContextVariableNames.LINE);
849         if (lineDataObject == null) {
850             throw new RuntimeException("Unable to get data object for line from component: " + selectField.getId());
851         }
852 
853         Control selectControl = selectField.getControl();
854         if ((selectControl != null) && (selectControl instanceof ValueConfiguredControl)) {
855             // get value for each field conversion from line and add to lineIdentifier
856             List<String> fromFieldNames = new ArrayList<String>(lookupForm.getFieldConversions().keySet());
857             // Per KULRICE-12125 we need to remove secure field names from this list.
858             fromFieldNames = new ArrayList<String>(
859                     KRADUtils.getPropertyKeyValuesFromDataObject(fromFieldNames, lineDataObject).keySet());
860 
861             Collections.sort(fromFieldNames);
862             lookupForm.setMultiValueReturnFields(fromFieldNames);
863             String lineIdentifier = "";
864             for (String fromFieldName : fromFieldNames) {
865                 Object fromFieldValue = ObjectPropertyUtils.getPropertyValue(lineDataObject, fromFieldName);
866 
867                 if (fromFieldValue != null) {
868                     lineIdentifier += fromFieldValue;
869                 }
870 
871                 lineIdentifier += ":";
872             }
873             lineIdentifier = StringUtils.removeEnd(lineIdentifier, ":");
874 
875             ((ValueConfiguredControl) selectControl).setValue(lineIdentifier);
876         }
877     }
878 
879     /**
880      * Determines if given data object has associated maintenance document that allows new or copy
881      * maintenance actions.
882      *
883      * @return boolean true if the maintenance new or copy action is allowed for the data object instance, false
884      *         otherwise
885      */
886     public boolean allowsMaintenanceNewOrCopyAction() {
887         boolean allowsNewOrCopy = false;
888 
889         String maintDocTypeName = getMaintenanceDocumentTypeName();
890         if (StringUtils.isNotBlank(maintDocTypeName)) {
891             allowsNewOrCopy = getDataObjectAuthorizationService().canCreate(getDataObjectClass(),
892                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
893         }
894 
895         return allowsNewOrCopy;
896     }
897 
898     /**
899      * Determines if given data object has associated maintenance document that allows edit maintenance
900      * actions.
901      * @param dataObject data object
902      *
903      * @return boolean true if the maintenance edit action is allowed for the data object instance, false otherwise
904      */
905     public boolean allowsMaintenanceEditAction(Object dataObject) {
906         boolean allowsEdit = false;
907 
908         String maintDocTypeName = getMaintenanceDocumentTypeName();
909         if (StringUtils.isNotBlank(maintDocTypeName)) {
910             allowsEdit = getDataObjectAuthorizationService().canMaintain(dataObject,
911                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
912         }
913 
914         return allowsEdit;
915     }
916 
917     /**
918      * Determines if given data object has associated maintenance document that allows delete maintenance
919      * actions.
920      * @param dataObject data object
921      *
922      * @return boolean true if the maintenance delete action is allowed for the data object instance, false otherwise
923      */
924     public boolean allowsMaintenanceDeleteAction(Object dataObject) {
925         boolean allowsMaintain = false;
926 
927         String maintDocTypeName = getMaintenanceDocumentTypeName();
928         if (StringUtils.isNotBlank(maintDocTypeName)) {
929             allowsMaintain = getDataObjectAuthorizationService().canMaintain(dataObject,
930                     GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
931         }
932 
933         boolean allowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion(getDataObjectClass());
934 
935         return allowsDelete && allowsMaintain;
936     }
937 
938     /**
939      * Returns the maintenance document type associated with the business object class or null if one does not exist.
940      *
941      * @return String representing the maintenance document type name
942      */
943     protected String getMaintenanceDocumentTypeName() {
944         DocumentDictionaryService dd = getDocumentDictionaryService();
945 
946         return dd.getMaintenanceDocumentTypeName(getDataObjectClass());
947     }
948 
949     /**
950      * Returns the criteria fields in a map keyed by the field property name.
951      *
952      * @param lookupView lookup view instance to pull criteria fields from
953      * @param form lookup form instance containing the lookup data
954      * @return map of criteria fields
955      */
956     protected Map<String, InputField> getCriteriaFieldsForValidation(LookupView lookupView, LookupForm form) {
957         Map<String, InputField> criteriaFieldMap = new HashMap<String, InputField>();
958 
959         if (lookupView.getCriteriaFields() == null) {
960             return criteriaFieldMap;
961         }
962 
963         List<InputField> fields = null;
964         if (lookupView.getCriteriaGroup().getItems().size() > 0) {
965             fields = ComponentUtils.getNestedContainerComponents(lookupView.getCriteriaGroup(), InputField.class);
966         } else if (lookupView.getCriteriaFields().size() > 0) {
967             // If criteriaGroup items are empty look to see if criteriaFields has any input components.
968             // This is to ensure that if initializeGroup hasn't been called on the view, the validations will still happen on criteriaFields
969             fields = ComponentUtils.getComponentsOfType(lookupView.getCriteriaFields(), InputField.class);
970         } else {
971             fields = new ArrayList<InputField>();
972         }
973         for (InputField field : fields) {
974             criteriaFieldMap.put(field.getPropertyName(), field);
975         }
976 
977         return criteriaFieldMap;
978     }
979 
980     /**
981      * {@inheritDoc}
982      */
983     @Override
984     public Class<?> getDataObjectClass() {
985         return this.dataObjectClass;
986     }
987 
988     /**
989      * {@inheritDoc}
990      */
991     @Override
992     public void setDataObjectClass(Class<?> dataObjectClass) {
993         this.dataObjectClass = dataObjectClass;
994     }
995 
996     protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
997         if (dataObjectAuthorizationService == null) {
998             this.dataObjectAuthorizationService = KRADServiceLocatorWeb.getDataObjectAuthorizationService();
999         }
1000         return dataObjectAuthorizationService;
1001     }
1002 
1003     public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
1004         this.dataObjectAuthorizationService = dataObjectAuthorizationService;
1005     }
1006 
1007     public DocumentDictionaryService getDocumentDictionaryService() {
1008         if (documentDictionaryService == null) {
1009             documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1010         }
1011         return documentDictionaryService;
1012     }
1013 
1014     public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1015         this.documentDictionaryService = documentDictionaryService;
1016     }
1017 
1018     protected LookupService getLookupService() {
1019         if (lookupService == null) {
1020             this.lookupService = KRADServiceLocatorWeb.getLookupService();
1021         }
1022         return lookupService;
1023     }
1024 
1025     public void setLookupService(LookupService lookupService) {
1026         this.lookupService = lookupService;
1027     }
1028 
1029     protected EncryptionService getEncryptionService() {
1030         if (encryptionService == null) {
1031             this.encryptionService = CoreApiServiceLocator.getEncryptionService();
1032         }
1033         return encryptionService;
1034     }
1035 
1036     public void setEncryptionService(EncryptionService encryptionService) {
1037         this.encryptionService = encryptionService;
1038     }
1039 
1040     /**
1041      * Creates a copy of this {@code LookupableImpl}.
1042      *
1043      * @return a copy of this {@code LookupableImpl}
1044      */
1045     public LookupableImpl copy() {
1046         LookupableImpl lookupableImplCopy = KRADUtils.createNewObjectFromClass(getClass());
1047 
1048         if (this.getDataObjectClass() != null) {
1049             lookupableImplCopy.setDataObjectClass(this.getDataObjectClass());
1050         }
1051 
1052         if (this.getConfigurationService() != null) {
1053             lookupableImplCopy.setConfigurationService(this.getConfigurationService());
1054         }
1055 
1056         if (this.getDataDictionaryService() != null) {
1057             lookupableImplCopy.setDataDictionaryService(this.getDataDictionaryService());
1058         }
1059 
1060         if (this.getDataObjectAuthorizationService() != null) {
1061             lookupableImplCopy.setDataObjectAuthorizationService(this.getDataObjectAuthorizationService());
1062         }
1063 
1064         if (this.getDataObjectService() != null) {
1065             lookupableImplCopy.setDataObjectService(this.getDataObjectService());
1066         }
1067 
1068         if (this.getDocumentDictionaryService() != null) {
1069             lookupableImplCopy.setDocumentDictionaryService(this.getDocumentDictionaryService());
1070         }
1071 
1072         if (this.getEncryptionService() != null) {
1073             lookupableImplCopy.setEncryptionService(this.getEncryptionService());
1074         }
1075 
1076         if (this.getExpressionEvaluatorFactory() != null) {
1077             lookupableImplCopy.setExpressionEvaluatorFactory(this.getExpressionEvaluatorFactory());
1078         }
1079 
1080         if (this.getLegacyDataAdapter() != null) {
1081             lookupableImplCopy.setLegacyDataAdapter(this.getLegacyDataAdapter());
1082         }
1083 
1084         if (this.getLookupService() != null) {
1085             lookupableImplCopy.setLookupService(this.getLookupService());
1086         }
1087 
1088         if (this.getViewDictionaryService() != null) {
1089             lookupableImplCopy.setViewDictionaryService(this.getViewDictionaryService());
1090         }
1091 
1092         return lookupableImplCopy;
1093     }
1094 }