View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.lookup;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.CoreApiServiceLocator;
21  import org.kuali.rice.core.api.encryption.EncryptionService;
22  import org.kuali.rice.core.api.search.SearchOperator;
23  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
24  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
25  import org.kuali.rice.krad.data.KradDataServiceLocator;
26  import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
27  import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
28  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
29  import org.kuali.rice.krad.service.ModuleService;
30  import org.kuali.rice.krad.uif.UifConstants;
31  import org.kuali.rice.krad.uif.UifPropertyPaths;
32  import org.kuali.rice.krad.uif.lifecycle.ComponentPostMetadata;
33  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
34  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
35  import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
36  import org.kuali.rice.krad.util.KRADConstants;
37  import org.kuali.rice.krad.util.KRADPropertyConstants;
38  import org.kuali.rice.krad.util.KRADUtils;
39  import org.kuali.rice.krad.web.form.UifFormBase;
40  import org.springframework.beans.PropertyAccessorUtils;
41  
42  import javax.servlet.http.HttpServletRequest;
43  
44  import java.sql.Date;
45  import java.sql.Timestamp;
46  import java.text.ParseException;
47  import java.util.ArrayList;
48  import java.util.Calendar;
49  import java.util.Collections;
50  import java.util.HashMap;
51  import java.util.HashSet;
52  import java.util.List;
53  import java.util.Map;
54  import java.util.Set;
55  
56  /**
57   * Provides static utility methods for use within the lookup framework.
58   *
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  public class LookupUtils {
62      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupUtils.class);
63      private static final String[] searchList = new String[SearchOperator.QUERY_CHARACTERS.size()];
64  
65      static {
66          int index = 0;
67          for (SearchOperator operator : SearchOperator.QUERY_CHARACTERS) {
68              searchList[index++] = operator.op();
69          }
70      }
71  
72      private static final String[] replacementList = Collections.nCopies(searchList.length, "").toArray(new String[0]);
73  
74      private LookupUtils() {}
75  
76      /**
77       * Retrieves the value for the given parameter name to send as a lookup parameter.
78       *
79       * @param form form instance to retrieve values from
80       * @param request request object to retrieve parameters from
81       * @param lookupObjectClass data object class associated with the lookup, used to check whether the
82       * value needs to be encyrpted
83       * @param propertyName name of the property associated with the parameter, used to check whether the
84       * value needs to be encrypted
85       * @param parameterName name of the parameter to retrieve the value for
86       * @return String parameter value or empty string if no value was found
87       */
88      public static String retrieveLookupParameterValue(UifFormBase form, HttpServletRequest request,
89              Class<?> lookupObjectClass, String propertyName, String parameterName) {
90          // return a null value if it is secure
91          if (KRADUtils.isSecure(propertyName, lookupObjectClass)) {
92              LOG.warn("field name " + propertyName + " is a secure value and not returned in parameter result value");
93              return null;
94          }
95  
96          String parameterValue = "";
97  
98          // get literal parameter values first
99          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 }