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