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