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