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