View Javadoc

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