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