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.krad.lookup;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.encryption.EncryptionService;
022import org.kuali.rice.core.api.search.SearchOperator;
023import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
024import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
025import org.kuali.rice.krad.data.KradDataServiceLocator;
026import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
027import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
028import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
029import org.kuali.rice.krad.service.ModuleService;
030import org.kuali.rice.krad.uif.UifConstants;
031import org.kuali.rice.krad.uif.UifPropertyPaths;
032import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata;
033import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
034import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
035import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
036import org.kuali.rice.krad.util.KRADConstants;
037import org.kuali.rice.krad.util.KRADPropertyConstants;
038import org.kuali.rice.krad.util.KRADUtils;
039import org.kuali.rice.krad.web.form.UifFormBase;
040import org.springframework.beans.PropertyAccessorUtils;
041
042import javax.servlet.http.HttpServletRequest;
043
044import java.sql.Date;
045import java.sql.Timestamp;
046import java.text.ParseException;
047import java.util.ArrayList;
048import java.util.Calendar;
049import java.util.Collections;
050import java.util.HashMap;
051import java.util.HashSet;
052import java.util.List;
053import java.util.Map;
054import java.util.Set;
055
056/**
057 * Provides static utility methods for use within the lookup framework.
058 *
059 * @author Kuali Rice Team (rice.collab@kuali.org)
060 */
061public class LookupUtils {
062    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupUtils.class);
063    private static final String[] searchList = new String[SearchOperator.QUERY_CHARACTERS.size()];
064
065    static {
066        int index = 0;
067        for (SearchOperator operator : SearchOperator.QUERY_CHARACTERS) {
068            searchList[index++] = operator.op();
069        }
070    }
071
072    private static final String[] replacementList = Collections.nCopies(searchList.length, "").toArray(new String[0]);
073
074    private LookupUtils() {}
075
076    /**
077     * Retrieves the value for the given parameter name to send as a lookup parameter.
078     *
079     * @param form form instance to retrieve values from
080     * @param request request object to retrieve parameters from
081     * @param lookupObjectClass data object class associated with the lookup, used to check whether the
082     * value needs to be encyrpted
083     * @param propertyName name of the property associated with the parameter, used to check whether the
084     * value needs to be encrypted
085     * @param parameterName name of the parameter to retrieve the value for
086     * @return String parameter value or empty string if no value was found
087     */
088    public static String retrieveLookupParameterValue(UifFormBase form, HttpServletRequest request,
089            Class<?> lookupObjectClass, String propertyName, String parameterName) {
090        // return a null value if it is secure
091        if (KRADUtils.isSecure(propertyName, lookupObjectClass)) {
092            LOG.warn("field name " + propertyName + " is a secure value and not returned in parameter result value");
093            return null;
094        }
095
096        String parameterValue = "";
097
098        // get literal parameter values first
099        if (StringUtils.startsWith(parameterName, "'") && StringUtils.endsWith(parameterName, "'")) {
100            parameterValue = StringUtils.substringBetween(parameterName, "'");
101        } else if (parameterValue.startsWith(KRADConstants.LOOKUP_PARAMETER_LITERAL_PREFIX
102                + KRADConstants.LOOKUP_PARAMETER_LITERAL_DELIMITER)) {
103            parameterValue = StringUtils.removeStart(parameterValue, KRADConstants.LOOKUP_PARAMETER_LITERAL_PREFIX
104                    + KRADConstants.LOOKUP_PARAMETER_LITERAL_DELIMITER);
105        }
106        // check if parameter is in request
107        else if (request.getParameterMap().containsKey(parameterName)) {
108            parameterValue = request.getParameter(parameterName);
109        }
110        // get parameter value from form object
111        else {
112            parameterValue = ObjectPropertyUtils.getPropertyValueAsText(form, parameterName);
113        }
114
115        return parameterValue;
116    }
117
118    /**
119     * Retrieves the default KRAD base lookup URL, used to build lookup URLs in code
120     *
121     * @return String base lookup URL (everything except query string)
122     */
123    public static String getBaseLookupUrl() {
124        return CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
125                KRADConstants.KRAD_LOOKUP_URL_KEY);
126    }
127
128    /**
129     * Uses the DataDictionary to determine whether to force uppercase the value, and if it should, then it does the
130     * uppercase, and returns the upper-cased value.
131     *
132     * @param dataObjectClass parent DO class that the fieldName is a member of
133     * @param fieldName name of the field to be forced to uppercase
134     * @param fieldValue value of the field that may be uppercased
135     * @return the correctly uppercased fieldValue if it should be uppercased, otherwise fieldValue is returned
136     *         unchanged
137     */
138    public static String forceUppercase(Class<?> dataObjectClass, String fieldName, String fieldValue) {
139        // short-circuit to exit if there isnt enough information to do the forceUppercase
140        if (StringUtils.isBlank(fieldValue)) {
141            return fieldValue;
142        }
143
144        // parameter validation
145        if (dataObjectClass == null) {
146            throw new IllegalArgumentException("Parameter dataObjectClass passed in with null value.");
147        }
148
149        if (StringUtils.isBlank(fieldName)) {
150            throw new IllegalArgumentException("Parameter fieldName passed in with empty value.");
151        }
152
153        if (!KRADServiceLocatorWeb.getDataDictionaryService().isAttributeDefined(dataObjectClass, fieldName)
154                .booleanValue()) {
155            return fieldValue;
156        }
157
158        boolean forceUpperCase = false;
159        try {
160            forceUpperCase = KRADServiceLocatorWeb.getDataDictionaryService()
161                    .getAttributeForceUppercase(dataObjectClass, fieldName).booleanValue();
162        } catch (UnknownBusinessClassAttributeException ubae) {
163            // do nothing, don't alter the fieldValue
164        }
165
166        if (forceUpperCase && !fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
167            return fieldValue.toUpperCase();
168        }
169
170        return fieldValue;
171    }
172
173    /**
174     * Uses the DataDictionary to determine whether to force uppercase the values, and if it should, then it does the
175     * uppercase, and returns the upper-cased Map of fieldname/fieldValue pairs.
176     *
177     * @param dataObjectClass parent DO class that the fieldName is a member of
178     * @param fieldValues a Map<String,String> where the key is the fieldName and the value is the fieldValue
179     * @return the same Map is returned, with the appropriate values uppercased (if any)
180     */
181    public static Map<String, String> forceUppercase(Class<?> dataObjectClass, Map<String, String> fieldValues) {
182        if (dataObjectClass == null) {
183            throw new IllegalArgumentException("Parameter boClass passed in with null value.");
184        }
185
186        if (fieldValues == null) {
187            throw new IllegalArgumentException("Parameter fieldValues passed in with null value.");
188        }
189
190        for (String fieldName : fieldValues.keySet()) {
191            fieldValues.put(fieldName, forceUppercase(dataObjectClass, fieldName, fieldValues.get(fieldName)));
192        }
193
194        return fieldValues;
195    }
196
197    /**
198     * Parses and returns the lookup result set limit, checking first for the limit for the specific view,
199     * then the class being looked up, and then the global application limit if there isn't a limit specific
200     * to this data object class.
201     *
202     * @param dataObjectClass class to get limit for
203     * @param lookupForm lookupForm to use.  May be null if the form is unknown. If lookupForm is null, only the
204     * dataObjectClass will be used to find the search results set limit
205     * @return result set limit
206     */
207    public static Integer getSearchResultsLimit(Class dataObjectClass, LookupForm lookupForm) {
208        Integer limit = KRADServiceLocatorWeb.getViewDictionaryService().getResultSetLimitForLookup(dataObjectClass,
209                lookupForm);
210        if (limit == null) {
211            limit = getApplicationSearchResultsLimit();
212        }
213
214        return limit;
215    }
216
217    /**
218     * Retrieves the default application search limit configured through a system parameter.
219     *
220     * @return default result set limit of the application
221     */
222    public static Integer getApplicationSearchResultsLimit() {
223        String limitString = CoreFrameworkServiceLocator.getParameterService()
224                .getParameterValueAsString(KRADConstants.KRAD_NAMESPACE,
225                        KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE,
226                        KRADConstants.SystemGroupParameterNames.LOOKUP_RESULTS_LIMIT);
227        if (limitString != null) {
228            return Integer.valueOf(limitString);
229        }
230
231        return null;
232    }
233
234    /**
235     * Retrieves the default application multiple value search limit configured through a system parameter.
236     *
237     * @return default multiple value result set limit of the application
238     */
239    public static Integer getApplicationMultipleValueSearchResultsLimit() {
240        String limitString = CoreFrameworkServiceLocator.getParameterService()
241                .getParameterValueAsString(KRADConstants.KRAD_NAMESPACE,
242                        KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE,
243                        KRADConstants.SystemGroupParameterNames.MULTIPLE_VALUE_LOOKUP_RESULTS_LIMIT);
244        if (limitString != null) {
245            return Integer.valueOf(limitString);
246        }
247
248        return null;
249    }
250
251    /**
252     * Determines what Timestamp should be used for active queries on effective dated records. Determination made as
253     * follows:
254     *
255     * <ul>
256     * <li>Use activeAsOfDate value from search values Map if value is not empty</li>
257     * <li>If search value given, try to convert to sql date, if conversion fails, try to convert to Timestamp</li>
258     * <li>If search value empty, use current Date</li>
259     * <li>If Timestamp value not given, create Timestamp from given Date setting the time as 1 second before midnight
260     * </ul>
261     *
262     * @param searchValues map containing search key/value pairs
263     * @return timestamp to be used for active criteria
264     */
265    public static Timestamp getActiveDateTimestampForCriteria(Map searchValues) {
266        Date activeDate = CoreApiServiceLocator.getDateTimeService().getCurrentSqlDate();
267
268        Timestamp activeTimestamp = null;
269        if (searchValues.containsKey(KRADPropertyConstants.ACTIVE_AS_OF_DATE)) {
270            String activeAsOfDate = (String) searchValues.get(KRADPropertyConstants.ACTIVE_AS_OF_DATE);
271            if (StringUtils.isNotBlank(activeAsOfDate)) {
272                try {
273                    activeDate = CoreApiServiceLocator.getDateTimeService()
274                            .convertToSqlDate(KRADUtils.clean(activeAsOfDate));
275                } catch (ParseException e) {
276                    // try to parse as timestamp
277                    try {
278                        activeTimestamp = CoreApiServiceLocator.getDateTimeService()
279                                .convertToSqlTimestamp(KRADUtils.clean(activeAsOfDate));
280                    } catch (ParseException e1) {
281                        throw new RuntimeException("Unable to convert date: " + KRADUtils.clean(activeAsOfDate));
282                    }
283                }
284            }
285        }
286
287        // if timestamp not given set to 1 second before midnight on the given date
288        if (activeTimestamp == null) {
289            Calendar cal = Calendar.getInstance();
290
291            cal.setTime(activeDate);
292            cal.set(Calendar.HOUR, cal.getMaximum(Calendar.HOUR));
293            cal.set(Calendar.MINUTE, cal.getMaximum(Calendar.MINUTE));
294            cal.set(Calendar.SECOND, cal.getMaximum(Calendar.SECOND));
295
296            activeTimestamp = new Timestamp(cal.getTime().getTime());
297        }
298
299        return activeTimestamp;
300    }
301
302    /**
303     * Changes from/to dates into the range operators the lookupable dao expects ("..",">" etc) this method modifies
304     * the passed in map and returns an updated search criteria map.
305     *
306     * @param searchCriteria map of criteria currently set for which the date criteria will be adjusted
307     * @return map updated search criteria
308     */
309    public static Map<String, String> preprocessDateFields(Map<String, String> searchCriteria) {
310        Map<String, String> fieldsToUpdate = new HashMap<String, String>();
311        Map<String, String> searchCriteriaUpdated = new HashMap<String, String>(searchCriteria);
312
313        Set<String> fieldsForLookup = searchCriteria.keySet();
314        for (String propName : fieldsForLookup) {
315            if (propName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
316                String from_DateValue = searchCriteria.get(propName);
317                String dateFieldName =
318                        StringUtils.remove(propName, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX);
319                String to_DateValue = searchCriteria.get(dateFieldName);
320                String newPropValue = to_DateValue;
321
322                if (StringUtils.isNotEmpty(from_DateValue) && StringUtils.isNotEmpty(to_DateValue)) {
323                    newPropValue = from_DateValue + SearchOperator.BETWEEN + to_DateValue;
324                } else if (StringUtils.isNotEmpty(from_DateValue) && StringUtils.isEmpty(to_DateValue)) {
325                    newPropValue = SearchOperator.GREATER_THAN_EQUAL.op() + from_DateValue;
326                } else if (StringUtils.isNotEmpty(to_DateValue) && StringUtils.isEmpty(from_DateValue)) {
327                    newPropValue = SearchOperator.LESS_THAN_EQUAL.op() + to_DateValue;
328                } // could optionally continue on else here
329
330                fieldsToUpdate.put(dateFieldName, newPropValue);
331            }
332        }
333
334        // update lookup values from found date values to update
335        Set<String> keysToUpdate = fieldsToUpdate.keySet();
336        for (String updateKey : keysToUpdate) {
337            searchCriteriaUpdated.put(updateKey, fieldsToUpdate.get(updateKey));
338        }
339
340        return searchCriteriaUpdated;
341    }
342
343    /**
344     * Checks whether any of the fieldValues being passed refer to a property within an ExternalizableBusinessObject.
345     *
346     * @param boClass business object class of the lookup
347     * @param fieldValues map of the lookup criteria values
348     * @return true if externalizable business object are contained, false otherwise
349     * @throws IllegalAccessException
350     * @throws InstantiationException
351     */
352    public static boolean hasExternalBusinessObjectProperty(Class<?> boClass,
353            Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
354        Object sampleBo = boClass.newInstance();
355        for (String key : fieldValues.keySet()) {
356            if (isExternalBusinessObjectProperty(sampleBo, key)) {
357                return true;
358            }
359        }
360
361        return false;
362    }
363
364    /**
365     * Check whether the given property represents a property within an EBO starting with the sampleBo object given.
366     * This is used to determine if a criteria needs to be applied to the EBO first,
367     * before sending to the normal lookup DAO.
368     *
369     * @param sampleBo business object of the property to be tested
370     * @param propertyName property name to be tested
371     * @return true if the property is within an externalizable business object.
372     */
373    public static boolean isExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
374        if (propertyName.indexOf(".") > 0 && !StringUtils.contains(propertyName, "add.")) {
375            Class<?> propertyClass =
376                    ObjectPropertyUtils.getPropertyType(sampleBo, StringUtils.substringBeforeLast(propertyName, "."));
377            if (propertyClass != null) {
378                return ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface(propertyClass);
379            }
380        }
381
382        return false;
383    }
384
385    /**
386     * Returns a map stripped of any properties which refer to ExternalizableBusinessObjects. These values may not be
387     * passed into the lookup service, since the objects they refer to are not in the
388     * local database.
389     *
390     * @param boClass business object class of the lookup
391     * @param fieldValues map of lookup criteria from which to remove the externalizable business objects
392     * @return map of lookup criteria without externalizable business objects
393     */
394    public static Map<String, String> removeExternalizableBusinessObjectFieldValues(Class<?> boClass,
395            Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
396        Map<String, String> eboFieldValues = new HashMap<String, String>();
397        Object sampleBo = boClass.newInstance();
398        for (String key : fieldValues.keySet()) {
399            if (!isExternalBusinessObjectProperty(sampleBo, key)) {
400                eboFieldValues.put(key, fieldValues.get(key));
401            }
402        }
403
404        return eboFieldValues;
405    }
406
407    /**
408     * Return the EBO fieldValue entries explicitly for the given eboPropertyName. (I.e., any properties with the given
409     * property name as a prefix.
410     *
411     * @param eboPropertyName the externalizable business object property name to retrieve
412     * @param fieldValues map of lookup criteria
413     * return map of lookup criteria for the given eboPropertyName
414     */
415    public static Map<String, String> getExternalizableBusinessObjectFieldValues(String eboPropertyName,
416            Map<String, String> fieldValues) {
417        Map<String, String> eboFieldValues = new HashMap<String, String>();
418        for (String key : fieldValues.keySet()) {
419            if (key.startsWith(eboPropertyName + ".")) {
420                eboFieldValues.put(StringUtils.substringAfterLast(key, "."), fieldValues.get(key));
421            }
422        }
423
424        return eboFieldValues;
425    }
426
427    /**
428     * Get the complete list of all properties referenced in the fieldValues that are ExternalizableBusinessObjects.
429     *
430     * <p>
431     * This is a list of the EBO object references themselves, not of the properties within them.
432     * </p>
433     *
434     * @param boClass business object class of the lookup
435     * @param fieldValues map of lookup criteria from which to return the externalizable business objects
436     * @return map of lookup criteria that are externalizable business objects
437     * @throws IllegalAccessException
438     * @throws InstantiationException
439     */
440    public static List<String> getExternalizableBusinessObjectProperties(Class<?> boClass,
441            Map<String, String> fieldValues) throws IllegalAccessException, InstantiationException {
442        Set<String> eboPropertyNames = new HashSet<String>();
443
444        Object sampleBo = boClass.newInstance();
445        for (String key : fieldValues.keySet()) {
446            if (isExternalBusinessObjectProperty(sampleBo, key)) {
447                eboPropertyNames.add(StringUtils.substringBeforeLast(key, "."));
448            }
449        }
450
451        return new ArrayList<String>(eboPropertyNames);
452    }
453
454    /**
455     * Given an property on the main BO class, return the defined type of the ExternalizableBusinessObject. This will
456     * be used by other code to determine the correct module service to call for the lookup.
457     *
458     * @param boClass business object class of the lookup
459     * @param propertyName property of which the externalizable business object type is to be determined
460     * @return externalizable business object type
461     * @throws IllegalAccessException
462     * @throws InstantiationException
463     */
464    public static Class<? extends ExternalizableBusinessObject> getExternalizableBusinessObjectClass(Class<?> boClass,
465            String propertyName) throws IllegalAccessException, InstantiationException {
466        return (Class<? extends ExternalizableBusinessObject>) ObjectPropertyUtils
467                .getPropertyType(boClass.newInstance(), StringUtils.substringBeforeLast(propertyName, "."));
468    }
469
470    /**
471     * Looks for criteria against nested EBOs and performs a search against that EBO and updates the criteria.
472     *
473     * @param searchCriteria map of criteria currently set
474     * @param unbounded indicates whether the complete result should be returned.  When set to false the result is
475     * limited (if necessary) to the max search result limit configured.
476     * @return Map of adjusted criteria for nested EBOs
477     * @throws InstantiationException
478     * @throws IllegalAccessException
479     */
480    public static Map<String, String> adjustCriteriaForNestedEBOs(Class<?> dataObjectClass,
481            Map<String, String> searchCriteria,
482            boolean unbounded) throws InstantiationException, IllegalAccessException {
483        // remove the EBO criteria
484        Map<String, String> nonEboFieldValues = removeExternalizableBusinessObjectFieldValues(
485                dataObjectClass, searchCriteria);
486        if (LOG.isDebugEnabled()) {
487            LOG.debug("Non EBO properties removed: " + nonEboFieldValues);
488        }
489
490        // get the list of EBO properties attached to this object
491        List<String> eboPropertyNames = getExternalizableBusinessObjectProperties(dataObjectClass, searchCriteria);
492        if (LOG.isDebugEnabled()) {
493            LOG.debug("EBO properties: " + eboPropertyNames);
494        }
495
496        // loop over those properties
497        for (String eboPropertyName : eboPropertyNames) {
498            // extract the properties as known to the EBO
499            Map<String, String> eboFieldValues = LookupUtils.getExternalizableBusinessObjectFieldValues(eboPropertyName,
500                    searchCriteria);
501            if (LOG.isDebugEnabled()) {
502                LOG.debug("EBO properties for master EBO property: " + eboPropertyName);
503                LOG.debug("properties: " + eboFieldValues);
504            }
505
506            // run search against attached EBO's module service
507            ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
508                    getExternalizableBusinessObjectClass(dataObjectClass, eboPropertyName));
509
510            // KULRICE-4401 made eboResults an empty list and only filled if service is found.
511            List<?> eboResults = Collections.emptyList();
512            if (eboModuleService != null) {
513                eboResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(
514                        getExternalizableBusinessObjectClass(dataObjectClass, eboPropertyName),
515                        (Map) eboFieldValues, unbounded);
516            } else {
517                LOG.debug("EBO ModuleService is null: " + eboPropertyName);
518            }
519
520            // get the parent property type
521            Class<?> eboParentClass;
522            String eboParentPropertyName;
523            if (PropertyAccessorUtils.isNestedOrIndexedProperty(eboPropertyName)) {
524                eboParentPropertyName = StringUtils.substringBeforeLast(eboPropertyName, ".");
525                try {
526                    eboParentClass = KradDataServiceLocator.getDataObjectService().wrap(dataObjectClass.newInstance()).getPropertyType(
527                            eboParentPropertyName);
528                } catch (Exception ex) {
529                    throw new RuntimeException(
530                            "Unable to create an instance of the business object class: " + dataObjectClass
531                                    .getName(), ex);
532                }
533            } else {
534                eboParentClass = dataObjectClass;
535                eboParentPropertyName = null;
536            }
537
538            if (LOG.isDebugEnabled()) {
539                LOG.debug("determined EBO parent class/property name: " + eboParentClass + "/" + eboParentPropertyName);
540            }
541
542            // look that up in the DD (BOMDS) find the appropriate relationship
543            // CHECK THIS: what if eboPropertyName is a nested attribute - need to strip off the
544            // eboParentPropertyName if not null
545            RelationshipDefinition rd = KRADServiceLocatorWeb.getLegacyDataAdapter().getDictionaryRelationship(
546                    eboParentClass, eboPropertyName);
547            if (LOG.isDebugEnabled()) {
548                LOG.debug("Obtained RelationshipDefinition for " + eboPropertyName);
549                LOG.debug(rd);
550            }
551
552            // copy the needed properties (primary only) to the field values KULRICE-4446 do
553            // so only if the relationship definition exists
554            // NOTE: this will work only for single-field PK unless the ORM
555            // layer is directly involved
556            // (can't make (field1,field2) in ( (v1,v2),(v3,v4) ) style
557            // queries in the lookup framework
558            if (KRADUtils.isNotNull(rd)) {
559                if (rd.getPrimitiveAttributes().size() > 1) {
560                    throw new RuntimeException(
561                            "EBO Links don't work for relationships with multiple-field primary keys.");
562                }
563                String boProperty = rd.getPrimitiveAttributes().get(0).getSourceName();
564                String eboProperty = rd.getPrimitiveAttributes().get(0).getTargetName();
565                StringBuffer boPropertyValue = new StringBuffer();
566
567                // loop over the results, making a string that the lookup DAO will convert into an
568                // SQL "IN" clause
569                for (Object ebo : eboResults) {
570                    if (boPropertyValue.length() != 0) {
571                        boPropertyValue.append(SearchOperator.OR.op());
572                    }
573                    try {
574                        boPropertyValue.append(PropertyUtils.getProperty(ebo, eboProperty).toString());
575                    } catch (Exception ex) {
576                        LOG.warn("Unable to get value for " + eboProperty + " on " + ebo);
577                    }
578                }
579
580                if (eboParentPropertyName == null) {
581                    // non-nested property containing the EBO
582                    nonEboFieldValues.put(boProperty, boPropertyValue.toString());
583                } else {
584                    // property nested within the main searched-for BO that contains the EBO
585                    nonEboFieldValues.put(eboParentPropertyName + "." + boProperty, boPropertyValue.toString());
586                }
587            }
588        }
589
590        return nonEboFieldValues;
591    }
592
593    /**
594     * Removes query characters (such as wildcards) from the given string value.
595     *
596     * @param criteriaValue string to clean
597     * @return string with query characters removed
598     */
599    public static String scrubQueryCharacters(String criteriaValue) {
600        return StringUtils.replaceEach(criteriaValue, searchList, replacementList);
601    }
602
603    /**
604     * Generates a key string in case of multivalue return. The values are extracted
605     * from the list of properties on the lineDataObject.
606     *
607     * If fieldConversionKeys is empty return the identifier string for the lineDataObject
608     *
609     * @param lineDataObject   Object from which to extract values
610     * @param fieldConversionKeys List of keys whose values have to be concatenated
611     * @return string representing the multivalue key 
612     */
613    public static String generateMultiValueKey(Object lineDataObject, List<String> fieldConversionKeys) {
614        String lineIdentifier = "";
615
616        if(fieldConversionKeys == null || fieldConversionKeys.isEmpty()) {
617            lineIdentifier =
618                    KRADServiceLocatorWeb.getLegacyDataAdapter().getDataObjectIdentifierString(lineDataObject);
619        } else {
620            Collections.sort(fieldConversionKeys);
621            for (String fromFieldName : fieldConversionKeys) {
622                Object fromFieldValue = ObjectPropertyUtils.getPropertyValue(lineDataObject, fromFieldName);
623
624                if (fromFieldValue != null) {
625                    lineIdentifier += fromFieldValue;
626                }
627
628                lineIdentifier += ":";
629            }
630            lineIdentifier = StringUtils.removeEnd(lineIdentifier, ":");
631        }
632
633        return lineIdentifier;
634    }
635
636    /**
637     * Merges the lookup result selections that are part of the request with the selectedLookupResultsCache maintained in
638     * the session.
639     *
640     * @param form lookup form instance containing the selected results and lookup configuration
641     */
642    public static void refreshLookupResultSelections(LookupForm form) {
643        int displayStart = 0;
644        int displayLength = 0;
645
646        // avoid blowing the stack if the session expired
647        ViewPostMetadata viewPostMetadata = form.getViewPostMetadata();
648        if (viewPostMetadata != null) {
649
650            // only one concurrent request per view please
651            synchronized (viewPostMetadata) {
652                ComponentPostMetadata oldCollectionGroup = viewPostMetadata.getComponentPostMetadata("uLookupResults");
653                displayStart = (Integer) oldCollectionGroup.getData(UifConstants.PostMetadata.COLL_DISPLAY_START);
654                displayLength = (Integer) oldCollectionGroup.getData(UifConstants.PostMetadata.COLL_DISPLAY_LENGTH);
655            }
656        }
657
658        List<? extends Object> lookupResults = (List<? extends Object>) form.getLookupResults();
659        List<String> fromFieldNames = form.getMultiValueReturnFields();
660
661        Set<String> selectedLines = form.getSelectedCollectionLines().get(UifPropertyPaths.LOOKUP_RESULTS);
662        Set<String> selectedLookupResultsCache = form.getSelectedLookupResultsCache();
663
664        selectedLines = (selectedLines == null) ? new HashSet<String>() : selectedLines;
665
666        for(int i = displayStart; i < displayStart + displayLength; i++ ) {
667            if(i >= form.getLookupResults().size()) break;
668
669            Object lineItem = lookupResults.get(i);
670            String lineIdentifier = LookupUtils.generateMultiValueKey(lineItem, fromFieldNames);
671
672            if(!selectedLines.contains(lineIdentifier)) {
673                 selectedLookupResultsCache.remove(lineIdentifier);
674            } else {
675                selectedLookupResultsCache.add(lineIdentifier);
676            }
677        }
678
679        selectedLines.addAll( selectedLookupResultsCache );
680
681        form.getSelectedCollectionLines().put(UifPropertyPaths.LOOKUP_RESULTS, selectedLines);
682    }
683
684}