View Javadoc

1   /**
2    * Copyright 2005-2011 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.ValueConfiguredControl;
42  import org.kuali.rice.krad.uif.field.InputField;
43  import org.kuali.rice.krad.uif.field.LookupInputField;
44  import org.kuali.rice.krad.uif.util.ComponentUtils;
45  import org.kuali.rice.krad.uif.view.LookupView;
46  import org.kuali.rice.krad.uif.view.View;
47  import org.kuali.rice.krad.uif.control.HiddenControl;
48  import org.kuali.rice.krad.uif.field.LinkField;
49  import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
50  import org.kuali.rice.krad.uif.util.LookupInquiryUtils;
51  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
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                     StringBuilder script = new StringBuilder("e.preventDefault();");
599                     for (String returnField : lookupForm.getFieldConversions().values()) {
600                         if (props.containsKey(returnField)) {
601                             Object fieldName = returnField.replace("'", "\\'");
602                             Object value = props.get(returnField);
603                             script = script.append("returnLookupResultByScript('" + returnField + "', '" + value + "');");
604                         }
605                     }
606                     returnLinkField.setOnClickScript(script.append("closeLightbox();").toString());
607                 }  else{
608                     // Close the light box if return target is not _self or _parent
609                     returnLinkField.setOnClickScript("e.preventDefault();closeLightbox();createLoading(true);window.open(jq(this).attr('href'), jq(this).attr('target'));");
610                 }
611             }
612         } else {
613             // If no return target is set return in same frame
614             // This is to insure that non light box lookups return correctly
615             returnLinkField.setTarget("_self");
616         }
617     }
618 
619     /**
620      * Builds the URL for returning the given data object result row
621      *
622      * <p>
623      * Note return URL will only be built if a return location is specified on the <code>LookupForm</code>
624      * </p>
625      *
626      * @param lookupView - lookup view instance containing lookup configuration
627      * @param lookupForm - lookup form instance containing the data
628      * @param dataObject - data object instance for the current line and for which the return URL is being built
629      * @return String return URL or blank if URL cannot be built
630      */
631     protected String getReturnUrl(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
632         Properties props = getReturnUrlParameters(lookupView, lookupForm, dataObject);
633 
634         String href = "";
635         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
636             href = UrlFactory.parameterizeUrl(lookupForm.getReturnLocation(), props);
637         }
638 
639         return href;
640     }
641 
642     /**
643      * Builds up a <code>Properties</code> object that will be used to provide the request parameters for the
644      * return URL link
645      *
646      * @param lookupView - lookup view instance containing lookup configuration
647      * @param lookupForm - lookup form instance containing the data
648      * @param dataObject - data object instance for the current line and for which the return URL is being built
649      * @return Properties instance containing request parameters for return URL
650      */
651     protected Properties getReturnUrlParameters(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
652         Properties props = new Properties();
653         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, KRADConstants.RETURN_METHOD_TO_CALL);
654 
655         if (StringUtils.isNotBlank(lookupForm.getReturnFormKey())) {
656             props.put(UifParameters.FORM_KEY, lookupForm.getReturnFormKey());
657         }
658 
659         props.put(KRADConstants.REFRESH_CALLER, lookupView.getId());
660         props.put(KRADConstants.REFRESH_DATA_OBJECT_CLASS, getDataObjectClass().getName());
661 
662         if (StringUtils.isNotBlank(lookupForm.getDocNum())) {
663             props.put(UifParameters.DOC_NUM, lookupForm.getDocNum());
664         }
665 
666         if (StringUtils.isNotBlank(lookupForm.getReferencesToRefresh())) {
667             props.put(KRADConstants.REFERENCES_TO_REFRESH, lookupForm.getReferencesToRefresh());
668         }
669 
670         List<String> returnKeys = getReturnKeys(lookupView, lookupForm, dataObject);
671         Map<String, String> returnKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(returnKeys, dataObject);
672 
673         for (String returnKey : returnKeyValues.keySet()) {
674             String returnValue = returnKeyValues.get(returnKey);
675             if (lookupForm.getFieldConversions().containsKey(returnKey)) {
676                 returnKey = lookupForm.getFieldConversions().get(returnKey);
677             }
678 
679             props.put(returnKey, returnValue);
680         }
681 
682         return props;
683     }
684 
685     /**
686      * Returns the configured return key property names or if not configured defaults to the primary keys
687      * for the data object class
688      *
689      * @return List<String> property names which should be passed back on the return URL
690      */
691     protected List<String> getReturnKeys(LookupView lookupView, LookupForm lookupForm, Object dataObject) {
692         List<String> returnKeys;
693         if (lookupForm.getFieldConversions() != null && !lookupForm.getFieldConversions().isEmpty()) {
694             returnKeys = new ArrayList<String>(lookupForm.getFieldConversions().keySet());
695         } else {
696             returnKeys = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
697         }
698 
699         return returnKeys;
700     }
701 
702     /**
703      * @see org.kuali.rice.krad.lookup.Lookupable#getMaintenanceActionLink
704      */
705     public void getMaintenanceActionLink(LinkField actionLinkField, Object model, String maintenanceMethodToCall) {
706         LookupForm lookupForm = (LookupForm) model;
707         LookupView lookupView = (LookupView) actionLinkField.getContext().get(UifConstants.ContextVariableNames.VIEW);
708         Object dataObject = actionLinkField.getContext().get(UifConstants.ContextVariableNames.LINE);
709 
710         List<String> pkNames = getDataObjectMetaDataService().listPrimaryKeyFieldNames(getDataObjectClass());
711 
712         // build maintenance link href
713         String href = getActionUrlHref(lookupForm, dataObject, maintenanceMethodToCall, pkNames);
714         if (StringUtils.isBlank(href)) {
715             actionLinkField.setRender(false);
716             return;
717         }
718         // TODO: need to handle returning anchor
719         actionLinkField.setHrefText(href);
720 
721         // build action title
722         String prependTitleText = actionLinkField.getLinkLabel() + " " +
723                 getDataDictionaryService().getDataDictionary().getDataObjectEntry(getDataObjectClass().getName())
724                         .getObjectLabel() + " " +
725                 getConfigurationService().getPropertyValueAsString(
726                         KRADConstants.Lookup.TITLE_ACTION_URL_PREPENDTEXT_PROPERTY);
727 
728         Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
729         String title = LookupInquiryUtils.getLinkTitleText(prependTitleText, getDataObjectClass(), primaryKeyValues);
730         actionLinkField.setTitle(title);
731         // TODO : do not hardcode the _self string
732         actionLinkField.setTarget("_self");
733         lookupForm.setAtLeastOneRowHasActions(true);
734     }
735 
736     /**
737      * Generates a URL to perform a maintenance action on the given result data object
738      *
739      * <p>
740      * Will build a URL containing keys of the data object to invoke the given maintenance action method
741      * within the maintenance controller
742      * </p>
743      *
744      * @param dataObject - data object instance for the line to build the maintenance action link for
745      * @param methodToCall - method name on the maintenance controller that should be invoked
746      * @param pkNames - list of primary key field names for the data object whose key/value pairs will be added to
747      * the maintenance link
748      * @return String URL link for the maintenance action
749      */
750     protected String getActionUrlHref(LookupForm lookupForm, Object dataObject, String methodToCall,
751             List<String> pkNames) {
752         LookupView lookupView = (LookupView) lookupForm.getView();
753 
754         Properties props = new Properties();
755         props.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
756 
757         Map<String, String> primaryKeyValues = KRADUtils.getPropertyKeyValuesFromDataObject(pkNames, dataObject);
758         for (String primaryKey : primaryKeyValues.keySet()) {
759             String primaryKeyValue = primaryKeyValues.get(primaryKey);
760 
761             props.put(primaryKey, primaryKeyValue);
762         }
763 
764         if (StringUtils.isNotBlank(lookupForm.getReturnLocation())) {
765             props.put(KRADConstants.RETURN_LOCATION_PARAMETER, lookupForm.getReturnLocation());
766         }
767 
768         props.put(UifParameters.DATA_OBJECT_CLASS_NAME, lookupForm.getDataObjectClassName());
769         props.put(UifParameters.VIEW_TYPE_NAME, UifConstants.ViewType.MAINTENANCE.name());
770 
771         String maintenanceMapping = KRADConstants.Maintenance.REQUEST_MAPPING_MAINTENANCE;
772         if (StringUtils.isNotBlank(lookupView.getMaintenanceUrlMapping())) {
773             maintenanceMapping = lookupView.getMaintenanceUrlMapping();
774         }
775 
776         return UrlFactory.parameterizeUrl(maintenanceMapping, props);
777     }
778 
779     /**
780      * Sets the value for the attribute field control to contain the field conversion values for the line
781      *
782      * @see org.kuali.rice.krad.lookup.LookupableImpl#setMultiValueLookupSelect
783      */
784     @Override
785     public void setMultiValueLookupSelect(InputField selectField, Object model) {
786         LookupForm lookupForm = (LookupForm) model;
787         Object lineDataObject = selectField.getContext().get(UifConstants.ContextVariableNames.LINE);
788         if (lineDataObject == null) {
789             throw new RuntimeException("Unable to get data object for line from component: " + selectField.getId());
790         }
791 
792         Control selectControl = ((InputField) selectField).getControl();
793         if ((selectControl != null) && (selectControl instanceof ValueConfiguredControl)) {
794             String lineIdentifier = "";
795 
796             // get value for each field conversion from line and add to lineIdentifier
797             Map<String, String> fieldConversions = lookupForm.getFieldConversions();
798             List<String> fromFieldNames = new ArrayList<String>(fieldConversions.keySet());
799             Collections.sort(fromFieldNames);
800             for (String fromFieldName : fromFieldNames) {
801                 Object fromFieldValue = ObjectPropertyUtils.getPropertyValue(lineDataObject, fromFieldName);
802                 if (fromFieldValue != null) {
803                     lineIdentifier += fromFieldValue;
804                 }
805                 lineIdentifier += ":";
806             }
807             lineIdentifier = StringUtils.removeEnd(lineIdentifier, ":");
808 
809             ((ValueConfiguredControl) selectControl).setValue(lineIdentifier);
810         }
811     }
812 
813     /**
814      * Determines if given data object has associated maintenance document that allows new or copy
815      * maintenance
816      * actions
817      *
818      * @return boolean true if the maintenance new or copy action is allowed for the data object instance, false
819      *         otherwise
820      */
821     public boolean allowsMaintenanceNewOrCopyAction() {
822         boolean allowsNewOrCopy = false;
823 
824         String maintDocTypeName = getMaintenanceDocumentTypeName();
825         if (StringUtils.isNotBlank(maintDocTypeName)) {
826             allowsNewOrCopy = getDataObjectAuthorizationService()
827                     .canCreate(getDataObjectClass(), GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
828         }
829 
830         return allowsNewOrCopy;
831     }
832 
833     /**
834      * Determines if given data object has associated maintenance document that allows edit maintenance
835      * actions
836      *
837      * @return boolean true if the maintenance edit action is allowed for the data object instance, false otherwise
838      */
839     public boolean allowsMaintenanceEditAction(Object dataObject) {
840         boolean allowsEdit = false;
841 
842         String maintDocTypeName = getMaintenanceDocumentTypeName();
843         if (StringUtils.isNotBlank(maintDocTypeName)) {
844             allowsEdit = getDataObjectAuthorizationService()
845                     .canMaintain(dataObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
846         }
847 
848         return allowsEdit;
849     }
850 
851     /**
852      * Determines if given data object has associated maintenance document that allows delete maintenance
853      * actions.
854      *
855      * @return boolean true if the maintenance delete action is allowed for the data object instance, false otherwise
856      */
857     public boolean allowsMaintenanceDeleteAction(Object dataObject) {
858         boolean allowsMaintain = false;
859         boolean allowsDelete = false;
860 
861         String maintDocTypeName = getMaintenanceDocumentTypeName();
862         if (StringUtils.isNotBlank(maintDocTypeName)) {
863             allowsMaintain = getDataObjectAuthorizationService()
864                     .canMaintain(dataObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
865         }
866 
867         allowsDelete = getDocumentDictionaryService().getAllowsRecordDeletion(getDataObjectClass());
868 
869         return allowsDelete && allowsMaintain;
870     }
871 
872     /**
873      * Returns the maintenance document type associated with the business object class or null if one does not exist.
874      *
875      * @return String representing the maintenance document type name
876      */
877     protected String getMaintenanceDocumentTypeName() {
878         DocumentDictionaryService dd = getDocumentDictionaryService();
879         String maintDocTypeName = dd.getMaintenanceDocumentTypeName(getDataObjectClass());
880 
881         return maintDocTypeName;
882     }
883 
884     /**
885      * Determines whether a given data object that's returned as one of the lookup's results is considered returnable,
886      * which means that for single-value lookups, a "return value" link may be rendered, and for multiple
887      * value lookups, a checkbox is rendered.
888      *
889      * Note that this can be part of an authorization mechanism, but not the complete authorization mechanism.  The
890      * component that invoked the lookup/ lookup caller (e.g. document, nesting lookup, etc.) needs to check
891      * that the object that was passed to it was returnable as well because there are ways around this method
892      * (e.g. crafting a custom return URL).
893      *
894      * @param dataObject - an object from the search result set
895      * @return true if the row is returnable and false if it is not
896      */
897     protected boolean isResultReturnable(Object dataObject) {
898         return true;
899     }
900 
901     /**
902      * @see org.kuali.rice.krad.lookup.Lookupable#setDataObjectClass
903      */
904     @Override
905     public void setDataObjectClass(Class<?> dataObjectClass) {
906         this.dataObjectClass = dataObjectClass;
907     }
908 
909     /**
910      * @see org.kuali.rice.krad.lookup.Lookupable#getDataObjectClass
911      */
912     @Override
913     public Class<?> getDataObjectClass() {
914         return this.dataObjectClass;
915     }
916 
917     /**
918      * @see org.kuali.rice.krad.lookup.Lookupable#setFieldConversions
919      */
920     @Override
921     public void setFieldConversions(Map<String, String> fieldConversions) {
922         this.fieldConversions = fieldConversions;
923     }
924 
925     /**
926      * @see org.kuali.rice.krad.lookup.Lookupable#setReadOnlyFieldsList
927      */
928     @Override
929     public void setReadOnlyFieldsList(List<String> readOnlyFieldsList) {
930         this.readOnlyFieldsList = readOnlyFieldsList;
931     }
932 
933     public Map<String, String> getParameters() {
934         return parameters;
935     }
936 
937     public void setParameters(Map<String, String> parameters) {
938         this.parameters = parameters;
939     }
940 
941     public List<String> getDefaultSortAttributeNames() {
942         return defaultSortAttributeNames;
943     }
944 
945     public void setDefaultSortAttributeNames(List<String> defaultSortAttributeNames) {
946         this.defaultSortAttributeNames = defaultSortAttributeNames;
947     }
948 
949     public boolean isSortAscending() {
950         return sortAscending;
951     }
952 
953     public void setSortAscending(boolean sortAscending) {
954         this.sortAscending = sortAscending;
955     }
956 
957     public List<String> getReadOnlyFieldsList() {
958         return readOnlyFieldsList;
959     }
960 
961     public Map<String, String> getFieldConversions() {
962         return fieldConversions;
963     }
964 
965     protected ConfigurationService getConfigurationService() {
966         if (configurationService == null) {
967             this.configurationService = KRADServiceLocator.getKualiConfigurationService();
968         }
969         return configurationService;
970     }
971 
972     public void setConfigurationService(ConfigurationService configurationService) {
973         this.configurationService = configurationService;
974     }
975 
976     protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
977         if (dataObjectAuthorizationService == null) {
978             this.dataObjectAuthorizationService = KRADServiceLocatorWeb.getDataObjectAuthorizationService();
979         }
980         return dataObjectAuthorizationService;
981     }
982 
983     public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
984         this.dataObjectAuthorizationService = dataObjectAuthorizationService;
985     }
986 
987     protected DataObjectMetaDataService getDataObjectMetaDataService() {
988         if (dataObjectMetaDataService == null) {
989             this.dataObjectMetaDataService = KRADServiceLocatorWeb.getDataObjectMetaDataService();
990         }
991         return dataObjectMetaDataService;
992     }
993 
994     public void setDataObjectMetaDataService(DataObjectMetaDataService dataObjectMetaDataService) {
995         this.dataObjectMetaDataService = dataObjectMetaDataService;
996     }
997 
998     public DocumentDictionaryService getDocumentDictionaryService() {
999         if (documentDictionaryService == null) {
1000             documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1001         }
1002         return documentDictionaryService;
1003     }
1004 
1005     public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1006         this.documentDictionaryService = documentDictionaryService;
1007     }
1008 
1009     protected LookupService getLookupService() {
1010         if (lookupService == null) {
1011             this.lookupService = KRADServiceLocatorWeb.getLookupService();
1012         }
1013         return lookupService;
1014     }
1015 
1016     public void setLookupService(LookupService lookupService) {
1017         this.lookupService = lookupService;
1018     }
1019 
1020     protected EncryptionService getEncryptionService() {
1021         if (encryptionService == null) {
1022             this.encryptionService = CoreApiServiceLocator.getEncryptionService();
1023         }
1024         return encryptionService;
1025     }
1026 
1027     public void setEncryptionService(EncryptionService encryptionService) {
1028         this.encryptionService = encryptionService;
1029     }
1030 }