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