001/**
002 * Copyright 2005-2016 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.beanutils.PropertyUtils;
019import org.apache.commons.lang.BooleanUtils;
020import org.apache.commons.lang.StringUtils;
021import org.kuali.rice.core.api.CoreApiServiceLocator;
022import org.kuali.rice.core.api.config.property.ConfigurationService;
023import org.kuali.rice.core.api.encryption.EncryptionService;
024import org.kuali.rice.core.api.search.SearchOperator;
025import org.kuali.rice.core.api.util.RiceKeyConstants;
026import org.kuali.rice.core.api.util.type.TypeUtils;
027import org.kuali.rice.kim.api.identity.Person;
028import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
029import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
030import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
031import org.kuali.rice.krad.service.DataObjectAuthorizationService;
032import org.kuali.rice.krad.service.DataObjectMetaDataService;
033import org.kuali.rice.krad.service.DocumentDictionaryService;
034import org.kuali.rice.krad.service.KRADServiceLocator;
035import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
036import org.kuali.rice.krad.service.LookupService;
037import org.kuali.rice.krad.service.ModuleService;
038import org.kuali.rice.krad.uif.UifConstants;
039import org.kuali.rice.krad.uif.UifParameters;
040import org.kuali.rice.krad.uif.control.Control;
041import org.kuali.rice.krad.uif.control.HiddenControl;
042import org.kuali.rice.krad.uif.control.ValueConfiguredControl;
043import org.kuali.rice.krad.uif.field.InputField;
044import org.kuali.rice.krad.uif.field.LinkField;
045import org.kuali.rice.krad.uif.field.LookupInputField;
046import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
047import org.kuali.rice.krad.uif.util.ComponentUtils;
048import org.kuali.rice.krad.uif.util.LookupInquiryUtils;
049import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
050import org.kuali.rice.krad.uif.view.LookupView;
051import org.kuali.rice.krad.uif.view.View;
052import org.kuali.rice.krad.util.BeanPropertyComparator;
053import org.kuali.rice.krad.util.GlobalVariables;
054import org.kuali.rice.krad.util.KRADConstants;
055import org.kuali.rice.krad.util.KRADUtils;
056import org.kuali.rice.krad.util.ObjectUtils;
057import org.kuali.rice.krad.util.UrlFactory;
058import org.kuali.rice.krad.web.form.LookupForm;
059
060import java.security.GeneralSecurityException;
061import java.util.ArrayList;
062import java.util.Collection;
063import java.util.Collections;
064import java.util.HashMap;
065import java.util.List;
066import java.util.Map;
067import java.util.Properties;
068
069/**
070 * Default implementation of <code>Lookupable</code>
071 *
072 * @author Kuali Rice Team (rice.collab@kuali.org)
073 */
074public class LookupableImpl extends ViewHelperServiceImpl implements Lookupable {
075    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupableImpl.class);
076
077    private Class<?> dataObjectClass;
078
079    private Map<String, String> parameters;
080    private List<String> defaultSortAttributeNames;
081
082    // TODO delyea: where to take into account the sort ascending value (old KNS appeared to ignore?)
083    private boolean sortAscending;
084
085    private Map<String, String> fieldConversions;
086    private List<String> readOnlyFieldsList;
087
088    private transient ConfigurationService configurationService;
089    private transient DataObjectAuthorizationService dataObjectAuthorizationService;
090    private transient DataObjectMetaDataService dataObjectMetaDataService;
091    private transient DocumentDictionaryService documentDictionaryService;
092    private transient LookupService lookupService;
093    private transient EncryptionService encryptionService;
094
095    /**
096     * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#populateViewFromRequestParameters(org.kuali.rice.krad.uif.view.View,
097     *      java.util.Map)
098     */
099    @Override
100    public void populateViewFromRequestParameters(View view, Map<String, String> parameters) {
101        super.populateViewFromRequestParameters(view, parameters);
102        /* On the old Lookupable and LookupableHelperService in KNS the parameters list used to have multipart form
103           * data in it where it may not in the new KRAD. See PojoFormBase.populate() method for more information
104           */
105        setParameters(parameters);
106    }
107
108    /**
109     * Initialization of Lookupable requires that the business object class be set for the {@link
110     * #initializeAttributeFieldFromDataDictionary(View, org.kuali.rice.krad.uif.field.InputField)} method
111     *
112     * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#performInitialization(org.kuali.rice.krad.uif.view.View, java.lang.Object)
113     */
114    @Override
115    public void performInitialization(View view, Object model) {
116        if (!LookupView.class.isAssignableFrom(view.getClass())) {
117            throw new IllegalArgumentException(
118                    "View class '" + view.getClass() + " is not assignable from the '" + LookupView.class + "'");
119        }
120
121        LookupView lookupView = (LookupView) view;
122        initializeLookupViewHelperService(lookupView);
123
124        super.performInitialization(view, model);
125    }
126
127    /**
128     * Initializes properties on this lookupable from the <code>LookupView</code>
129     *
130     * @param lookupView - lookup view instance
131     */
132    protected void initializeLookupViewHelperService(LookupView lookupView) {
133        setDefaultSortAttributeNames(lookupView.getDefaultSortAttributeNames());
134        setSortAscending(lookupView.isDefaultSortAscending());
135        setDataObjectClass(lookupView.getDataObjectClassName());
136    }
137
138    /**
139     * @see org.kuali.rice.krad.lookup.Lookupable#initSuppressAction(org.kuali.rice.krad.web.form.LookupForm)
140     */
141    @Override
142    public void initSuppressAction(LookupForm lookupForm) {
143        LookupViewAuthorizerBase lookupAuthorizer = (LookupViewAuthorizerBase) lookupForm.getView().getAuthorizer();
144        Person user = GlobalVariables.getUserSession().getPerson();
145        ((LookupView) lookupForm.getView()).setSuppressActions(!lookupAuthorizer.canInitiateDocument(lookupForm, user));
146    }
147
148    /**
149     * @see org.kuali.rice.krad.lookup.Lookupable#performSearch
150     */
151    @Override
152    public Collection<?> performSearch(LookupForm form, Map<String, String> searchCriteria, boolean bounded) {
153        Collection<?> displayList;
154
155        LookupUtils.preprocessDateFields(searchCriteria);
156
157        // TODO: force uppercase will be done in binding at some point
158        displayList = getSearchResults(form, LookupUtils.forceUppercase(getDataObjectClass(), searchCriteria),
159                !bounded);
160
161        // TODO delyea - is this the best way to set that the entire set has a returnable row?
162        List<String> pkNames = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
163        Person user = GlobalVariables.getUserSession().getPerson();
164
165        for (Object object : displayList) {
166            if (isResultReturnable(object)) {
167                form.setAtLeastOneRowReturnable(true);
168            }
169        }
170
171        return displayList;
172    }
173
174    protected List<?> getSearchResults(LookupForm form, Map<String, String> searchCriteria, boolean unbounded) {
175        List<?> searchResults;
176
177        // removed blank search values and decrypt any encrypted search values
178        Map<String, String> nonBlankSearchCriteria = processSearchCriteria(form, searchCriteria);
179
180        boolean searchUsingOnlyPrimaryKeyValues =
181                getLookupService().allPrimaryKeyValuesPresentAndNotWildcard(getDataObjectClass(), searchCriteria);
182
183        // if this class is an EBO, just call the module service to get the results
184        if (ExternalizableBusinessObject.class.isAssignableFrom(getDataObjectClass())) {
185            return getSearchResultsForEBO(nonBlankSearchCriteria, unbounded);
186        }
187
188        // if any of the properties refer to an embedded EBO, call the EBO
189        // lookups first and apply to the local lookup
190        try {
191            if (LookupUtils.hasExternalBusinessObjectProperty(getDataObjectClass(), nonBlankSearchCriteria)) {
192                Map<String, String> eboSearchCriteria = adjustCriteriaForNestedEBOs(nonBlankSearchCriteria, unbounded);
193
194                if (LOG.isDebugEnabled()) {
195                    LOG.debug("Passing these results into the lookup service: " + eboSearchCriteria);
196                }
197
198                // add those results as criteria run the normal search (but with the EBO criteria added)
199                searchResults = (List<?>) getLookupService()
200                        .findCollectionBySearchHelper(getDataObjectClass(), eboSearchCriteria, unbounded);
201            } else {
202                searchResults = (List<?>) getLookupService()
203                        .findCollectionBySearchHelper(getDataObjectClass(), nonBlankSearchCriteria, unbounded);
204            }
205        } catch (IllegalAccessException e) {
206            LOG.error("Error trying to perform search", e);
207            throw new RuntimeException("Error trying to perform search", e);
208        } catch (InstantiationException e1) {
209            LOG.error("Error trying to perform search", e1);
210            throw new RuntimeException("Error trying to perform search", e1);
211        }
212
213        if (searchResults == null) {
214            searchResults = new ArrayList<Object>();
215        }
216
217        // sort list if default sort column given
218        List<String> defaultSortColumns = getDefaultSortAttributeNames();
219        if ((defaultSortColumns != null) && (defaultSortColumns.size() > 0)) {
220            Collections.sort(searchResults, new BeanPropertyComparator(defaultSortColumns, true));
221        }
222
223        return searchResults;
224    }
225
226    protected Map<String, String> processSearchCriteria(LookupForm lookupForm, Map<String, String> searchCriteria) {
227        Map<String, InputField> criteriaFields = getCriteriaFieldsForValidation((LookupView) lookupForm.getView(),
228                lookupForm);
229
230        Map<String, String> nonBlankSearchCriteria = new HashMap<String, String>();
231        for (String fieldName : searchCriteria.keySet()) {
232            String fieldValue = searchCriteria.get(fieldName);
233
234            // don't add hidden criteria
235            LookupView lookupView = (LookupView) lookupForm.getView();
236            InputField inputField = criteriaFields.get(fieldName);
237            if (inputField.getControl() instanceof HiddenControl) {
238                continue;
239            }
240
241            // only add criteria if non blank
242            if (StringUtils.isNotBlank(fieldValue)) {
243                if (fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
244                    String encryptedValue = StringUtils.removeEnd(fieldValue, EncryptionService.ENCRYPTION_POST_PREFIX);
245                    try {
246                        if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
247                            fieldValue = getEncryptionService().decrypt(encryptedValue);
248                        }
249                    } catch (GeneralSecurityException e) {
250                        LOG.error("Error decrypting value for business object class " + getDataObjectClass() +
251                                " attribute " + fieldName, e);
252                        throw new RuntimeException(
253                                "Error decrypting value for business object class " + getDataObjectClass() +
254                                        " attribute " + fieldName, e);
255                    }
256                }
257
258                nonBlankSearchCriteria.put(fieldName, fieldValue);
259            }
260        }
261
262        return nonBlankSearchCriteria;
263    }
264
265    protected List<?> getSearchResultsForEBO(Map<String, String> searchCriteria, boolean unbounded) {
266        ModuleService eboModuleService =
267                KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(getDataObjectClass());
268        BusinessObjectEntry ddEntry =
269                eboModuleService.getExternalizableBusinessObjectDictionaryEntry(getDataObjectClass());
270
271        Map<String, String> filteredFieldValues = new HashMap<String, String>();
272        for (String fieldName : searchCriteria.keySet()) {
273            if (ddEntry.getAttributeNames().contains(fieldName)) {
274                filteredFieldValues.put(fieldName, searchCriteria.get(fieldName));
275            }
276        }
277
278        List<?> searchResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
279                (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), (Map) filteredFieldValues,
280                unbounded);
281
282        return searchResults;
283    }
284
285    protected Map<String, String> adjustCriteriaForNestedEBOs(Map<String, String> searchCriteria,
286            boolean unbounded) throws InstantiationException, IllegalAccessException {
287        if (LOG.isDebugEnabled()) {
288            LOG.debug("has EBO reference: " + getDataObjectClass());
289            LOG.debug("properties: " + searchCriteria);
290        }
291
292        // remove the EBO criteria
293        Map<String, String> nonEboFieldValues =
294                LookupUtils.removeExternalizableBusinessObjectFieldValues(getDataObjectClass(), searchCriteria);
295        if (LOG.isDebugEnabled()) {
296            LOG.debug("Non EBO properties removed: " + nonEboFieldValues);
297        }
298
299        // get the list of EBO properties attached to this object
300        List<String> eboPropertyNames =
301                LookupUtils.getExternalizableBusinessObjectProperties(getDataObjectClass(), searchCriteria);
302        if (LOG.isDebugEnabled()) {
303            LOG.debug("EBO properties: " + eboPropertyNames);
304        }
305
306        // loop over those properties
307        for (String eboPropertyName : eboPropertyNames) {
308            // extract the properties as known to the EBO
309            Map<String, String> eboFieldValues =
310                    LookupUtils.getExternalizableBusinessObjectFieldValues(eboPropertyName, searchCriteria);
311            if (LOG.isDebugEnabled()) {
312                LOG.debug("EBO properties for master EBO property: " + eboPropertyName);
313                LOG.debug("properties: " + eboFieldValues);
314            }
315
316            // run search against attached EBO's module service
317            ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
318                    LookupUtils.getExternalizableBusinessObjectClass(getDataObjectClass(), eboPropertyName));
319
320            // KULRICE-4401 made eboResults an empty list and only filled if
321            // service is found.
322            List<?> eboResults = Collections.emptyList();
323            if (eboModuleService != null) {
324                eboResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
325                        LookupUtils.getExternalizableBusinessObjectClass(getDataObjectClass(), eboPropertyName),
326                        (Map) eboFieldValues, unbounded);
327            } else {
328                LOG.debug("EBO ModuleService is null: " + eboPropertyName);
329            }
330            // get the mapping/relationship between the EBO object and it's
331            // parent object
332            // use that to adjust the searchCriteria
333
334            // get the parent property type
335            Class<?> eboParentClass;
336            String eboParentPropertyName;
337            if (ObjectUtils.isNestedAttribute(eboPropertyName)) {
338                eboParentPropertyName = StringUtils.substringBeforeLast(eboPropertyName, ".");
339                try {
340                    eboParentClass =
341                            PropertyUtils.getPropertyType(getDataObjectClass().newInstance(), eboParentPropertyName);
342                } catch (Exception ex) {
343                    throw new RuntimeException("Unable to create an instance of the business object class: " +
344                            getDataObjectClass().getName(), ex);
345                }
346            } else {
347                eboParentClass = getDataObjectClass();
348                eboParentPropertyName = null;
349            }
350
351            if (LOG.isDebugEnabled()) {
352                LOG.debug("determined EBO parent class/property name: " + eboParentClass + "/" + eboParentPropertyName);
353            }
354
355            // look that up in the DD (BOMDS)
356            // find the appropriate relationship
357            // CHECK THIS: what if eboPropertyName is a nested attribute -
358            // need to strip off the eboParentPropertyName if not null
359            RelationshipDefinition rd =
360                    getDataObjectMetaDataService().getDictionaryRelationship(eboParentClass, eboPropertyName);
361            if (LOG.isDebugEnabled()) {
362                LOG.debug("Obtained RelationshipDefinition for " + eboPropertyName);
363                LOG.debug(rd);
364            }
365
366            // copy the needed properties (primary only) to the field values KULRICE-4446 do
367            // so only if the relationship definition exists
368            // NOTE: this will work only for single-field PK unless the ORM
369            // layer is directly involved
370            // (can't make (field1,field2) in ( (v1,v2),(v3,v4) ) style
371            // queries in the lookup framework
372            if (ObjectUtils.isNotNull(rd)) {
373                if (rd.getPrimitiveAttributes().size() > 1) {
374                    throw new RuntimeException(
375                            "EBO Links don't work for relationships with multiple-field primary keys.");
376                }
377                String boProperty = rd.getPrimitiveAttributes().get(0).getSourceName();
378                String eboProperty = rd.getPrimitiveAttributes().get(0).getTargetName();
379                StringBuffer boPropertyValue = new StringBuffer();
380
381                // loop over the results, making a string that the lookup
382                // DAO will convert into an
383                // SQL "IN" clause
384                for (Object ebo : eboResults) {
385                    if (boPropertyValue.length() != 0) {
386                        boPropertyValue.append(SearchOperator.OR.op());
387                    }
388                    try {
389                        boPropertyValue.append(PropertyUtils.getProperty(ebo, eboProperty).toString());
390                    } catch (Exception ex) {
391                        LOG.warn("Unable to get value for " + eboProperty + " on " + ebo);
392                    }
393                }
394
395                if (eboParentPropertyName == null) {
396                    // non-nested property containing the EBO
397                    nonEboFieldValues.put(boProperty, boPropertyValue.toString());
398                } else {
399                    // property nested within the main searched-for BO that
400                    // contains the EBO
401                    nonEboFieldValues.put(eboParentPropertyName + "." + boProperty, boPropertyValue.toString());
402                }
403            }
404        }
405
406        return nonEboFieldValues;
407    }
408
409    /**
410     * @see org.kuali.rice.krad.lookup.Lookupable#performClear
411     */
412    @Override
413    public Map<String, String> performClear(LookupForm form, Map<String, String> searchCriteria) {
414        Map<String, InputField> criteriaFieldMap = getCriteriaFieldsForValidation((LookupView) form.getView(), form);
415        Map<String, String> clearedSearchCriteria = new HashMap<String, String>();
416        for (Map.Entry<String, String> searchKeyValue : searchCriteria.entrySet()) {
417            String searchPropertyName = searchKeyValue.getKey();
418
419            InputField inputField = criteriaFieldMap.get(searchPropertyName);
420            if (inputField != null) {
421                // TODO: check secure fields
422//                                if (field.isSecure()) {
423//                    field.setSecure(false);
424//                    field.setDisplayMaskValue(null);
425//                    field.setEncryptedValue(null);
426//                }
427
428                // TODO: need formatting on default value and make sure it works when control converts
429                // from checkbox to radio
430                clearedSearchCriteria.put(searchPropertyName, inputField.getDefaultValue());
431            } else {
432                throw new RuntimeException("Invalid search field sent for property name: " + searchPropertyName);
433            }
434        }
435
436        return clearedSearchCriteria;
437    }
438
439    /**
440     * @see org.kuali.rice.krad.lookup.Lookupable#validateSearchParameters
441     */
442    @Override
443    public boolean validateSearchParameters(LookupForm form, Map<String, String> searchCriteria) {
444        boolean valid = true;
445
446        if (!getViewDictionaryService().isLookupable(getDataObjectClass())) {
447            throw new RuntimeException("Lookup not defined for data object " + getDataObjectClass());
448        }
449
450        Map<String, InputField> criteriaFields = getCriteriaFieldsForValidation((LookupView) form.getView(), form);
451
452        // validate required
453        // TODO: this will be done by the uif validation service at some point
454        for (Map.Entry<String, String> searchKeyValue : searchCriteria.entrySet()) {
455            String searchPropertyName = searchKeyValue.getKey();
456            String searchPropertyValue = searchKeyValue.getValue();
457
458            LookupView lookupView = (LookupView) form.getView();
459            InputField inputField = criteriaFields.get(searchPropertyName);
460            if (inputField != null) {
461                if (StringUtils.isBlank(searchPropertyValue) && BooleanUtils.isTrue(inputField.getRequired())) {
462                    GlobalVariables.getMessageMap()
463                            .putError(inputField.getPropertyName(), RiceKeyConstants.ERROR_REQUIRED,
464                                    inputField.getLabel());
465                }
466
467                validateSearchParameterWildcardAndOperators(inputField, searchPropertyValue);
468            } else {
469                throw new RuntimeException("Invalid search field sent for property name: " + searchPropertyName);
470            }
471        }
472
473        if (GlobalVariables.getMessageMap().hasErrors()) {
474            valid = false;
475        }
476
477        return valid;
478    }
479
480    protected Map<String, InputField> getCriteriaFieldsForValidation(LookupView lookupView, LookupForm form) {
481        Map<String, InputField> criteriaFieldMap = new HashMap<String, InputField>();
482
483        // TODO; need hooks for code generated components and also this doesn't have lifecycle which
484        // could change fields
485        List<InputField> fields = ComponentUtils.getComponentsOfTypeDeep(lookupView.getCriteriaFields(),
486                InputField.class);
487        for (InputField field : fields) {
488            criteriaFieldMap.put(field.getPropertyName(), field);
489        }
490
491        return criteriaFieldMap;
492    }
493
494    /**
495     * Validates that any wildcards contained within the search value are valid wilcards and allowed for the
496     * property type for which the field is searching
497     *
498     * @param inputField - attribute field instance for the field that is being searched
499     * @param searchPropertyValue - value given for field to search for
500     */
501    protected void validateSearchParameterWildcardAndOperators(InputField inputField,
502            String searchPropertyValue) {
503        if (StringUtils.isBlank(searchPropertyValue))
504            return;
505
506        // make sure a wildcard/operator is in the value
507        boolean found = false;
508        for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
509            String queryCharacter = op.op();
510
511            if (searchPropertyValue.contains(queryCharacter)) {
512                found = true;
513            }
514        }
515
516        if (!found) {
517            return;
518        }
519
520        String attributeLabel = inputField.getLabel();
521        if ((LookupInputField.class.isAssignableFrom(inputField.getClass())) &&
522                (((LookupInputField) inputField).isTreatWildcardsAndOperatorsAsLiteral())) {
523            Object dataObjectExample = null;
524            try {
525                dataObjectExample = getDataObjectClass().newInstance();
526            } catch (Exception e) {
527                LOG.error("Exception caught instantiating " + getDataObjectClass().getName(), e);
528                throw new RuntimeException("Cannot instantiate " + getDataObjectClass().getName(), e);
529            }
530
531            Class<?> propertyType =
532                    ObjectPropertyUtils.getPropertyType(getDataObjectClass(), inputField.getPropertyName());
533            if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) ||
534                    TypeUtils.isTemporalClass(propertyType)) {
535                GlobalVariables.getMessageMap().putError(inputField.getPropertyName(),
536                        RiceKeyConstants.ERROR_WILDCARDS_AND_OPERATORS_NOT_ALLOWED_ON_FIELD, attributeLabel);
537            }
538
539            if (TypeUtils.isStringClass(propertyType)) {
540                GlobalVariables.getMessageMap().putInfo(inputField.getPropertyName(),
541                        RiceKeyConstants.INFO_WILDCARDS_AND_OPERATORS_TREATED_LITERALLY, attributeLabel);
542            }
543        } else {
544            if (getDataObjectAuthorizationService()
545                    .attributeValueNeedsToBeEncryptedOnFormsAndLinks(getDataObjectClass(),
546                            inputField.getPropertyName())) {
547                if (!searchPropertyValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
548                    // encrypted values usually come from the DB, so we don't
549                    // need to filter for wildcards
550                    // wildcards are not allowed on restricted fields, because
551                    // they are typically encrypted, and wildcard searches cannot be performed without
552                    // decrypting every row, which is currently not supported by KRAD
553
554                    GlobalVariables.getMessageMap()
555                            .putError(inputField.getPropertyName(), RiceKeyConstants.ERROR_SECURE_FIELD,
556                                    attributeLabel);
557                }
558            }
559        }
560    }
561
562    /**
563     * @see org.kuali.rice.krad.lookup.Lookupable#getReturnUrlForResults
564     */
565    public void getReturnUrlForResults(LinkField returnLinkField, Object model) {
566        LookupForm lookupForm = (LookupForm) model;
567        LookupView lookupView = (LookupView) returnLinkField.getContext().get(UifConstants.ContextVariableNames.VIEW);
568
569        Object dataObject = returnLinkField.getContext().get(UifConstants.ContextVariableNames.LINE);
570
571        // don't render return link if the object is null or if the row is not returnable
572        if ((dataObject == null) || (!isResultReturnable(dataObject))) {
573            returnLinkField.setRender(false);
574            return;
575        }
576
577        // build return link href
578        String href = getReturnUrl(lookupView, lookupForm, dataObject);
579        if (StringUtils.isBlank(href)) {
580            returnLinkField.setRender(false);
581            return;
582        }
583        // TODO: need to handle returning anchor
584        returnLinkField.setHrefText(href);
585
586        // build return link label and title
587        String linkLabel = getConfigurationService().getPropertyValueAsString(
588                        KRADConstants.Lookup.TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
589        returnLinkField.setLinkLabel(linkLabel);
590
591        List<String> returnKeys = getReturnKeys(lookupView, lookupForm, dataObject);
592        Map<String, String> returnKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, dataObject);
593
594        String title = LookupInquiryUtils.getLinkTitleText(linkLabel, getDataObjectClass(), returnKeyValues);
595        returnLinkField.setTitle(title);
596
597        // Add the return target if it is set
598        String returnTarget = lookupView.getReturnTarget();
599        if (returnTarget != null) {
600            returnLinkField.setTarget(returnTarget);
601
602            //  Add the close script if lookup is in a light box
603            if (!returnTarget.equals("_self")) {
604
605                // Add the return script if the returnByScript flag is set
606                if (lookupView.isReturnByScript()) {
607                    Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
608
609                    StringBuilder script = new StringBuilder("e.preventDefault();");
610                    for (String returnField : lookupForm.getFieldConversions().values()) {
611                        if (props.containsKey(returnField)) {
612                            Object fieldName = returnField.replace("'", "\\'");
613                            Object value = props.get(returnField);
614                            script = script.append("returnLookupResultByScript(\"" + returnField + "\", '" + value + "');");
615                        }
616                    }
617                    returnLinkField.setOnClickScript(script.append("closeLightbox();").toString());
618                }  else{
619                    // Close the light box if return target is not _self or _parent
620                    returnLinkField.setOnClickScript("e.preventDefault();closeLightbox();createLoading(true);returnLookupResultReload(jQuery(this));");
621                }
622            }
623        } else {
624            // If no return target is set return in same frame
625            // This is to insure that non light box lookups return correctly
626            returnLinkField.setTarget("_self");
627        }
628    }
629
630    /**
631     * Builds the URL for returning the given data object result row
632     *
633     * <p>
634     * Note return URL will only be built if a return location is specified on the <code>LookupForm</code>
635     * </p>
636     *
637     * @param lookupView - lookup view instance containing lookup configuration
638     * @param lookupForm - lookup form instance containing the data
639     * @param dataObject - data object instance for the current line and for which the return URL is being built
640     * @return String return URL or blank if URL cannot be built
641     */
642    protected String getReturnUrl(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
643        Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
644
645        String href = "";
646        if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
647            href = UrlFactory.parameterizeUrl(lookupForm.getReturnLocation(), props);
648        }
649
650        return href;
651    }
652
653    /**
654     * Builds up a <code>Properties</code> object that will be used to provide the request parameters for the
655     * return URL link
656     *
657     * @param lookupView - lookup view instance containing lookup configuration
658     * @param lookupForm - lookup form instance containing the data
659     * @param dataObject - data object instance for the current line and for which the return URL is being built
660     * @return Properties instance containing request parameters for return URL
661     */
662    protected Properties getReturnUrlParameters(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
663        Properties props = new Properties();
664        props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, KRADConstants.RETURN_METHOD_TO_CALL);
665
666        if (StringUtils.isNotBlank(lookupForm.getReturnFormKey())) {
667            props.put(UifParameters.FORM_KEY, lookupForm.getReturnFormKey());
668        }
669
670        props.put(KRADConstants.REFRESH_CALLER, lookupView.getId());
671        props.put(KRADConstants.REFRESH_DATA_OBJECT_CLASS, getDataObjectClass().getName());
672
673        if (StringUtils.isNotBlank(lookupForm.getDocNum())) {
674            props.put(UifParameters.DOC_NUM, lookupForm.getDocNum());
675        }
676
677        if (StringUtils.isNotBlank(lookupForm.getReferencesToRefresh())) {
678            props.put(KRADConstants.REFERENCES_TO_REFRESH, lookupForm.getReferencesToRefresh());
679        }
680
681        List<String> returnKeys = getReturnKeys(lookupView, lookupForm, dataObject);
682        Map<String, String> returnKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, dataObject);
683
684        for (String returnKey : returnKeyValues.keySet()) {
685            String returnValue = returnKeyValues.get(returnKey);
686            if (lookupForm.getFieldConversions().containsKey(returnKey)) {
687                returnKey = lookupForm.getFieldConversions().get(returnKey);
688            }
689
690            props.put(returnKey, returnValue);
691        }
692
693        return props;
694    }
695
696    /**
697     * Returns the configured return key property names or if not configured defaults to the primary keys
698     * for the data object class
699     *
700     * @return List<String> property names which should be passed back on the return URL
701     */
702    protected List<String> getReturnKeys(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
703        List<String> returnKeys;
704        if (lookupForm.getFieldConversions() != null && !lookupForm.getFieldConversions().isEmpty()) {
705            returnKeys = new ArrayList<String>(lookupForm.getFieldConversions().keySet());
706        } else {
707            returnKeys = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
708        }
709
710        return returnKeys;
711    }
712
713    /**
714     * @see org.kuali.rice.krad.lookup.Lookupable#getMaintenanceActionLink
715     */
716    public void getMaintenanceActionLink(LinkField actionLinkField, Object model, String maintenanceMethodToCall) {
717        LookupForm lookupForm = (LookupForm) model;
718        LookupView lookupView = (LookupView) actionLinkField.getContext().get(UifConstants.ContextVariableNames.VIEW);
719        Object dataObject = actionLinkField.getContext().get(UifConstants.ContextVariableNames.LINE);
720
721        List<String> pkNames = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
722
723        // build maintenance link href
724        String href = getActionUrlHref(lookupForm, dataObject, maintenanceMethodToCall, pkNames);
725        if (StringUtils.isBlank(href)) {
726            actionLinkField.setRender(false);
727            return;
728        }
729        // TODO: need to handle returning anchor
730        actionLinkField.setHrefText(href);
731
732        // build action title
733        String prependTitleText = actionLinkField.getLinkLabel() + " " +
734                getDataDictionaryService().getDataDictionary().getDataObjectEntry(getDataObjectClass().getName())
735                        .getObjectLabel() + " " +
736                getConfigurationService().getPropertyValueAsString(
737                        KRADConstants.Lookup.TITLE_ACTION_URL_PREPENDTEXT_PROPERTY);
738
739        Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
740        String title = LookupInquiryUtils.getLinkTitleText(prependTitleText, getDataObjectClass(), primaryKeyValues);
741        actionLinkField.setTitle(title);
742        // TODO : do not hardcode the _self string
743        actionLinkField.setTarget("_self");
744        lookupForm.setAtLeastOneRowHasActions(true);
745    }
746
747    /**
748     * Generates a URL to perform a maintenance action on the given result data object
749     *
750     * <p>
751     * Will build a URL containing keys of the data object to invoke the given maintenance action method
752     * within the maintenance controller
753     * </p>
754     *
755     * @param dataObject - data object instance for the line to build the maintenance action link for
756     * @param methodToCall - method name on the maintenance controller that should be invoked
757     * @param pkNames - list of primary key field names for the data object whose key/value pairs will be added to
758     * the maintenance link
759     * @return String URL link for the maintenance action
760     */
761    protected String getActionUrlHref(LookupForm lookupForm, Object dataObject, String methodToCall,
762            List<String> pkNames) {
763        LookupView lookupView = (LookupView) lookupForm.getView();
764
765        Properties props = new Properties();
766        props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
767
768        Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
769        for (String primaryKey : primaryKeyValues.keySet()) {
770            String primaryKeyValue = primaryKeyValues.get(primaryKey);
771
772            props.put(primaryKey, primaryKeyValue);
773        }
774
775        if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
776            props.put(KRADConstants.RETURN_LOCATION_PARAMETER, lookupForm.getReturnLocation());
777        }
778
779        props.put(UifParameters.DATA_OBJECT_CLASS_NAME, lookupForm.getDataObjectClassName());
780        props.put(UifParameters.VIEW_TYPE_NAME, UifConstants.ViewType.MAINTENANCE.name());
781
782        String maintenanceMapping = KRADConstants.Maintenance.REQUEST_MAPPING_MAINTENANCE;
783        if (StringUtils.isNotBlank(lookupView.getMaintenanceUrlMapping())) {
784            maintenanceMapping = lookupView.getMaintenanceUrlMapping();
785        }
786
787        return UrlFactory.parameterizeUrl(maintenanceMapping, props);
788    }
789
790    /**
791     * Sets the value for the attribute field control to contain the field conversion values for the line
792     *
793     * @see org.kuali.rice.krad.lookup.LookupableImpl#setMultiValueLookupSelect
794     */
795    @Override
796    public void setMultiValueLookupSelect(InputField selectField, Object model) {
797        LookupForm lookupForm = (LookupForm) model;
798        Object lineDataObject = selectField.getContext().get(UifConstants.ContextVariableNames.LINE);
799        if (lineDataObject == null) {
800            throw new RuntimeException("Unable to get data object for line from component: " + selectField.getId());
801        }
802
803        Control selectControl = ((InputField) selectField).getControl();
804        if ((selectControl != null) && (selectControl instanceof ValueConfiguredControl)) {
805            String lineIdentifier = "";
806
807            // get value for each field conversion from line and add to lineIdentifier
808            Map<String, String> fieldConversions = lookupForm.getFieldConversions();
809            List<String> fromFieldNames = new ArrayList<String>(fieldConversions.keySet());
810            Collections.sort(fromFieldNames);
811            for (String fromFieldName : fromFieldNames) {
812                Object fromFieldValue = ObjectPropertyUtils.getPropertyValue(lineDataObject, fromFieldName);
813                if (fromFieldValue != null) {
814                    lineIdentifier += fromFieldValue;
815                }
816                lineIdentifier += ":";
817            }
818            lineIdentifier = StringUtils.removeEnd(lineIdentifier, ":");
819
820            ((ValueConfiguredControl) selectControl).setValue(lineIdentifier);
821        }
822    }
823
824    /**
825     * Determines if given data object has associated maintenance document that allows new or copy
826     * maintenance
827     * actions
828     *
829     * @return boolean true if the maintenance new or copy action is allowed for the data object instance, false
830     *         otherwise
831     */
832    public boolean allowsMaintenanceNewOrCopyAction() {
833        boolean allowsNewOrCopy = false;
834
835        String maintDocTypeName = getMaintenanceDocumentTypeName();
836        if (StringUtils.isNotBlank(maintDocTypeName)) {
837            allowsNewOrCopy = getDataObjectAuthorizationService()
838                    .canCreate(getDataObjectClass(), GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
839        }
840
841        return allowsNewOrCopy;
842    }
843
844    /**
845     * Determines if given data object has associated maintenance document that allows edit maintenance
846     * actions
847     *
848     * @return boolean true if the maintenance edit action is allowed for the data object instance, false otherwise
849     */
850    public boolean allowsMaintenanceEditAction(Object dataObject) {
851        boolean allowsEdit = false;
852
853        String maintDocTypeName = getMaintenanceDocumentTypeName();
854        if (StringUtils.isNotBlank(maintDocTypeName)) {
855            allowsEdit = getDataObjectAuthorizationService()
856                    .canMaintain(dataObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
857        }
858
859        return allowsEdit;
860    }
861
862    /**
863     * Determines if given data object has associated maintenance document that allows delete maintenance
864     * actions.
865     *
866     * @return boolean true if the maintenance delete action is allowed for the data object instance, false otherwise
867     */
868    public boolean allowsMaintenanceDeleteAction(Object dataObject) {
869        boolean allowsMaintain = false;
870        boolean allowsDelete = false;
871
872        String maintDocTypeName = getMaintenanceDocumentTypeName();
873        if (StringUtils.isNotBlank(maintDocTypeName)) {
874            allowsMaintain = getDataObjectAuthorizationService()
875                    .canMaintain(dataObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
876        }
877
878        allowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion(getDataObjectClass());
879
880        return allowsDelete && allowsMaintain;
881    }
882
883    /**
884     * Returns the maintenance document type associated with the business object class or null if one does not exist.
885     *
886     * @return String representing the maintenance document type name
887     */
888    protected String getMaintenanceDocumentTypeName() {
889        DocumentDictionaryService dd = getDocumentDictionaryService();
890        String maintDocTypeName = dd.getMaintenanceDocumentTypeName(getDataObjectClass());
891
892        return maintDocTypeName;
893    }
894
895    /**
896     * Determines whether a given data object that's returned as one of the lookup's results is considered returnable,
897     * which means that for single-value lookups, a "return value" link may be rendered, and for multiple
898     * value lookups, a checkbox is rendered.
899     *
900     * Note that this can be part of an authorization mechanism, but not the complete authorization mechanism.  The
901     * component that invoked the lookup/ lookup caller (e.g. document, nesting lookup, etc.) needs to check
902     * that the object that was passed to it was returnable as well because there are ways around this method
903     * (e.g. crafting a custom return URL).
904     *
905     * @param dataObject - an object from the search result set
906     * @return true if the row is returnable and false if it is not
907     */
908    protected boolean isResultReturnable(Object dataObject) {
909        return true;
910    }
911
912    /**
913     * @see org.kuali.rice.krad.lookup.Lookupable#setDataObjectClass
914     */
915    @Override
916    public void setDataObjectClass(Class<?> dataObjectClass) {
917        this.dataObjectClass = dataObjectClass;
918    }
919
920    /**
921     * @see org.kuali.rice.krad.lookup.Lookupable#getDataObjectClass
922     */
923    @Override
924    public Class<?> getDataObjectClass() {
925        return this.dataObjectClass;
926    }
927
928    /**
929     * @see org.kuali.rice.krad.lookup.Lookupable#setFieldConversions
930     */
931    @Override
932    public void setFieldConversions(Map<String, String> fieldConversions) {
933        this.fieldConversions = fieldConversions;
934    }
935
936    /**
937     * @see org.kuali.rice.krad.lookup.Lookupable#setReadOnlyFieldsList
938     */
939    @Override
940    public void setReadOnlyFieldsList(List<String> readOnlyFieldsList) {
941        this.readOnlyFieldsList = readOnlyFieldsList;
942    }
943
944    public Map<String, String> getParameters() {
945        return parameters;
946    }
947
948    public void setParameters(Map<String, String> parameters) {
949        this.parameters = parameters;
950    }
951
952    public List<String> getDefaultSortAttributeNames() {
953        return defaultSortAttributeNames;
954    }
955
956    public void setDefaultSortAttributeNames(List<String> defaultSortAttributeNames) {
957        this.defaultSortAttributeNames = defaultSortAttributeNames;
958    }
959
960    public boolean isSortAscending() {
961        return sortAscending;
962    }
963
964    public void setSortAscending(boolean sortAscending) {
965        this.sortAscending = sortAscending;
966    }
967
968    public List<String> getReadOnlyFieldsList() {
969        return readOnlyFieldsList;
970    }
971
972    public Map<String, String> getFieldConversions() {
973        return fieldConversions;
974    }
975
976    protected ConfigurationService getConfigurationService() {
977        if (configurationService == null) {
978            this.configurationService = KRADServiceLocator.getKualiConfigurationService();
979        }
980        return configurationService;
981    }
982
983    public void setConfigurationService(ConfigurationService configurationService) {
984        this.configurationService = configurationService;
985    }
986
987    protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
988        if (dataObjectAuthorizationService == null) {
989            this.dataObjectAuthorizationService = KRADServiceLocatorWeb.getDataObjectAuthorizationService();
990        }
991        return dataObjectAuthorizationService;
992    }
993
994    public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
995        this.dataObjectAuthorizationService = dataObjectAuthorizationService;
996    }
997
998    protected DataObjectMetaDataService getDataObjectMetaDataService() {
999        if (dataObjectMetaDataService == null) {
1000            this.dataObjectMetaDataService = KRADServiceLocatorWeb.getDataObjectMetaDataService();
1001        }
1002        return dataObjectMetaDataService;
1003    }
1004
1005    public void setDataObjectMetaDataService(DataObjectMetaDataService dataObjectMetaDataService) {
1006        this.dataObjectMetaDataService = dataObjectMetaDataService;
1007    }
1008
1009    public DocumentDictionaryService getDocumentDictionaryService() {
1010        if (documentDictionaryService == null) {
1011            documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1012        }
1013        return documentDictionaryService;
1014    }
1015
1016    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1017        this.documentDictionaryService = documentDictionaryService;
1018    }
1019
1020    protected LookupService getLookupService() {
1021        if (lookupService == null) {
1022            this.lookupService = KRADServiceLocatorWeb.getLookupService();
1023        }
1024        return lookupService;
1025    }
1026
1027    public void setLookupService(LookupService lookupService) {
1028        this.lookupService = lookupService;
1029    }
1030
1031    protected EncryptionService getEncryptionService() {
1032        if (encryptionService == null) {
1033            this.encryptionService = CoreApiServiceLocator.getEncryptionService();
1034        }
1035        return encryptionService;
1036    }
1037
1038    public void setEncryptionService(EncryptionService encryptionService) {
1039        this.encryptionService = encryptionService;
1040    }
1041}