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