001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.lookup;
017
018import org.apache.commons.lang.BooleanUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.encryption.EncryptionService;
022import org.kuali.rice.core.api.search.SearchOperator;
023import org.kuali.rice.core.api.util.RiceKeyConstants;
024import org.kuali.rice.core.api.util.type.TypeUtils;
025import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
026import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
027import org.kuali.rice.krad.datadictionary.DataObjectEntry;
028import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
029import org.kuali.rice.krad.service.DataObjectAuthorizationService;
030import org.kuali.rice.krad.service.DocumentDictionaryService;
031import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
032import org.kuali.rice.krad.service.LookupService;
033import org.kuali.rice.krad.service.ModuleService;
034import org.kuali.rice.krad.uif.UifConstants;
035import org.kuali.rice.krad.uif.UifParameters;
036import org.kuali.rice.krad.uif.UifPropertyPaths;
037import org.kuali.rice.krad.uif.control.Control;
038import org.kuali.rice.krad.uif.control.FilterableLookupCriteriaControl;
039import org.kuali.rice.krad.uif.control.FilterableLookupCriteriaControlPostData;
040import org.kuali.rice.krad.uif.control.HiddenControl;
041import org.kuali.rice.krad.uif.control.ValueConfiguredControl;
042import org.kuali.rice.krad.uif.element.Link;
043import org.kuali.rice.krad.uif.field.InputField;
044import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
045import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
046import org.kuali.rice.krad.uif.util.ComponentUtils;
047import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
048import org.kuali.rice.krad.uif.util.ScriptUtils;
049import org.kuali.rice.krad.util.BeanPropertyComparator;
050import org.kuali.rice.krad.util.ErrorMessage;
051import org.kuali.rice.krad.util.GlobalVariables;
052import org.kuali.rice.krad.util.KRADConstants;
053import org.kuali.rice.krad.util.KRADUtils;
054import org.kuali.rice.krad.util.MessageMap;
055import org.kuali.rice.krad.util.UrlFactory;
056
057import java.security.GeneralSecurityException;
058import java.util.ArrayList;
059import java.util.Collection;
060import java.util.Collections;
061import java.util.HashMap;
062import java.util.HashSet;
063import java.util.List;
064import java.util.Map;
065import java.util.Properties;
066import java.util.Set;
067import java.util.regex.Matcher;
068import java.util.regex.Pattern;
069
070/**
071 * View helper service that implements {@link Lookupable} and executes a search using the
072 * {@link org.kuali.rice.krad.service.LookupService}.
073 *
074 * @author Kuali Rice Team (rice.collab@kuali.org)
075 * @see LookupForm
076 * @see LookupView
077 * @see org.kuali.rice.krad.service.LookupService
078 */
079public class LookupableImpl extends ViewHelperServiceImpl implements Lookupable {
080    private static final long serialVersionUID = 1885161468871327740L;
081    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupableImpl.class);
082
083    private Class<?> dataObjectClass;
084
085    private transient DataObjectAuthorizationService dataObjectAuthorizationService;
086    private transient DocumentDictionaryService documentDictionaryService;
087    private transient LookupService lookupService;
088    private transient EncryptionService encryptionService;
089
090    /**
091     * {@inheritDoc}
092     */
093    @Override
094    public Collection<?> performSearch(LookupForm form, Map<String, String> searchCriteria, boolean bounded) {
095        // removed blank search values and decrypt any encrypted search values
096        Map<String, String> adjustedSearchCriteria = processSearchCriteria(form, searchCriteria);
097
098        boolean isValidCriteria = validateSearchParameters(form, adjustedSearchCriteria);
099        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}