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