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.kns.lookup;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
20  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
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.cache.CopiedObject;
27  import org.kuali.rice.core.api.util.type.TypeUtils;
28  import org.kuali.rice.core.web.format.DateFormatter;
29  import org.kuali.rice.core.web.format.Formatter;
30  import org.kuali.rice.kim.api.identity.Person;
31  import org.kuali.rice.kns.document.authorization.BusinessObjectRestrictions;
32  import org.kuali.rice.kns.document.authorization.FieldRestriction;
33  import org.kuali.rice.kns.inquiry.Inquirable;
34  import org.kuali.rice.kns.service.BusinessObjectAuthorizationService;
35  import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
36  import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
37  import org.kuali.rice.kns.service.KNSServiceLocator;
38  import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
39  import org.kuali.rice.kns.util.FieldUtils;
40  import org.kuali.rice.kns.web.comparator.CellComparatorHelper;
41  import org.kuali.rice.kns.web.struts.form.LookupForm;
42  import org.kuali.rice.kns.web.struts.form.MultipleValueLookupForm;
43  import org.kuali.rice.kns.web.ui.Column;
44  import org.kuali.rice.kns.web.ui.Field;
45  import org.kuali.rice.kns.web.ui.ResultRow;
46  import org.kuali.rice.kns.web.ui.Row;
47  import org.kuali.rice.krad.bo.BusinessObject;
48  import org.kuali.rice.krad.bo.PersistableBusinessObject;
49  import org.kuali.rice.krad.datadictionary.AttributeSecurity;
50  import org.kuali.rice.krad.datadictionary.mask.MaskFormatter;
51  import org.kuali.rice.krad.exception.ValidationException;
52  import org.kuali.rice.krad.service.BusinessObjectService;
53  import org.kuali.rice.krad.service.DataDictionaryService;
54  import org.kuali.rice.krad.service.KRADServiceLocator;
55  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
56  import org.kuali.rice.krad.service.LookupService;
57  import org.kuali.rice.krad.service.PersistenceStructureService;
58  import org.kuali.rice.krad.service.SequenceAccessorService;
59  import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
60  import org.kuali.rice.krad.util.GlobalVariables;
61  import org.kuali.rice.krad.util.KRADConstants;
62  import org.kuali.rice.krad.util.ObjectUtils;
63  import org.kuali.rice.krad.util.UrlFactory;
64  
65  import java.security.GeneralSecurityException;
66  import java.sql.Date;
67  import java.util.ArrayList;
68  import java.util.Collection;
69  import java.util.HashMap;
70  import java.util.HashSet;
71  import java.util.Iterator;
72  import java.util.List;
73  import java.util.Map;
74  import java.util.Properties;
75  import java.util.Set;
76  
77  /**
78   * This class declares many of the common spring injected properties, the get/set-ers for them,
79   * and some common util methods that require the injected services
80   */
81  public abstract class AbstractLookupableHelperServiceImpl implements LookupableHelperService {
82  
83      protected static final String TITLE_RETURN_URL_PREPENDTEXT_PROPERTY = "title.return.url.value.prependtext";
84      protected static final String TITLE_ACTION_URL_PREPENDTEXT_PROPERTY = "title.action.url.value.prependtext";
85      protected static final String ACTION_URLS_CHILDREN_SEPARATOR = " | ";
86      protected static final String ACTION_URLS_CHILDREN_STARTER = " [";
87      protected static final String ACTION_URLS_CHILDREN_END = "]";
88      protected static final String ACTION_URLS_SEPARATOR = "  ";
89      protected static final String ACTION_URLS_EMPTY = " ";
90  
91      protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AbstractLookupableHelperServiceImpl.class);
92  
93      protected Class businessObjectClass;
94      protected Map<String, String[]> parameters;
95      protected BusinessObjectDictionaryService businessObjectDictionaryService;
96      protected BusinessObjectMetaDataService businessObjectMetaDataService;
97      protected DataDictionaryService dataDictionaryService;
98      protected PersistenceStructureService persistenceStructureService;
99      protected EncryptionService encryptionService;
100     protected List<String> readOnlyFieldsList;
101     protected String backLocation;
102     protected String docFormKey;
103     protected Map fieldConversions;
104     protected LookupService lookupService;
105     protected List<Row> rows;
106     protected String referencesToRefresh;
107     protected SequenceAccessorService sequenceAccessorService;
108     protected BusinessObjectService businessObjectService;
109     protected LookupResultsService lookupResultsService;
110     protected String docNum;
111     protected ConfigurationService configurationService;
112     protected ParameterService parameterService;
113     protected BusinessObjectAuthorizationService businessObjectAuthorizationService;
114 
115     /**
116      * @return the docNum
117      */
118     public String getDocNum() {
119         return this.docNum;
120     }
121 
122     /**
123      * @param docNum the docNum to set
124      */
125     public void setDocNum(String docNum) {
126         this.docNum = docNum;
127     }
128 
129     public AbstractLookupableHelperServiceImpl() {
130         rows = null;
131     }
132 
133     /**
134      * This implementation always returns false.
135      *
136      * @see LookupableHelperService#checkForAdditionalFields(java.util.Map)
137      */
138     public boolean checkForAdditionalFields(Map<String, String> fieldValues) {
139         return false;
140     }
141 
142     /**
143      * @see LookupableHelperService#getBusinessObjectClass()
144      */
145     public Class getBusinessObjectClass() {
146         return businessObjectClass;
147     }
148 
149     /**
150      * @see LookupableHelperService#setBusinessObjectClass(java.lang.Class)
151      */
152     public void setBusinessObjectClass(Class businessObjectClass) {
153         this.businessObjectClass = businessObjectClass;
154         setRows();
155     }
156 
157     /**
158      * @see LookupableHelperService#getParameters()
159      */
160     public Map<String, String[]> getParameters() {
161         return parameters;
162     }
163 
164     /**
165      * @see LookupableHelperService#setParameters(java.util.Map)
166      */
167     public void setParameters(Map<String, String[]> parameters) {
168         this.parameters = parameters;
169     }
170 
171     /**
172      * Gets the dataDictionaryService attribute.
173      *
174      * @return Returns the dataDictionaryService.
175      */
176     public DataDictionaryService getDataDictionaryService() {
177         return dataDictionaryService != null ? dataDictionaryService : KRADServiceLocatorWeb.getDataDictionaryService();
178     }
179 
180     /**
181      * Sets the dataDictionaryService attribute value.
182      *
183      * @param dataDictionaryService The dataDictionaryService to set.
184      */
185     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
186         this.dataDictionaryService = dataDictionaryService;
187     }
188 
189     /**
190      * Gets the businessObjectDictionaryService attribute.
191      *
192      * @return Returns the businessObjectDictionaryService.
193      */
194     public BusinessObjectDictionaryService getBusinessObjectDictionaryService() {
195         return businessObjectDictionaryService != null ? businessObjectDictionaryService : KNSServiceLocator
196                 .getBusinessObjectDictionaryService();
197     }
198 
199     /**
200      * Sets the businessObjectDictionaryService attribute value.
201      *
202      * @param businessObjectDictionaryService
203      *         The businessObjectDictionaryService to set.
204      */
205     public void setBusinessObjectDictionaryService(BusinessObjectDictionaryService businessObjectDictionaryService) {
206         this.businessObjectDictionaryService = businessObjectDictionaryService;
207     }
208 
209     /**
210      * Gets the businessObjectMetaDataService attribute.
211      *
212      * @return Returns the businessObjectMetaDataService.
213      */
214     public BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
215         return businessObjectMetaDataService != null ? businessObjectMetaDataService : KNSServiceLocator
216                 .getBusinessObjectMetaDataService();
217     }
218 
219     /**
220      * Sets the businessObjectMetaDataService attribute value.
221      *
222      * @param businessObjectMetaDataService The businessObjectMetaDataService to set.
223      */
224     public void setBusinessObjectMetaDataService(BusinessObjectMetaDataService businessObjectMetaDataService) {
225         this.businessObjectMetaDataService = businessObjectMetaDataService;
226     }
227 
228     /**
229      * Gets the persistenceStructureService attribute.
230      *
231      * @return Returns the persistenceStructureService.
232      */
233     protected PersistenceStructureService getPersistenceStructureService() {
234         return persistenceStructureService != null ? persistenceStructureService : KRADServiceLocator
235                 .getPersistenceStructureService();
236     }
237 
238     /**
239      * Sets the persistenceStructureService attribute value.
240      *
241      * @param persistenceStructureService The persistenceStructureService to set.
242      */
243     public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
244         this.persistenceStructureService = persistenceStructureService;
245     }
246 
247     /**
248      * Gets the encryptionService attribute.
249      *
250      * @return Returns the encryptionService.
251      */
252     protected EncryptionService getEncryptionService() {
253         return encryptionService != null ? encryptionService : CoreApiServiceLocator.getEncryptionService();
254     }
255 
256     /**
257      * Sets the encryptionService attribute value.
258      *
259      * @param encryptionService The encryptionService to set.
260      */
261     public void setEncryptionService(EncryptionService encryptionService) {
262         this.encryptionService = encryptionService;
263     }
264 
265     protected MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
266 
267     public MaintenanceDocumentDictionaryService getMaintenanceDocumentDictionaryService() {
268         if (maintenanceDocumentDictionaryService == null) {
269             maintenanceDocumentDictionaryService = KNSServiceLocator.getMaintenanceDocumentDictionaryService();
270         }
271         return maintenanceDocumentDictionaryService;
272     }
273 
274 
275     public BusinessObjectAuthorizationService getBusinessObjectAuthorizationService() {
276         if (businessObjectAuthorizationService == null) {
277             businessObjectAuthorizationService = KNSServiceLocator.getBusinessObjectAuthorizationService();
278         }
279         return businessObjectAuthorizationService;
280     }
281 
282     protected Inquirable kualiInquirable;
283 
284     public Inquirable getKualiInquirable() {
285         if (kualiInquirable == null) {
286             kualiInquirable = KNSServiceLocator.getKualiInquirable();
287         }
288         return kualiInquirable;
289     }
290 
291     public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) {
292         this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService;
293     }
294 
295     public void setKualiInquirable(Inquirable kualiInquirable) {
296         this.kualiInquirable = kualiInquirable;
297     }
298 
299 
300     public ConfigurationService getKualiConfigurationService() {
301         if (configurationService == null) {
302             configurationService = KRADServiceLocator.getKualiConfigurationService();
303         }
304         return configurationService;
305     }
306 
307     public void setParameterService(ConfigurationService configurationService) {
308         this.configurationService = configurationService;
309     }
310 
311 
312     public ParameterService getParameterService() {
313         if (parameterService == null) {
314             parameterService = CoreFrameworkServiceLocator.getParameterService();
315         }
316         return parameterService;
317     }
318 
319     public void setParameterService(ParameterService parameterService) {
320         this.parameterService = parameterService;
321     }
322 
323     /**
324      * Determines if underlying lookup bo has associated maintenance document that allows new or copy maintenance actions.
325      *
326      * @return true if bo has maint doc that allows new or copy actions
327      */
328     public boolean allowsMaintenanceNewOrCopyAction() {
329         boolean allowsNewOrCopy = false;
330 
331         String maintDocTypeName = getMaintenanceDocumentTypeName();
332         Class boClass = this.getBusinessObjectClass();
333 
334         if (StringUtils.isNotBlank(maintDocTypeName)) {
335             allowsNewOrCopy = getBusinessObjectAuthorizationService().canCreate(boClass, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
336         }
337         return allowsNewOrCopy;
338     }
339 
340     protected boolean allowsMaintenanceEditAction(BusinessObject businessObject) {
341         boolean allowsEdit = false;
342 
343         String maintDocTypeName = getMaintenanceDocumentTypeName();
344 
345         if (StringUtils.isNotBlank(maintDocTypeName)) {
346             allowsEdit = getBusinessObjectAuthorizationService().canMaintain(businessObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
347         }
348         return allowsEdit;
349     }
350 
351 
352     /**
353      * Build a maintenance url.
354      *
355      * @param bo           - business object representing the record for maint.
356      * @param methodToCall - maintenance action
357      * @return
358      */
359     final public String getMaintenanceUrl(BusinessObject businessObject, HtmlData htmlData, List pkNames, BusinessObjectRestrictions businessObjectRestrictions) {
360         htmlData.setTitle(getActionUrlTitleText(businessObject, htmlData.getDisplayText(), pkNames, businessObjectRestrictions));
361         return htmlData.constructCompleteHtmlTag();
362     }
363 
364     /**
365      * This method is called by performLookup method to generate action urls.
366      * It calls the method getCustomActionUrls to get html data, calls getMaintenanceUrl to get the actual html tag,
367      * and returns a formatted/concatenated string of action urls.
368      *
369      * @see LookupableHelperService#getActionUrls(org.kuali.rice.krad.bo.BusinessObject)
370      */
371     final public String getActionUrls(BusinessObject businessObject, List pkNames, BusinessObjectRestrictions businessObjectRestrictions) {
372         StringBuffer actions = new StringBuffer();
373         List<HtmlData> htmlDataList = getCustomActionUrls(businessObject, pkNames);
374         for (HtmlData htmlData : htmlDataList) {
375             actions.append(getMaintenanceUrl(businessObject, htmlData, pkNames, businessObjectRestrictions));
376             if (htmlData.getChildUrlDataList() != null) {
377                 if (htmlData.getChildUrlDataList().size() > 0) {
378                     actions.append(ACTION_URLS_CHILDREN_STARTER);
379                     for (HtmlData childURLData : htmlData.getChildUrlDataList()) {
380                         actions.append(getMaintenanceUrl(businessObject, childURLData, pkNames, businessObjectRestrictions));
381                         actions.append(ACTION_URLS_CHILDREN_SEPARATOR);
382                     }
383                     if (actions.toString().endsWith(ACTION_URLS_CHILDREN_SEPARATOR))
384                         actions.delete(actions.length() - ACTION_URLS_CHILDREN_SEPARATOR.length(), actions.length());
385                     actions.append(ACTION_URLS_CHILDREN_END);
386                 }
387             }
388             actions.append(ACTION_URLS_SEPARATOR);
389         }
390         if (actions.toString().endsWith(ACTION_URLS_SEPARATOR))
391             actions.delete(actions.length() - ACTION_URLS_SEPARATOR.length(), actions.length());
392         return actions.toString();
393     }
394 
395     /**
396      * Child classes should override this method if they want to return some other action urls.
397      *
398      * @returns This default implementation returns links to edit and copy maintenance action for
399      * the current maintenance record if the business object class has an associated maintenance document.
400      * Also checks value of allowsNewOrCopy in maintenance document xml before rendering the copy link.
401      * @see LookupableHelperService#getCustomActionUrls(org.kuali.rice.krad.bo.BusinessObject, java.util.List, java.util.List pkNames)
402      */
403     public List<HtmlData> getCustomActionUrls(BusinessObject businessObject, List pkNames) {
404         List<HtmlData> htmlDataList = new ArrayList<HtmlData>();
405         if (allowsMaintenanceEditAction(businessObject)) {
406             htmlDataList.add(getUrlData(businessObject, KRADConstants.MAINTENANCE_EDIT_METHOD_TO_CALL, pkNames));
407         }
408         if (allowsMaintenanceNewOrCopyAction()) {
409             htmlDataList.add(getUrlData(businessObject, KRADConstants.MAINTENANCE_COPY_METHOD_TO_CALL, pkNames));
410         }
411         if (allowsMaintenanceDeleteAction(businessObject)) {
412             htmlDataList.add(getUrlData(businessObject, KRADConstants.MAINTENANCE_DELETE_METHOD_TO_CALL, pkNames));
413         }
414         return htmlDataList;
415     }
416 
417     /**
418      * This method ...
419      * for KULRice 3070
420      *
421      * @return
422      */
423     protected boolean allowsMaintenanceDeleteAction(BusinessObject businessObject) {
424 
425         boolean allowsMaintain = false;
426         boolean allowsDelete = false;
427 
428         String maintDocTypeName = getMaintenanceDocumentTypeName();
429 
430         if (StringUtils.isNotBlank(maintDocTypeName)) {
431             allowsMaintain = getBusinessObjectAuthorizationService().canMaintain(businessObject, GlobalVariables.getUserSession().getPerson(), maintDocTypeName);
432         }
433 
434         allowsDelete = KNSServiceLocator.getMaintenanceDocumentDictionaryService().getAllowsRecordDeletion(businessObjectClass);
435 
436         return allowsDelete && allowsMaintain;
437     }
438 
439     /**
440      * This method constructs an AnchorHtmlData.
441      * This method can be overriden by child classes if they want to construct the html data in a different way.
442      * Foe example, if they want different type of html tag, like input/image.
443      *
444      * @param businessObject
445      * @param methodToCall
446      * @param displayText
447      * @param pkNames
448      * @return
449      */
450     protected HtmlData.AnchorHtmlData getUrlData(BusinessObject businessObject, String methodToCall, String displayText, List pkNames) {
451 
452         String href = getActionUrlHref(businessObject, methodToCall, pkNames);
453         //String title = StringUtils.isBlank(href)?"":getActionUrlTitleText(businessObject, displayText, pkNames);
454         HtmlData.AnchorHtmlData anchorHtmlData = new HtmlData.AnchorHtmlData(href, methodToCall, displayText);
455         return anchorHtmlData;
456     }
457 
458     /**
459      * This method calls its overloaded method with displayText as methodToCall
460      *
461      * @param businessObject
462      * @param methodToCall
463      * @param pkNames
464      * @return
465      */
466     protected HtmlData.AnchorHtmlData getUrlData(BusinessObject businessObject, String methodToCall, List pkNames) {
467         return getUrlData(businessObject, methodToCall, methodToCall, pkNames);
468     }
469 
470     /**
471      * A utility method that returns an empty list of action urls.
472      *
473      * @return
474      */
475     protected List<HtmlData> getEmptyActionUrls() {
476         return new ArrayList<HtmlData>();
477     }
478 
479     protected HtmlData getEmptyAnchorHtmlData() {
480         return new HtmlData.AnchorHtmlData();
481     }
482 
483     /**
484      * This method generates and returns href for the given parameters.
485      * This method can be overridden by child classes if they have to generate href differently.
486      * For example, refer to IntendedIncumbentLookupableHelperServiceImpl
487      *
488      * @param businessObject
489      * @param methodToCall
490      * @param pkNames
491      * @return
492      */
493     protected String getActionUrlHref(BusinessObject businessObject, String methodToCall, List pkNames) {
494         Properties parameters = new Properties();
495         parameters.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
496         // TODO: why is this not using the businessObject parmeter's class?
497         parameters.put(KRADConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, businessObject.getClass().getName());
498         parameters.putAll(getParametersFromPrimaryKey(businessObject, pkNames));
499         if (StringUtils.isNotBlank(getReturnLocation())) {
500             parameters.put(KRADConstants.RETURN_LOCATION_PARAMETER, getReturnLocation());
501         }
502         return UrlFactory.parameterizeUrl(KRADConstants.MAINTENANCE_ACTION, parameters);
503     }
504 
505     protected Properties getParametersFromPrimaryKey(BusinessObject businessObject, List pkNames) {
506         Properties parameters = new Properties();
507         for (Iterator iter = pkNames.iterator(); iter.hasNext();) {
508             String fieldNm = (String) iter.next();
509 
510             Object fieldVal = ObjectUtils.getPropertyValue(businessObject, fieldNm);
511             if (fieldVal == null) {
512                 fieldVal = KRADConstants.EMPTY_STRING;
513             }
514             if (fieldVal instanceof java.sql.Date) {
515                 String formattedString = "";
516                 if (Formatter.findFormatter(fieldVal.getClass()) != null) {
517                     Formatter formatter = Formatter.getFormatter(fieldVal.getClass());
518                     formattedString = (String) formatter.format(fieldVal);
519                     fieldVal = formattedString;
520                 }
521             }
522 
523             // Encrypt value if it is a secure field
524             if (getBusinessObjectAuthorizationService().attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, fieldNm)) {
525                 try {
526                     fieldVal = getEncryptionService().encrypt(fieldVal) + EncryptionService.ENCRYPTION_POST_PREFIX;
527                 } catch (GeneralSecurityException e) {
528                     LOG.error("Exception while trying to encrypted value for inquiry framework.", e);
529                     throw new RuntimeException(e);
530                 }
531 
532             }
533 
534             parameters.put(fieldNm, fieldVal.toString());
535         }
536         return parameters;
537     }
538 
539     /**
540      * This method generates and returns title text for action urls.
541      * Child classes can override this if they want to generate the title text differently.
542      * For example, refer to BatchJobStatusLookupableHelperServiceImpl
543      *
544      * @param businessObject
545      * @param displayText
546      * @param pkNames
547      * @return
548      */
549     protected String getActionUrlTitleText(BusinessObject businessObject, String displayText, List pkNames, BusinessObjectRestrictions businessObjectRestrictions) {
550         String prependTitleText = displayText + " "
551                 + getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(getBusinessObjectClass().getName()).getObjectLabel()
552                 + " "
553                 + this.getKualiConfigurationService().getPropertyValueAsString(TITLE_ACTION_URL_PREPENDTEXT_PROPERTY);
554         return HtmlData.getTitleText(prependTitleText, businessObject, pkNames, businessObjectRestrictions);
555     }
556 
557     /**
558      * Returns the maintenance document type associated with the business object class or null if one does not
559      * exist.
560      *
561      * @return String representing the maintenance document type name
562      */
563     protected String getMaintenanceDocumentTypeName() {
564         MaintenanceDocumentDictionaryService dd = getMaintenanceDocumentDictionaryService();
565         String maintDocTypeName = dd.getDocumentTypeName(getBusinessObjectClass());
566         return maintDocTypeName;
567     }
568 
569     /**
570      * Gets the readOnlyFieldsList attribute.
571      *
572      * @return Returns the readOnlyFieldsList.
573      */
574     public List<String> getReadOnlyFieldsList() {
575         return readOnlyFieldsList;
576     }
577 
578 
579     /**
580      * Sets the readOnlyFieldsList attribute value.
581      *
582      * @param readOnlyFieldsList The readOnlyFieldsList to set.
583      */
584     public void setReadOnlyFieldsList(List<String> readOnlyFieldsList) {
585         this.readOnlyFieldsList = readOnlyFieldsList;
586     }
587 
588     protected HashMap<String, Boolean> noLookupResultFieldInquiryCache = new HashMap<String, Boolean>();
589     protected HashMap<Class, Class> inquirableClassCache = new HashMap<Class, Class>();
590     protected HashMap<String, Boolean> forceLookupResultFieldInquiryCache = new HashMap<String, Boolean>();
591 
592     /**
593      * Returns the inquiry url for a field if one exist.
594      *
595      * @param bo           the business object instance to build the urls for
596      * @param propertyName the property which links to an inquirable
597      * @return String url to inquiry
598      */
599     public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) {
600         HtmlData inquiryUrl = new HtmlData.AnchorHtmlData();
601 
602         String cacheKey = bo.getClass().getName() + "." + propertyName;
603         Boolean noLookupResultFieldInquiry = noLookupResultFieldInquiryCache.get(cacheKey);
604         if (noLookupResultFieldInquiry == null) {
605             noLookupResultFieldInquiry = getBusinessObjectDictionaryService().noLookupResultFieldInquiry(bo.getClass(), propertyName);
606             if (noLookupResultFieldInquiry == null) {
607                 noLookupResultFieldInquiry = Boolean.TRUE;
608             }
609             noLookupResultFieldInquiryCache.put(cacheKey, noLookupResultFieldInquiry);
610         }
611         if (!noLookupResultFieldInquiry) {
612 
613             Class<Inquirable> inquirableClass = inquirableClassCache.get(bo.getClass());
614             if (!inquirableClassCache.containsKey(bo.getClass())) {
615                 inquirableClass = getBusinessObjectDictionaryService().getInquirableClass(bo.getClass());
616                 inquirableClassCache.put(bo.getClass(), inquirableClass);
617             }
618             Inquirable inq = null;
619             try {
620                 if (inquirableClass != null) {
621                     inq = inquirableClass.newInstance();
622                 } else {
623                     inq = getKualiInquirable();
624                     if (LOG.isDebugEnabled()) {
625                         LOG.debug("Default Inquirable Class: " + inq.getClass());
626                     }
627                 }
628                 Boolean forceLookupResultFieldInquiry = forceLookupResultFieldInquiryCache.get(cacheKey);
629                 if (forceLookupResultFieldInquiry == null) {
630                     forceLookupResultFieldInquiry = getBusinessObjectDictionaryService().forceLookupResultFieldInquiry(bo.getClass(), propertyName);
631                     if (forceLookupResultFieldInquiry == null) {
632                         forceLookupResultFieldInquiry = Boolean.FALSE;
633                     }
634                     forceLookupResultFieldInquiryCache.put(cacheKey, forceLookupResultFieldInquiry);
635                 }
636                 inquiryUrl = inq.getInquiryUrl(bo, propertyName, forceLookupResultFieldInquiry);
637             } catch (Exception ex) {
638                 LOG.error("unable to create inquirable to get inquiry URL", ex);
639             }
640         }
641 
642         return inquiryUrl;
643     }
644 
645     protected CopiedObject<ArrayList<Column>> resultColumns = null;
646 
647     /**
648      * Constructs the list of columns for the search results. All properties for the column objects come from the DataDictionary.
649      */
650     public List<Column> getColumns() {
651         if (resultColumns == null) {
652             ArrayList<Column> columns = new ArrayList<Column>();
653             for (String attributeName : getBusinessObjectDictionaryService().getLookupResultFieldNames(getBusinessObjectClass())) {
654                 Column column = new Column();
655                 column.setPropertyName(attributeName);
656                 String columnTitle = getDataDictionaryService().getAttributeLabel(getBusinessObjectClass(), attributeName);
657                 Boolean useShortLabel = getBusinessObjectDictionaryService().getLookupResultFieldUseShortLabel(businessObjectClass, attributeName);
658                 if (useShortLabel != null && useShortLabel) {
659                     columnTitle = getDataDictionaryService().getAttributeShortLabel(getBusinessObjectClass(), attributeName);
660                 }
661                 if (StringUtils.isBlank(columnTitle)) {
662                     columnTitle = getDataDictionaryService().getCollectionLabel(getBusinessObjectClass(), attributeName);
663                 }
664                 column.setColumnTitle(columnTitle);
665                 column.setMaxLength(getColumnMaxLength(attributeName));
666 
667                 if (!businessObjectClass.isInterface()) {
668                     try {
669                         column.setFormatter(ObjectUtils.getFormatterWithDataDictionary(getBusinessObjectClass()
670                                 .newInstance(), attributeName));
671                     } catch (InstantiationException e) {
672                         LOG.info("Unable to get new instance of business object class: " + businessObjectClass.getName(), e);
673                         // just swallow exception and leave formatter blank
674                     } catch (IllegalAccessException e) {
675                         LOG.info("Unable to get new instance of business object class: " + businessObjectClass.getName(), e);
676                         // just swallow exception and leave formatter blank
677                     }
678                 }
679 
680                 String alternateDisplayPropertyName = getBusinessObjectDictionaryService()
681                         .getLookupFieldAlternateDisplayAttributeName(getBusinessObjectClass(), attributeName);
682                 if (StringUtils.isNotBlank(alternateDisplayPropertyName)) {
683                     column.setAlternateDisplayPropertyName(alternateDisplayPropertyName);
684                 }
685 
686                 String additionalDisplayPropertyName = getBusinessObjectDictionaryService()
687                         .getLookupFieldAdditionalDisplayAttributeName(getBusinessObjectClass(), attributeName);
688                 if (StringUtils.isNotBlank(additionalDisplayPropertyName)) {
689                     column.setAdditionalDisplayPropertyName(additionalDisplayPropertyName);
690                 } else {
691                     boolean translateCodes = getBusinessObjectDictionaryService().tranlateCodesInLookup(getBusinessObjectClass());
692                     if (translateCodes) {
693                         FieldUtils.setAdditionalDisplayPropertyForCodes(getBusinessObjectClass(), attributeName, column);
694                     }
695                 }
696 
697                 column.setTotal(getBusinessObjectDictionaryService().getLookupResultFieldTotal(getBusinessObjectClass(), attributeName));
698 
699                 columns.add(column);
700             }
701             resultColumns = ObjectUtils.deepCopyForCaching(columns);
702             return columns;
703         }
704         return resultColumns.getContent();
705     }
706 
707     protected static Integer RESULTS_DEFAULT_MAX_COLUMN_LENGTH = null;
708 
709     protected int getColumnMaxLength(String attributeName) {
710         Integer fieldDefinedMaxLength = getBusinessObjectDictionaryService().getLookupResultFieldMaxLength(getBusinessObjectClass(), attributeName);
711         if (fieldDefinedMaxLength == null) {
712             if (RESULTS_DEFAULT_MAX_COLUMN_LENGTH == null) {
713                 try {
714                     RESULTS_DEFAULT_MAX_COLUMN_LENGTH = Integer.valueOf(getParameterService().getParameterValueAsString(
715                             KRADConstants.KNS_NAMESPACE, KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE, KRADConstants.RESULTS_DEFAULT_MAX_COLUMN_LENGTH));
716                 } catch (NumberFormatException ex) {
717                     LOG.error("Lookup field max length parameter not found and unable to parse default set in system parameters (RESULTS_DEFAULT_MAX_COLUMN_LENGTH).");
718                 }
719             }
720             return RESULTS_DEFAULT_MAX_COLUMN_LENGTH.intValue();
721         }
722         return fieldDefinedMaxLength.intValue();
723     }
724 
725     /**
726      * @return Returns the backLocation.
727      */
728     public String getBackLocation() {
729         return backLocation;
730     }
731 
732     /**
733      * @param backLocation The backLocation to set.
734      */
735     public void setBackLocation(String backLocation) {
736         this.backLocation = backLocation;
737     }
738 
739     /**
740      * @see LookupableHelperService#getReturnLocation()
741      */
742     public String getReturnLocation() {
743         return backLocation;
744     }
745 
746     /**
747      * This method is for lookupable implementations
748      *
749      * @see LookupableHelperService#getReturnUrl(org.kuali.rice.krad.bo.BusinessObject, java.util.Map, java.lang.String, java.util.List)
750      */
751     final public HtmlData getReturnUrl(BusinessObject businessObject, Map fieldConversions, String lookupImpl, List returnKeys, BusinessObjectRestrictions businessObjectRestrictions) {
752         String href = getReturnHref(businessObject, fieldConversions, lookupImpl, returnKeys);
753         String returnUrlAnchorLabel =
754                 this.getKualiConfigurationService().getPropertyValueAsString(TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
755         HtmlData.AnchorHtmlData anchor = new HtmlData.AnchorHtmlData(href, HtmlData.getTitleText(returnUrlAnchorLabel, businessObject, returnKeys, businessObjectRestrictions));
756         anchor.setDisplayText(returnUrlAnchorLabel);
757         return anchor;
758     }
759 
760     /**
761      * This method is for lookupable implementations
762      *
763      * @param businessObject
764      * @param fieldConversions
765      * @param lookupImpl
766      * @param returnKeys
767      * @return
768      */
769     final protected String getReturnHref(BusinessObject businessObject, Map fieldConversions, String lookupImpl, List returnKeys) {
770         if (StringUtils.isNotBlank(backLocation)) {
771             return UrlFactory.parameterizeUrl(backLocation, getParameters(
772                     businessObject, fieldConversions, lookupImpl, returnKeys));
773         }
774         return "";
775     }
776 
777     /**
778      * @see LookupableHelperService#getReturnUrl(org.kuali.core.bo.BusinessObject, java.util.Map, java.lang.String)
779      */
780     public HtmlData getReturnUrl(BusinessObject businessObject, LookupForm lookupForm, List returnKeys, BusinessObjectRestrictions businessObjectRestrictions) {
781         Properties parameters = getParameters(
782                 businessObject, lookupForm.getFieldConversions(), lookupForm.getLookupableImplServiceName(), returnKeys);
783         if (StringUtils.isEmpty(lookupForm.getHtmlDataType()) || HtmlData.ANCHOR_HTML_DATA_TYPE.equals(lookupForm.getHtmlDataType()))
784             return getReturnAnchorHtmlData(businessObject, parameters, lookupForm, returnKeys, businessObjectRestrictions);
785         else
786             return getReturnInputHtmlData(businessObject, parameters, lookupForm, returnKeys, businessObjectRestrictions);
787     }
788 
789     protected HtmlData getReturnInputHtmlData(BusinessObject businessObject, Properties parameters, LookupForm lookupForm, List returnKeys, BusinessObjectRestrictions businessObjectRestrictions) {
790         String returnUrlAnchorLabel =
791                 this.getKualiConfigurationService().getPropertyValueAsString(TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
792         String name = KRADConstants.MULTIPLE_VALUE_LOOKUP_SELECTED_OBJ_ID_PARAM_PREFIX + lookupForm.getLookupObjectId();
793         HtmlData.InputHtmlData input = new HtmlData.InputHtmlData(name, HtmlData.InputHtmlData.CHECKBOX_INPUT_TYPE);
794         input.setTitle(HtmlData.getTitleText(returnUrlAnchorLabel, businessObject, returnKeys, businessObjectRestrictions));
795         if (((MultipleValueLookupForm) lookupForm).getCompositeObjectIdMap() == null ||
796                 ((MultipleValueLookupForm) lookupForm).getCompositeObjectIdMap().get(
797                         ((PersistableBusinessObject) businessObject).getObjectId()) == null) {
798             input.setChecked("");
799         } else {
800             input.setChecked(HtmlData.InputHtmlData.CHECKBOX_CHECKED_VALUE);
801         }
802         input.setValue(HtmlData.InputHtmlData.CHECKBOX_CHECKED_VALUE);
803         return input;
804     }
805 
806     protected HtmlData getReturnAnchorHtmlData(BusinessObject businessObject, Properties parameters, LookupForm lookupForm, List returnKeys, BusinessObjectRestrictions businessObjectRestrictions) {
807         String returnUrlAnchorLabel =
808                 this.getKualiConfigurationService().getPropertyValueAsString(TITLE_RETURN_URL_PREPENDTEXT_PROPERTY);
809         HtmlData.AnchorHtmlData anchor = new HtmlData.AnchorHtmlData(
810                 getReturnHref(parameters, lookupForm, returnKeys),
811                 HtmlData.getTitleText(returnUrlAnchorLabel, businessObject, returnKeys, businessObjectRestrictions));
812         anchor.setDisplayText(returnUrlAnchorLabel);
813         return anchor;
814     }
815 
816     protected String getReturnHref(Properties parameters, LookupForm lookupForm, List returnKeys) {
817         if (StringUtils.isNotBlank(backLocation)) {
818             String href = UrlFactory.parameterizeUrl(backLocation, parameters);
819             return addToReturnHref(href, lookupForm);
820         }
821         return "";
822     }
823 
824     protected String addToReturnHref(String href, LookupForm lookupForm) {
825         String lookupAnchor = "";
826         if (StringUtils.isNotEmpty(lookupForm.getAnchor())) {
827             lookupAnchor = lookupForm.getAnchor();
828         }
829         href += "&anchor=" + lookupAnchor + "&docNum=" + (StringUtils.isEmpty(getDocNum()) ? "" : getDocNum());
830         return href;
831     }
832 
833     protected Properties getParameters(BusinessObject bo, Map<String, String> fieldConversions, String lookupImpl, List returnKeys) {
834         Properties parameters = new Properties();
835         parameters.put(KRADConstants.DISPATCH_REQUEST_PARAMETER, KRADConstants.RETURN_METHOD_TO_CALL);
836         if (getDocFormKey() != null) {
837             parameters.put(KRADConstants.DOC_FORM_KEY, getDocFormKey());
838         }
839         if (lookupImpl != null) {
840             parameters.put(KRADConstants.REFRESH_CALLER, lookupImpl);
841         }
842         if (getDocNum() != null) {
843             parameters.put(KRADConstants.DOC_NUM, getDocNum());
844         }
845 
846         if (getReferencesToRefresh() != null) {
847             parameters.put(KRADConstants.REFERENCES_TO_REFRESH, getReferencesToRefresh());
848         }
849 
850         Iterator returnKeysIt = getReturnKeys().iterator();
851         while (returnKeysIt.hasNext()) {
852             String fieldNm = (String) returnKeysIt.next();
853 
854             Object fieldVal = ObjectUtils.getPropertyValue(bo, fieldNm);
855             if (fieldVal == null) {
856                 fieldVal = KRADConstants.EMPTY_STRING;
857             }
858 
859             if (getBusinessObjectAuthorizationService().attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, fieldNm)) {
860                 try {
861                     fieldVal = getEncryptionService().encrypt(fieldVal) + EncryptionService.ENCRYPTION_POST_PREFIX;
862                 } catch (GeneralSecurityException e) {
863                     LOG.error("Exception while trying to encrypted value for inquiry framework.", e);
864                     throw new RuntimeException(e);
865                 }
866 
867             }
868 
869             //need to format date in url
870             if (fieldVal instanceof Date) {
871                 DateFormatter dateFormatter = new DateFormatter();
872                 fieldVal = dateFormatter.format(fieldVal);
873             }
874 
875             if (fieldConversions.containsKey(fieldNm)) {
876                 fieldNm = (String) fieldConversions.get(fieldNm);
877             }
878 
879             parameters.put(fieldNm, fieldVal.toString());
880         }
881 
882         return parameters;
883     }
884 
885     /**
886      * @return a List of the names of fields which are marked in data dictionary as return fields.
887      */
888     public List<String> getReturnKeys() {
889         List<String> returnKeys;
890         if (fieldConversions != null && !fieldConversions.isEmpty()) {
891             returnKeys = new ArrayList<String>(fieldConversions.keySet());
892         } else {
893             returnKeys = getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(getBusinessObjectClass());
894         }
895 
896         return returnKeys;
897     }
898 
899     /**
900      * Gets the docFormKey attribute.
901      *
902      * @return Returns the docFormKey.
903      */
904     public String getDocFormKey() {
905         return docFormKey;
906     }
907 
908     /**
909      * Sets the docFormKey attribute value.
910      *
911      * @param docFormKey The docFormKey to set.
912      */
913     public void setDocFormKey(String docFormKey) {
914         this.docFormKey = docFormKey;
915     }
916 
917     /**
918      * @see org.kuali.core.lookup.LookupableHelperService#setFieldConversions(java.util.Map)
919      */
920     public void setFieldConversions(Map fieldConversions) {
921         this.fieldConversions = fieldConversions;
922     }
923 
924     /**
925      * Gets the lookupService attribute.
926      *
927      * @return Returns the lookupService.
928      */
929     protected LookupService getLookupService() {
930         return lookupService != null ? lookupService : KRADServiceLocatorWeb.getLookupService();
931     }
932 
933     /**
934      * Sets the lookupService attribute value.
935      *
936      * @param lookupService The lookupService to set.
937      */
938     public void setLookupService(LookupService lookupService) {
939         this.lookupService = lookupService;
940     }
941 
942     /**
943      * Uses the DD to determine which is the default sort order.
944      *
945      * @return property names that will be used to sort on by default
946      */
947     public List<String> getDefaultSortColumns() {
948         return getBusinessObjectDictionaryService().getLookupDefaultSortFieldNames(getBusinessObjectClass());
949     }
950 
951     /**
952      * Checks that any required search fields have value.
953      *
954      * @see org.kuali.core.lookup.LookupableHelperService#validateSearchParameters(java.util.Map)
955      */
956     public void validateSearchParameters(Map<String, String> fieldValues) {
957         List<String> lookupFieldAttributeList = null;
958         if (getBusinessObjectMetaDataService().isLookupable(getBusinessObjectClass())) {
959             lookupFieldAttributeList = getBusinessObjectMetaDataService().getLookupableFieldNames(getBusinessObjectClass());
960         }
961         if (lookupFieldAttributeList == null) {
962             throw new RuntimeException("Lookup not defined for business object " + getBusinessObjectClass());
963         }
964         for (Iterator iter = lookupFieldAttributeList.iterator(); iter.hasNext();) {
965             String attributeName = (String) iter.next();
966             if (fieldValues.containsKey(attributeName)) {
967                 // get label of attribute for message
968                 String attributeLabel = getDataDictionaryService().getAttributeLabel(getBusinessObjectClass(), attributeName);
969 
970                 String attributeValue = (String) fieldValues.get(attributeName);
971 
972                 // check for required if field does not have value
973                 if (StringUtils.isBlank(attributeValue)) {
974                     if ((getBusinessObjectDictionaryService().getLookupAttributeRequired(getBusinessObjectClass(), attributeName)).booleanValue()) {
975                         GlobalVariables.getMessageMap().putError(attributeName, RiceKeyConstants.ERROR_REQUIRED, attributeLabel);
976                     }
977                 }
978                 validateSearchParameterWildcardAndOperators(attributeName, attributeValue);
979             }
980         }
981 
982         if (GlobalVariables.getMessageMap().hasErrors()) {
983             throw new ValidationException("errors in search criteria");
984         }
985     }
986 
987     protected void validateSearchParameterWildcardAndOperators(String attributeName, String attributeValue) {
988         if (StringUtils.isBlank(attributeValue))
989             return;
990 
991         // make sure a wildcard/operator is in the value
992         boolean found = false;
993         for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
994             String queryCharacter = op.op();
995 
996             if (attributeValue.contains(queryCharacter)) {
997                 found = true;
998             }
999         }
1000         if (!found)
1001             return;
1002 
1003         String attributeLabel = getDataDictionaryService().getAttributeLabel(getBusinessObjectClass(), attributeName);
1004         if (getBusinessObjectDictionaryService().isLookupFieldTreatWildcardsAndOperatorsAsLiteral(businessObjectClass, attributeName)) {
1005             BusinessObject example = null;
1006             try {
1007                 example = (BusinessObject) businessObjectClass.newInstance();
1008             } catch (Exception e) {
1009                 LOG.error("Exception caught instantiating " + businessObjectClass.getName(), e);
1010                 throw new RuntimeException("Cannot instantiate " + businessObjectClass.getName(), e);
1011             }
1012 
1013             Class propertyType = ObjectUtils.getPropertyType(example, attributeName, getPersistenceStructureService());
1014             if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {
1015                 GlobalVariables.getMessageMap().putError(attributeName, RiceKeyConstants.ERROR_WILDCARDS_AND_OPERATORS_NOT_ALLOWED_ON_FIELD, attributeLabel);
1016             }
1017             if (TypeUtils.isStringClass(propertyType)) {
1018                 GlobalVariables.getMessageMap().putInfo(attributeName, RiceKeyConstants.INFO_WILDCARDS_AND_OPERATORS_TREATED_LITERALLY, attributeLabel);
1019             }
1020         } else {
1021             if (getBusinessObjectAuthorizationService().attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, attributeName)) {
1022                 if (!attributeValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
1023                     // encrypted values usually come from the DB, so we don't need to filter for wildcards
1024 
1025                     // wildcards are not allowed on restricted fields, because they are typically encrypted, and wildcard searches cannot be performed without
1026                     // decrypting every row, which is currently not supported by KNS
1027 
1028                     GlobalVariables.getMessageMap().putError(attributeName, RiceKeyConstants.ERROR_SECURE_FIELD, attributeLabel);
1029                 }
1030             }
1031         }
1032     }
1033 
1034     /**
1035      * Constructs the list of rows for the search fields. All properties for the field objects come
1036      * from the DataDictionary. To be called by setBusinessObject
1037      */
1038     protected void setRows() {
1039         List<String> lookupFieldAttributeList = null;
1040         if (getBusinessObjectMetaDataService().isLookupable(getBusinessObjectClass())) {
1041             lookupFieldAttributeList = getBusinessObjectMetaDataService().getLookupableFieldNames(
1042                     getBusinessObjectClass());
1043         }
1044         if (lookupFieldAttributeList == null) {
1045             throw new RuntimeException("Lookup not defined for business object " + getBusinessObjectClass());
1046         }
1047 
1048         // construct field object for each search attribute
1049         List fields = new ArrayList();
1050         try {
1051             fields = FieldUtils.createAndPopulateFieldsForLookup(lookupFieldAttributeList, getReadOnlyFieldsList(),
1052                     getBusinessObjectClass());
1053         } catch (InstantiationException e) {
1054             throw new RuntimeException("Unable to create instance of business object class" + e.getMessage());
1055         } catch (IllegalAccessException e) {
1056             throw new RuntimeException("Unable to create instance of business object class" + e.getMessage());
1057         }
1058 
1059         int numCols = getBusinessObjectDictionaryService().getLookupNumberOfColumns(this.getBusinessObjectClass());
1060 
1061         this.rows = FieldUtils.wrapFields(fields, numCols);
1062     }
1063 
1064     public List<Row> getRows() {
1065         return rows;
1066     }
1067 
1068     public abstract List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues);
1069 
1070     /**
1071      * This implementation of this method throws an UnsupportedOperationException, since not every implementation
1072      * may actually want to use this operation.  Subclasses desiring other behaviors
1073      * will need to override this.
1074      *
1075      * @see org.kuali.core.lookup.LookupableHelperService#getSearchResultsUnbounded(java.util.Map)
1076      */
1077     public List<? extends BusinessObject> getSearchResultsUnbounded(Map<String, String> fieldValues) {
1078         throw new UnsupportedOperationException("Lookupable helper services do not always support getSearchResultsUnbounded");
1079     }
1080 
1081     /**
1082      * Performs the lookup and returns a collection of lookup items
1083      *
1084      * @param lookupForm
1085      * @param resultTable
1086      * @param bounded
1087      * @return
1088      */
1089     public Collection<? extends BusinessObject> performLookup(LookupForm lookupForm, Collection<ResultRow> resultTable, boolean bounded) {
1090         Map lookupFormFields = lookupForm.getFieldsForLookup();
1091 
1092         setBackLocation((String) lookupForm.getFieldsForLookup().get(KRADConstants.BACK_LOCATION));
1093         setDocFormKey((String) lookupForm.getFieldsForLookup().get(KRADConstants.DOC_FORM_KEY));
1094         Collection<? extends BusinessObject> displayList;
1095 
1096         LookupUtils.preProcessRangeFields(lookupFormFields);
1097 
1098         Map fieldsForLookup = new HashMap(lookupForm.getFieldsForLookup());
1099         // call search method to get results
1100         if (bounded) {
1101             displayList = getSearchResults(lookupForm.getFieldsForLookup());
1102         } else {
1103             displayList = getSearchResultsUnbounded(lookupForm.getFieldsForLookup());
1104         }
1105 
1106         boolean hasReturnableRow = false;
1107 
1108         List returnKeys = getReturnKeys();
1109         List pkNames = getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(getBusinessObjectClass());
1110         Person user = GlobalVariables.getUserSession().getPerson();
1111 
1112         // iterate through result list and wrap rows with return url and action
1113         // urls
1114         for (Iterator iter = displayList.iterator(); iter.hasNext();) {
1115             BusinessObject element = (BusinessObject) iter.next();
1116             BusinessObject baseElement = element;
1117             //if ebo, then use base BO to get lookupId and find restrictions
1118             //we don't need to do this anymore as the BO is required to implement the EBO interface as of this time
1119             //if this needs reimplemented in the future, one should consider what happens/needs to happen
1120             //with the base BO fields (OBJ ID in particular) as they are all null/empty on new instantiation
1121             //which will fail if we try to depend on any values within it.
1122             //KULRICE-7223
1123 //            if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObject(element.getClass())) {
1124 //                try {
1125 //                    baseElement = (BusinessObject)this.getBusinessObjectClass().newInstance();
1126 //                } catch (InstantiationException e) {
1127 //                    e.printStackTrace();
1128 //                } catch (IllegalAccessException e) {
1129 //                    e.printStackTrace();
1130 //                }
1131 //            }
1132 
1133             final String lookupId = KNSServiceLocator.getLookupResultsService().getLookupId(baseElement);
1134             if (lookupId != null) {
1135                 lookupForm.setLookupObjectId(lookupId);
1136             }
1137 
1138             BusinessObjectRestrictions businessObjectRestrictions = getBusinessObjectAuthorizationService()
1139                     .getLookupResultRestrictions(element, user);
1140 
1141             HtmlData returnUrl = getReturnUrl(element, lookupForm, returnKeys, businessObjectRestrictions);
1142             String actionUrls = getActionUrls(element, pkNames, businessObjectRestrictions);
1143             // Fix for JIRA - KFSMI-2417
1144             if ("".equals(actionUrls)) {
1145                 actionUrls = ACTION_URLS_EMPTY;
1146             }
1147 
1148             List<Column> columns = getColumns();
1149             for (Iterator iterator = columns.iterator(); iterator.hasNext();) {
1150                 Column col = (Column) iterator.next();
1151 
1152                 String propValue = ObjectUtils.getFormattedPropertyValue(element, col.getPropertyName(), col.getFormatter());
1153                 Class propClass = getPropertyClass(element, col.getPropertyName());
1154 
1155                 col.setComparator(CellComparatorHelper.getAppropriateComparatorForPropertyClass(propClass));
1156                 col.setValueComparator(CellComparatorHelper.getAppropriateValueComparatorForPropertyClass(propClass));
1157 
1158                 String propValueBeforePotientalMasking = propValue;
1159                 propValue = maskValueIfNecessary(element.getClass(), col.getPropertyName(), propValue,
1160                         businessObjectRestrictions);
1161                 col.setPropertyValue(propValue);
1162 
1163                 // if property value is masked, don't display additional or alternate properties, or allow totals
1164                 if (StringUtils.equals(propValueBeforePotientalMasking, propValue)) {
1165                     if (StringUtils.isNotBlank(col.getAlternateDisplayPropertyName())) {
1166                         String alternatePropertyValue = ObjectUtils.getFormattedPropertyValue(element, col
1167                                 .getAlternateDisplayPropertyName(), null);
1168                         col.setPropertyValue(alternatePropertyValue);
1169                     }
1170 
1171                     if (StringUtils.isNotBlank(col.getAdditionalDisplayPropertyName())) {
1172                         String additionalPropertyValue = ObjectUtils.getFormattedPropertyValue(element, col
1173                                 .getAdditionalDisplayPropertyName(), null);
1174                         col.setPropertyValue(col.getPropertyValue() + " *-* " + additionalPropertyValue);
1175                     }
1176                 } else {
1177                     col.setTotal(false);
1178                 }
1179 
1180                 if (col.isTotal()) {
1181                     Object unformattedPropValue = ObjectUtils.getPropertyValue(element, col.getPropertyName());
1182                     col.setUnformattedPropertyValue(unformattedPropValue);
1183                 }
1184 
1185                 if (StringUtils.isNotBlank(propValue)) {
1186                     col.setColumnAnchor(getInquiryUrl(element, col.getPropertyName()));
1187                 }
1188             }
1189 
1190             ResultRow row = new ResultRow(columns, returnUrl.constructCompleteHtmlTag(), actionUrls);
1191             row.setRowId(returnUrl.getName());
1192             row.setReturnUrlHtmlData(returnUrl);
1193 
1194             // because of concerns of the BO being cached in session on the
1195             // ResultRow,
1196             // let's only attach it when needed (currently in the case of
1197             // export)
1198             if (getBusinessObjectDictionaryService().isExportable(getBusinessObjectClass())) {
1199                 row.setBusinessObject(element);
1200             }
1201 
1202             if (lookupId != null) {
1203                 row.setObjectId(lookupId);
1204             }
1205 
1206             boolean rowReturnable = isResultReturnable(element);
1207             row.setRowReturnable(rowReturnable);
1208             if (rowReturnable) {
1209                 hasReturnableRow = true;
1210             }
1211             resultTable.add(row);
1212         }
1213 
1214         lookupForm.setHasReturnableRow(hasReturnableRow);
1215 
1216         return displayList;
1217     }
1218 
1219     /**
1220      * Gets the Class for the property in the given BusinessObject instance, if
1221      * property is not accessible then runtime exception is thrown
1222      *
1223      * @param element      BusinessObject instance that contains property
1224      * @param propertyName Name of property in BusinessObject to get class for
1225      * @return Type for property as Class
1226      */
1227     protected Class getPropertyClass(BusinessObject element, String propertyName) {
1228         Class propClass = null;
1229 
1230         try {
1231             propClass = ObjectUtils.getPropertyType(element, propertyName, getPersistenceStructureService());
1232 
1233         } catch (Exception e) {
1234             throw new RuntimeException("Cannot access PropertyType for property " + "'" + propertyName + "' "
1235                     + " on an instance of '" + element.getClass().getName() + "'.", e);
1236         }
1237 
1238         return propClass;
1239     }
1240 
1241 
1242 
1243     protected String maskValueIfNecessary(Class businessObjectClass, String propertyName, String propertyValue, BusinessObjectRestrictions businessObjectRestrictions) {
1244         String maskedPropertyValue = propertyValue;
1245         if (businessObjectRestrictions != null) {
1246             FieldRestriction fieldRestriction = businessObjectRestrictions.getFieldRestriction(propertyName);
1247             if (fieldRestriction != null && (fieldRestriction.isMasked() || fieldRestriction.isPartiallyMasked())) {
1248                 maskedPropertyValue = fieldRestriction.getMaskFormatter().maskValue(propertyValue);
1249             }
1250         }
1251         return maskedPropertyValue;
1252     }
1253 
1254 
1255     protected void setReferencesToRefresh(String referencesToRefresh) {
1256         this.referencesToRefresh = referencesToRefresh;
1257     }
1258 
1259     public String getReferencesToRefresh() {
1260         return referencesToRefresh;
1261     }
1262 
1263     protected SequenceAccessorService getSequenceAccessorService() {
1264         return sequenceAccessorService != null ? sequenceAccessorService : KRADServiceLocator
1265                 .getSequenceAccessorService();
1266     }
1267 
1268     public void setSequenceAccessorService(SequenceAccessorService sequenceAccessorService) {
1269         this.sequenceAccessorService = sequenceAccessorService;
1270     }
1271 
1272     public BusinessObjectService getBusinessObjectService() {
1273         return businessObjectService != null ? businessObjectService : KRADServiceLocator.getBusinessObjectService();
1274     }
1275 
1276     public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1277         this.businessObjectService = businessObjectService;
1278     }
1279 
1280     protected LookupResultsService getLookupResultsService() {
1281         return lookupResultsService != null ? lookupResultsService : KNSServiceLocator.getLookupResultsService();
1282     }
1283 
1284     public void setLookupResultsService(LookupResultsService lookupResultsService) {
1285         this.lookupResultsService = lookupResultsService;
1286     }
1287 
1288     /**
1289      * @return false always, subclasses should override to do something smarter
1290      * @see org.kuali.core.lookup.LookupableHelperService#isSearchUsingOnlyPrimaryKeyValues()
1291      */
1292     public boolean isSearchUsingOnlyPrimaryKeyValues() {
1293         // by default, this implementation returns false, as lookups may not necessarily support this
1294         return false;
1295     }
1296 
1297     /**
1298      * Returns "N/A"
1299      *
1300      * @return "N/A"
1301      * @see org.kuali.core.lookup.LookupableHelperService#getPrimaryKeyFieldLabels()
1302      */
1303     public String getPrimaryKeyFieldLabels() {
1304         return KRADConstants.NOT_AVAILABLE_STRING;
1305     }
1306 
1307     /**
1308      * @see org.kuali.core.lookup.LookupableHelperService#isResultReturnable(org.kuali.core.bo.BusinessObject)
1309      */
1310     public boolean isResultReturnable(BusinessObject object) {
1311         return true;
1312     }
1313 
1314     /**
1315      * This method does the logic for the clear action.
1316      *
1317      * @see LookupableHelperService#performClear()
1318      */
1319     public void performClear(LookupForm lookupForm) {
1320         for (Iterator iter = this.getRows().iterator(); iter.hasNext();) {
1321             Row row = (Row) iter.next();
1322             for (Iterator iterator = row.getFields().iterator(); iterator.hasNext();) {
1323                 Field field = (Field) iterator.next();
1324                 if (field.isSecure()) {
1325                     field.setSecure(false);
1326                     field.setDisplayMaskValue(null);
1327                     field.setEncryptedValue(null);
1328                 }
1329 
1330                 if (!field.getFieldType().equals(Field.RADIO)) {
1331                     field.setPropertyValue(field.getDefaultValue());
1332                 }
1333             }
1334         }
1335     }
1336 
1337     /**
1338      * @see LookupableHelperService#shouldDisplayHeaderNonMaintActions()
1339      */
1340     public boolean shouldDisplayHeaderNonMaintActions() {
1341         return true;
1342     }
1343 
1344     /**
1345      * @see LookupableHelperService#shouldDisplayLookupCriteria()
1346      */
1347     public boolean shouldDisplayLookupCriteria() {
1348         return true;
1349     }
1350 
1351     /**
1352      * @see LookupableHelperService#getSupplementalMenuBar()
1353      */
1354     public String getSupplementalMenuBar() {
1355         return new String();
1356     }
1357 
1358     /**
1359      * @see LookupableHelperService#getTitle()
1360      */
1361     public String getTitle() {
1362         return getBusinessObjectDictionaryService().getLookupTitle(getBusinessObjectClass());
1363     }
1364 
1365     /**
1366      * @see LookupableHelperService#performCustomAction(boolean)
1367      */
1368     public boolean performCustomAction(boolean ignoreErrors) {
1369         return false;
1370     }
1371 
1372     /**
1373      * @see Lookupable#getExtraField()
1374      */
1375     public Field getExtraField() {
1376         return null;
1377     }
1378 
1379     public boolean allowsNewOrCopyAction(String documentTypeName) {
1380         throw new UnsupportedOperationException("Function not supported.");
1381     }
1382 
1383     /**
1384      * Functional requirements state that users are able to perform searches using criteria values that they are not allowed to view.
1385      *
1386      * @see LookupableHelperService#applyFieldAuthorizationsFromNestedLookups(org.kuali.rice.krad.web.ui.Field)
1387      */
1388     public void applyFieldAuthorizationsFromNestedLookups(Field field) {
1389         BusinessObjectAuthorizationService boAuthzService = this.getBusinessObjectAuthorizationService();
1390         if (!Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
1391             if (field.getPropertyValue() != null && field.getPropertyValue().endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
1392                 if (boAuthzService.attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, field.getPropertyName())) {
1393                     AttributeSecurity attributeSecurity = getDataDictionaryService().getAttributeSecurity(businessObjectClass.getName(), field.getPropertyName());
1394                     Person user = GlobalVariables.getUserSession().getPerson();
1395                     String decryptedValue;
1396                     try {
1397                         String cipherText = StringUtils.removeEnd(field.getPropertyValue(), EncryptionService.ENCRYPTION_POST_PREFIX);
1398                         decryptedValue = getEncryptionService().decrypt(cipherText);
1399                     } catch (GeneralSecurityException e) {
1400                         throw new RuntimeException("Error decrypting value for business object " + businessObjectClass + " attribute " + field.getPropertyName(), e);
1401                     }
1402                     if (attributeSecurity.isMask() && !boAuthzService.canFullyUnmaskField(user,
1403                             businessObjectClass, field.getPropertyName(), null)) {
1404                         MaskFormatter maskFormatter = attributeSecurity.getMaskFormatter();
1405                         field.setEncryptedValue(field.getPropertyValue());
1406                         field.setDisplayMaskValue(maskFormatter.maskValue(decryptedValue));
1407                         field.setSecure(true);
1408                     } else if (attributeSecurity.isPartialMask() && !boAuthzService.canPartiallyUnmaskField(user,
1409                             businessObjectClass, field.getPropertyName(), null)) {
1410                         MaskFormatter maskFormatter = attributeSecurity.getPartialMaskFormatter();
1411                         field.setEncryptedValue(field.getPropertyValue());
1412                         field.setDisplayMaskValue(maskFormatter.maskValue(decryptedValue));
1413                         field.setSecure(true);
1414                     } else {
1415                         field.setPropertyValue(org.kuali.rice.krad.lookup.LookupUtils
1416                                 .forceUppercase(businessObjectClass, field.getPropertyName(), decryptedValue));
1417                     }
1418                 } else {
1419                     throw new RuntimeException("Field " + field.getPersonNameAttributeName() + " was encrypted on " + businessObjectClass.getName() +
1420                             " lookup was encrypted when it should not have been encrypted according to the data dictionary.");
1421                 }
1422             }
1423         } else {
1424             if (boAuthzService.attributeValueNeedsToBeEncryptedOnFormsAndLinks(businessObjectClass, field.getPropertyName())) {
1425                 LOG.error("Cannot handle multiple value field types that have field authorizations, please implement custom lookupable helper service");
1426                 throw new RuntimeException("Cannot handle multiple value field types that have field authorizations.");
1427             }
1428         }
1429     }
1430 
1431     /**
1432      * Calls methods that can be overridden by child lookupables to implement conditional logic for setting
1433      * read-only, required, and hidden attributes. Called in the last part of the lookup lifecycle so the
1434      * fields values that will be sent will be correctly reflected in the rows (like after a clear).
1435      *
1436      * @see #getConditionallyReadOnlyPropertyNames()
1437      * @see #getConditionallyRequiredPropertyNames()
1438      * @see #getConditionallyHiddenPropertyNames()
1439      * @see LookupableHelperService#applyConditionalLogicForFieldDisplay()
1440      */
1441     public void applyConditionalLogicForFieldDisplay() {
1442         Set<String> readOnlyFields = getConditionallyReadOnlyPropertyNames();
1443         Set<String> requiredFields = getConditionallyRequiredPropertyNames();
1444         Set<String> hiddenFields = getConditionallyHiddenPropertyNames();
1445 
1446         for (Iterator iter = this.getRows().iterator(); iter.hasNext();) {
1447             Row row = (Row) iter.next();
1448             for (Iterator iterator = row.getFields().iterator(); iterator.hasNext();) {
1449                 Field field = (Field) iterator.next();
1450 
1451                 if (readOnlyFields != null && readOnlyFields.contains(field.getPropertyName())) {
1452                     field.setReadOnly(true);
1453                 }
1454 
1455                 if (requiredFields != null && requiredFields.contains(field.getPropertyName())) {
1456                     field.setFieldRequired(true);
1457                 }
1458 
1459                 if (hiddenFields != null && hiddenFields.contains(field.getPropertyName())) {
1460                     field.setFieldType(Field.HIDDEN);
1461                 }
1462             }
1463         }
1464     }
1465 
1466     /**
1467      * @return Set of property names that should be set as read only based on the current search
1468      *         contents, note request parms containing search field values can be retrieved with
1469      *         {@link #getParameters()}
1470      */
1471     public Set<String> getConditionallyReadOnlyPropertyNames() {
1472         return new HashSet<String>();
1473     }
1474 
1475     /**
1476      * @return Set of property names that should be set as required based on the current search
1477      *         contents, note request parms containing search field values can be retrieved with
1478      *         {@link #getParameters()}
1479      */
1480     public Set<String> getConditionallyRequiredPropertyNames() {
1481         return new HashSet<String>();
1482     }
1483 
1484     /**
1485      * @return Set of property names that should be set as hidden based on the current search
1486      *         contents, note request parms containing search field values can be retrieved with
1487      *         {@link #getParameters()}
1488      */
1489     public Set<String> getConditionallyHiddenPropertyNames() {
1490         return new HashSet<String>();
1491     }
1492 
1493     /**
1494      * Helper method to get the value for a property out of the row-field graph. If property is
1495      * multi-value then the values will be joined by a semi-colon.
1496      *
1497      * @param propertyName - name of property to retrieve value for
1498      * @return current property value as a String
1499      */
1500     protected String getCurrentSearchFieldValue(String propertyName) {
1501         String currentValue = null;
1502 
1503         boolean fieldFound = false;
1504         for (Iterator iter = this.getRows().iterator(); iter.hasNext();) {
1505             Row row = (Row) iter.next();
1506             for (Iterator iterator = row.getFields().iterator(); iterator.hasNext();) {
1507                 Field field = (Field) iterator.next();
1508 
1509                 if (StringUtils.equalsIgnoreCase(propertyName, field.getPropertyName())) {
1510                     if (Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType())) {
1511                         currentValue = StringUtils.join(field.getPropertyValues(), ";");
1512                     } else {
1513                         currentValue = field.getPropertyValue();
1514                     }
1515                     fieldFound = true;
1516                 }
1517 
1518                 if (fieldFound) {
1519                     break;
1520                 }
1521             }
1522 
1523             if (fieldFound) {
1524                 break;
1525             }
1526         }
1527 
1528         return currentValue;
1529     }
1530 }