View Javadoc

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