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