001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.kns.lookup;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.ojb.broker.query.Criteria;
020    import org.kuali.rice.core.api.search.Range;
021    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
022    import org.kuali.rice.core.api.CoreApiServiceLocator;
023    import org.kuali.rice.core.api.config.property.ConfigurationService;
024    import org.kuali.rice.core.api.datetime.DateTimeService;
025    import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
026    import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
027    import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
028    import org.kuali.rice.kns.service.KNSServiceLocator;
029    import org.kuali.rice.kns.util.KNSGlobalVariables;
030    import org.kuali.rice.kns.web.comparator.NullValueComparator;
031    import org.kuali.rice.kns.web.struts.form.KualiForm;
032    import org.kuali.rice.kns.web.struts.form.LookupForm;
033    import org.kuali.rice.kns.web.ui.Field;
034    import org.kuali.rice.kns.web.ui.ResultRow;
035    import org.kuali.rice.krad.bo.BusinessObject;
036    import org.kuali.rice.krad.bo.DataObjectRelationship;
037    import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
038    import org.kuali.rice.krad.datadictionary.control.ControlDefinition;
039    import org.kuali.rice.krad.exception.ClassNotPersistableException;
040    import org.kuali.rice.krad.lookup.SelectiveReferenceRefresher;
041    import org.kuali.rice.krad.service.DataDictionaryService;
042    import org.kuali.rice.krad.service.KRADServiceLocator;
043    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
044    import org.kuali.rice.krad.service.PersistenceStructureService;
045    import org.kuali.rice.krad.util.KRADConstants;
046    import org.kuali.rice.krad.util.ObjectUtils;
047    
048    import java.util.ArrayList;
049    import java.util.Collection;
050    import java.util.Comparator;
051    import java.util.HashMap;
052    import java.util.HashSet;
053    import java.util.Iterator;
054    import java.util.List;
055    import java.util.Map;
056    import java.util.Set;
057    import java.util.StringTokenizer;
058    
059    /**
060     * Utility class for Lookup related utilities and helper methods.
061     */
062    public class LookupUtils {
063        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LookupUtils.class);
064    
065        public LookupUtils() {
066            // default constructor for Spring to call to start up initialization process
067        }
068    
069        /**
070         * Removes fields identified in the data dictionary as hidden from the lookup field values.
071         * (This will remove Universal User ID and Person name from search requests when a user ID is entered.)
072         *
073         * @param fieldValues
074         */
075        public static void removeHiddenCriteriaFields(Class businessObjectClass, Map fieldValues) {
076            List<String> lookupFieldAttributeList =
077                    getBusinessObjectMetaDataService().getLookupableFieldNames(businessObjectClass);
078            if (lookupFieldAttributeList != null) {
079                for (Iterator iter = lookupFieldAttributeList.iterator(); iter.hasNext(); ) {
080                    String attributeName = (String) iter.next();
081                    if (fieldValues.containsKey(attributeName)) {
082                        ControlDefinition controlDef = getDataDictionaryService()
083                                .getAttributeControlDefinition(businessObjectClass, attributeName);
084                        if (controlDef != null && controlDef.isHidden()) {
085                            fieldValues.remove(attributeName);
086                        }
087                    }
088                }
089            }
090        }
091    
092        /**
093         * Parses and returns the lookup result set limit, checking first for the limit
094         * for the BO being looked up, and then the global application limit if there isn't a limit
095         * specific to this BO.
096         *
097         * @param businessObjectClass BO class to search on / get limit for.  If the passed in type is not of type
098         * {@link org.kuali.rice.krad.bo.BusinessObject}, then the application-wide default limit is used.
099         * @return result set limit (or null if there isn't one)
100         */
101        public static Integer getSearchResultsLimit(Class businessObjectClass) {
102            Integer limit = null;
103            if (BusinessObject.class.isAssignableFrom(businessObjectClass)) {
104                limit = getBusinessObjectSearchResultsLimit(businessObjectClass);
105            }
106            if (limit == null) {
107                limit = getApplicationSearchResultsLimit();
108            }
109            return limit;
110        }
111    
112        /**
113         *
114         */
115        public static Integer getApplicationSearchResultsLimit() {
116            String limitString = CoreFrameworkServiceLocator.getParameterService()
117                    .getParameterValueAsString(KRADConstants.KNS_NAMESPACE,
118                            KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE,
119                            KRADConstants.SystemGroupParameterNames.LOOKUP_RESULTS_LIMIT);
120            if (limitString != null) {
121                return Integer.valueOf(limitString);
122            }
123            return null;
124        }
125    
126        /**
127         * Parses and returns the lookup result set limit for the passed in BO (if one exists)
128         *
129         * @param businessObjectClass
130         * @return result set limit for this BO (or null if the BO doesn't have a limit)
131         */
132        public static Integer getBusinessObjectSearchResultsLimit(Class businessObjectClass) {
133                    if (!(isMultipleValueLookup())) {
134                            return getBusinessObjectDictionaryService().getLookupResultSetLimit(businessObjectClass);
135                    } else {
136                    return getBusinessObjectDictionaryService().getMultipleValueLookupResultSetLimit(businessObjectClass);
137                    }
138        }
139    
140            private static boolean isMultipleValueLookup() {
141                    KualiForm kualiForm = KNSGlobalVariables.getKualiForm();
142                    if (kualiForm instanceof LookupForm) {
143                            LookupForm lookupForm = (LookupForm) kualiForm;
144                            return lookupForm.isMultipleValues();
145                    } else {
146                            return false;
147                    }
148            } 
149            
150        /**
151         * This method applies the search results limit to the search criteria for this BO
152         *
153         * @param businessObjectClass BO class to search on / get limit for
154         * @param criteria search criteria
155         * @param platform database platform
156         */
157        public static void applySearchResultsLimit(Class businessObjectClass, Criteria criteria,
158                DatabasePlatform platform) {
159            Integer limit = getSearchResultsLimit(businessObjectClass);
160            if (limit != null) {
161                platform.applyLimit(limit, criteria);
162            }
163        }
164    
165        /**
166         * Applies the search results limit to the search criteria for this BO (JPA)
167         *
168         * @param businessObjectClass BO class to search on / get limit for
169         * @param criteria search criteria
170         */
171        public static void applySearchResultsLimit(Class businessObjectClass,
172                org.kuali.rice.core.framework.persistence.jpa.criteria.Criteria criteria) {
173            Integer limit = getSearchResultsLimit(businessObjectClass);
174            if (limit != null) {
175                criteria.setSearchLimit(limit);
176            }
177        }
178    
179        /**
180         * This method the maximum rows per page in a multiple value lookup
181         *
182         * @see org.kuali.KRADConstants.SystemGroupParameterNames#MULTIPLE_VALUE_LOOKUP_RESULTS_PER_PAGE
183         * @return
184         */
185        public static Integer getApplicationMaximumSearchResulsPerPageForMultipleValueLookups() {
186            String limitString = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsString(KRADConstants.KNS_NAMESPACE, KRADConstants.DetailTypes.LOOKUP_PARM_DETAIL_TYPE, KRADConstants.SystemGroupParameterNames.MULTIPLE_VALUE_LOOKUP_RESULTS_PER_PAGE);
187            if (limitString != null) {
188                return Integer.valueOf(limitString);
189            }
190            return null;
191        }
192    
193        /**
194         * This makes a , delimited String list of fields separated by a , into a List of target --> lookup readOnlyFieldsList.
195         *
196         * @param readOnlyFields
197         * @return the List representation of the readOnlyFields  String provided.
198         */
199        public static List<String> translateReadOnlyFieldsToList(String readOnlyFieldsString) {
200            List<String> readOnlyFieldsList = new ArrayList<String>();
201            if (StringUtils.isNotEmpty(readOnlyFieldsString)) {
202                if (readOnlyFieldsString.indexOf(",") > 0) {
203                    StringTokenizer token = new StringTokenizer(readOnlyFieldsString, ",");
204                    while (token.hasMoreTokens()) {
205                        String element = token.nextToken();
206                        readOnlyFieldsList.add(element);
207                    }
208                }
209                else {
210                    readOnlyFieldsList.add(readOnlyFieldsString);
211                }
212            }
213          return readOnlyFieldsList;
214        }
215    
216        /**
217         * This translates a , delimited list of pairs separated by a : into a Map of target --> lookup field conversions.
218         *
219         * @param conversionFields
220         * @return the Map representation of the fieldConversions String provided.
221         */
222        public static Map<String, String> translateFieldConversions(String fieldConversionsString) {
223            Map<String, String> fieldConversionsMap = new HashMap();
224            if (StringUtils.isNotEmpty(fieldConversionsString)) {
225                if (fieldConversionsString.indexOf(",") > 0) {
226                    StringTokenizer token = new StringTokenizer(fieldConversionsString, ",");
227                    while (token.hasMoreTokens()) {
228                        String element = token.nextToken();
229                        fieldConversionsMap.put(element.substring(0, element.indexOf(":")), element.substring(element.indexOf(":") + 1));
230                    }
231                }
232                else {
233                    fieldConversionsMap.put(fieldConversionsString.substring(0, fieldConversionsString.indexOf(":")), fieldConversionsString.substring(fieldConversionsString.indexOf(":") + 1));
234                }
235            }
236            return fieldConversionsMap;
237        }
238    
239        @Deprecated
240        public static Field setFieldQuickfinder(BusinessObject businessObject,
241                String attributeName, Field field, List displayedFieldNames) {
242            return setFieldQuickfinder( businessObject, (String)null, false, 0, attributeName, field, displayedFieldNames );
243        }
244    
245        @Deprecated
246        public static Field setFieldQuickfinder(BusinessObject businessObject,
247                String attributeName, Field field, List displayedFieldNames, SelectiveReferenceRefresher srr) {
248            return setFieldQuickfinder( businessObject, (String)null, false, 0, attributeName, field, displayedFieldNames, srr );
249        }
250    
251        /**
252         * Sets a fields quickfinder class and field conversions for an attribute.
253         */
254        @Deprecated
255        public static Field setFieldQuickfinder(BusinessObject businessObject, String collectionName, boolean addLine, int index,
256                String attributeName, Field field, List displayedFieldNames, SelectiveReferenceRefresher srr) {
257            field = setFieldQuickfinder(businessObject, collectionName, addLine, index, attributeName, field, displayedFieldNames);
258            if (srr != null) {
259                String collectionPrefix = "";
260                if ( collectionName != null ) {
261                    if (addLine) {
262                        collectionPrefix = KRADConstants.MAINTENANCE_ADD_PREFIX + collectionName + ".";
263                    }
264                    else {
265                        collectionPrefix = collectionName + "[" + index + "].";
266                    }
267                }
268                field.setReferencesToRefresh(convertReferencesToSelectCollectionToString(
269                        srr.getAffectedReferencesFromLookup(businessObject, attributeName, collectionPrefix)));
270            }
271            return field;
272        }
273    
274        /**
275         * Sets a fields quickfinder class and field conversions for an attribute.
276         */
277        @Deprecated
278        public static Field setFieldQuickfinder(BusinessObject businessObject, String collectionName, boolean addLine, int index,
279                                                String attributeName, Field field, List displayedFieldNames) {
280            boolean noLookup = false;
281            if (businessObject == null) {
282                return field;
283            }
284    
285            Boolean noLookupField = getBusinessObjectDictionaryService().noLookupFieldLookup(businessObject.getClass(), attributeName);
286            if (noLookupField != null && noLookupField) {
287                noLookup = true;
288            }
289    
290             return setFieldQuickfinder(businessObject, collectionName, addLine, index, attributeName, field, displayedFieldNames, noLookup);
291    
292        }
293    
294        @Deprecated
295        public static Field setFieldQuickfinder(BusinessObject businessObject, String collectionName, boolean addLine, int index, String attributeName, Field field, List displayedFieldNames, boolean noLookupField)
296        {
297             if (businessObject == null) {
298                return field;
299            }
300    
301            if (noLookupField) {
302                return field;
303            }
304            DataObjectRelationship relationship = null;
305            if ( LOG.isDebugEnabled() ) {
306                LOG.debug( "setFieldQuickfinder("+businessObject.getClass().getName()+","+attributeName+","+field+","+displayedFieldNames+")" );
307            }
308    
309            relationship = getBusinessObjectMetaDataService().getBusinessObjectRelationship(businessObject, businessObject.getClass(), attributeName, "", false);
310    
311            String collectionPrefix = "";
312            if ( collectionName != null ) {
313                if (addLine) {
314                    collectionPrefix = KRADConstants.MAINTENANCE_ADD_PREFIX + collectionName + ".";
315                }
316                else {
317                    collectionPrefix = collectionName + "[" + index + "].";
318                }
319            }
320    
321            if (relationship == null) {
322                Class c = ObjectUtils.getPropertyType(businessObject, attributeName, getPersistenceStructureService());
323    
324                if(c!=null) {
325                    if (attributeName.contains(".")) {
326                        attributeName = StringUtils.substringBeforeLast( attributeName, "." );
327                    }
328    
329                    RelationshipDefinition ddReference = getBusinessObjectMetaDataService().getBusinessObjectRelationshipDefinition(businessObject, attributeName);
330                    relationship = getBusinessObjectMetaDataService().getBusinessObjectRelationship(ddReference, businessObject, businessObject.getClass(), attributeName, "", false);
331                    if(relationship!=null) {
332                        field.setQuickFinderClassNameImpl(relationship.getRelatedClass().getName());
333                        field.setFieldConversions(generateFieldConversions( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, null));
334                        field.setLookupParameters(generateLookupParameters( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, null));
335                        field.setBaseLookupUrl(LookupUtils.getBaseLookupUrl(false));
336                        field.setImageSrc(getBusinessObjectDictionaryService().getSearchIconOverride(businessObject.getClass()));
337                    }
338                }
339    
340                return field;
341            }
342            if (ObjectUtils.isNestedAttribute(attributeName)) {
343                //first determine the prefix and the attribute we are referring to
344                String nestedAttributePrefix = StringUtils.substringBeforeLast(attributeName, ".");
345    
346                field.setQuickFinderClassNameImpl(relationship.getRelatedClass().getName());
347                field.setFieldConversions( generateFieldConversions( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, nestedAttributePrefix ) );
348                field.setLookupParameters( generateLookupParameters( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, nestedAttributePrefix ) );
349                field.setBaseLookupUrl(LookupUtils.getBaseLookupUrl(false));
350            } else {
351                field.setQuickFinderClassNameImpl(relationship.getRelatedClass().getName());
352                field.setFieldConversions( generateFieldConversions( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, null ) );
353                field.setLookupParameters( generateLookupParameters( businessObject, collectionPrefix, relationship, field.getPropertyPrefix(), displayedFieldNames, null ) );
354                field.setBaseLookupUrl(LookupUtils.getBaseLookupUrl(false));
355            }
356            field.setImageSrc(getBusinessObjectDictionaryService().getSearchIconOverride(businessObject.getClass()));
357    
358            return field;
359        }
360    
361        private static String BASE_LOOKUP_ACTION_URL = null;
362        private static String BASE_MULTIPLE_VALUE_LOOKUP_ACTION_URL = null;
363        private static String BASE_INQUIRY_ACTION_URL = null;
364    
365        /**
366         * @see org.kuali.rice.krad.uif.util.LookupInquiryUtils#getBaseLookupUrl()
367         */
368        @Deprecated
369        public static String getBaseLookupUrl(boolean isMultipleValue) {
370            ConfigurationService kualiConfigurationService = KRADServiceLocator.getKualiConfigurationService();
371            if ( isMultipleValue ) {
372                    if ( BASE_MULTIPLE_VALUE_LOOKUP_ACTION_URL == null ) {
373                            String lookupUrl = kualiConfigurationService.getPropertyValueAsString(KRADConstants.APPLICATION_URL_KEY);
374                            if (!lookupUrl.endsWith("/")) {
375                                    lookupUrl = lookupUrl + "/";
376                            }
377                                    lookupUrl += "kr/" + KRADConstants.MULTIPLE_VALUE_LOOKUP_ACTION;
378                                    BASE_MULTIPLE_VALUE_LOOKUP_ACTION_URL = lookupUrl;
379                    }
380                    return BASE_MULTIPLE_VALUE_LOOKUP_ACTION_URL;
381            } else {
382                    if ( BASE_LOOKUP_ACTION_URL == null ) {
383                            String lookupUrl = kualiConfigurationService.getPropertyValueAsString(KRADConstants.APPLICATION_URL_KEY);
384                            if (!lookupUrl.endsWith("/")) {
385                                    lookupUrl = lookupUrl + "/";
386                            }
387                                    lookupUrl += "kr/" + KRADConstants.LOOKUP_ACTION;
388                                    BASE_LOOKUP_ACTION_URL = lookupUrl;
389                    }
390                    return BASE_LOOKUP_ACTION_URL;
391            }
392        }
393    
394        @Deprecated
395        public static String getBaseInquiryUrl() {
396            if ( BASE_INQUIRY_ACTION_URL == null ) {
397                    StringBuffer inquiryUrl = new StringBuffer( 
398                                    KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
399                                KRADConstants.APPLICATION_URL_KEY) );
400                            if (inquiryUrl.charAt(inquiryUrl.length()-1) != '/' ) {
401                                    inquiryUrl.append( '/' );
402                            }
403                            inquiryUrl.append("kr/");
404                            inquiryUrl.append( KRADConstants.INQUIRY_ACTION );
405                            BASE_INQUIRY_ACTION_URL = inquiryUrl.toString();
406            }
407            return BASE_INQUIRY_ACTION_URL;
408        }
409    
410        public static String transformLookupUrlToMultiple(String lookupUrl) {
411            return lookupUrl.replace("kr/" + KRADConstants.LOOKUP_ACTION, "kr/" + KRADConstants.MULTIPLE_VALUE_LOOKUP_ACTION);
412        }
413    
414        /**
415         * Sets whether a field should have direct inquiries enabled.  The direct inquiry is the functionality on a page such that if the primary key for
416         * a quickfinder is filled in and the direct inquiry button is pressed, then a new window will popup showing an inquiry page without going through
417         * the lookup first.
418         *
419         * For this method to work properly, it must be called after setFieldQuickfinder
420         * //TODO: chb: that should not be the case -- the relationship object the two rely upon should be established outside of the lookup/quickfinder code
421         *
422         *
423         * @param field
424         */
425        private static void setFieldDirectInquiry(Field field) {
426            if (StringUtils.isNotBlank(field.getFieldConversions())) {
427                boolean directInquiriesEnabled = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
428                        KRADConstants.KNS_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KRADConstants.SystemGroupParameterNames.ENABLE_DIRECT_INQUIRIES_IND);
429                if (directInquiriesEnabled) {
430                    if (StringUtils.isNotBlank(field.getFieldConversions())) {
431                        String fieldConversions = field.getFieldConversions();
432                        String newInquiryParameters = KRADConstants.EMPTY_STRING;
433                        String[] conversions = StringUtils.split(fieldConversions, KRADConstants.FIELD_CONVERSIONS_SEPARATOR);
434    
435                        for (int l = 0; l < conversions.length; l++) {
436                            String conversion = conversions[l];
437                            //String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR);
438                            String[] conversionPair = StringUtils.split(conversion, KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR, 2);
439                            String conversionFrom = conversionPair[0];
440                            String conversionTo = conversionPair[1];
441                            newInquiryParameters += (conversionTo + KRADConstants.FIELD_CONVERSION_PAIR_SEPARATOR + conversionFrom);
442    
443                            if (l < conversions.length - 1) {
444                                newInquiryParameters += KRADConstants.FIELD_CONVERSIONS_SEPARATOR;
445                            }
446                        }
447    
448                        field.setInquiryParameters(newInquiryParameters);
449                    }
450                }
451                field.setFieldDirectInquiryEnabled(directInquiriesEnabled);
452            }
453            else {
454                field.setFieldDirectInquiryEnabled(false);
455            }
456        }
457    
458        /**
459         *
460         * @param field
461         * @return the altered Field object
462         */
463        public static Field setFieldDirectInquiry(BusinessObject businessObject, String attributeName, Field field)
464        {
465                    if (businessObject == null)
466                    {
467                return field;
468            }
469    
470            Boolean noDirectInquiry = getBusinessObjectDictionaryService().noDirectInquiryFieldLookup(businessObject.getClass(), attributeName);
471            //check if noDirectInquiry is present and true, but if it's not set in existing data dictionary definitions, don't create a direct inquiry
472            if (noDirectInquiry != null && noDirectInquiry.booleanValue() || noDirectInquiry == null) {
473                return field;
474            }
475    
476            setFieldDirectInquiry(field);
477    
478            return field;
479        }
480    
481        private static Map<Class,Map<String,Map>> referencesForForeignKey = new HashMap<Class, Map<String,Map>>();
482    
483        @Deprecated
484        public static Map getPrimitiveReference(BusinessObject businessObject, String attributeName) {
485            Map chosenReferenceByKeySize = new HashMap();
486            Map chosenReferenceByFieldName = new HashMap();
487    
488            Map referenceClasses = null;
489    
490            try {
491                // add special caching of these relationships since the Spring caching is so expensive
492                Map<String,Map> propMap = referencesForForeignKey.get(businessObject.getClass());
493                if ( propMap == null ) {
494                    propMap = new HashMap<String, Map>();
495                    referencesForForeignKey.put(businessObject.getClass(), propMap);
496                }
497                if ( propMap.containsKey(attributeName) ) {
498                    referenceClasses = propMap.get( attributeName );
499                } else {
500                    //KFSMI-709: Make Inquiry Framework use BusinessObjectMetadataService instead of just PersistenceStructureService
501                    referenceClasses = getBusinessObjectMetaDataService().getReferencesForForeignKey(businessObject, attributeName);
502                    if(referenceClasses==null || referenceClasses.isEmpty()) {
503                        if ( getPersistenceStructureService().isPersistable(businessObject.getClass()) ) {
504                            referenceClasses = getPersistenceStructureService().getReferencesForForeignKey(businessObject.getClass(), attributeName);
505                        }
506                    }
507                    propMap.put(attributeName, referenceClasses);
508                }
509            } catch ( ClassNotPersistableException ex ) {
510                // do nothing, there is no quickfinder
511                Map<String,Map> propMap = referencesForForeignKey.get(businessObject.getClass());
512                propMap.put(attributeName, null);
513            }
514    
515            // if field is not fk to any reference class, return field object w no quickfinder
516            if (referenceClasses == null || referenceClasses.isEmpty()) {
517                return chosenReferenceByKeySize;
518            }
519    
520            /*
521             * if field is fk to more than one reference, take the class with the least # of pk fields, this should give the correct
522             * grain for the attribute
523             */
524            int minKeys = Integer.MAX_VALUE;
525            for (Iterator iter = referenceClasses.keySet().iterator(); iter.hasNext();) {
526                String attr = (String) iter.next();
527                Class clazz = (Class) referenceClasses.get(attr);
528                List pkNames = getBusinessObjectMetaDataService().listPrimaryKeyFieldNames(clazz);
529    
530                // Compare based on key size.
531                if (pkNames.size() < minKeys) {
532                    minKeys = pkNames.size();
533                    chosenReferenceByKeySize.clear();
534                    chosenReferenceByKeySize.put(attr, clazz);
535                }
536    
537                // Compare based on field name.
538                if (attributeName.startsWith(attr)) {
539                    chosenReferenceByFieldName.clear();
540                    chosenReferenceByFieldName.put(attr, clazz);
541                }
542            }
543    
544            // If a compatible key was found based on field names, prefer it, otherwise use choice by key size.
545            return chosenReferenceByFieldName.isEmpty() ? chosenReferenceByKeySize : chosenReferenceByFieldName;
546        }
547    
548        /**
549         *
550         * This method walks through the nested attribute and finds the last business object in the chain and returns it (excluding the
551         * last parameter which is the actual attribute)
552         *
553         * @param attributeName
554         * @return
555         */
556        public static BusinessObject getNestedBusinessObject(BusinessObject bo, String attributeName) {
557            String[] nestedAttributes = StringUtils.split(attributeName, ".");
558    
559            BusinessObject childBO = null;
560            String attributeRefName = "";
561            Class clazz = null;
562            if (nestedAttributes.length > 1) {
563                String attributeStringSoFar = "";
564                for (int i = 0; i < nestedAttributes.length - 1; i++) {
565                    // we need to build a string of the attribute names depending on which iteration we're in.
566                    // so if the original attributeName string we're using is "a.b.c.d.e", then first iteration would use
567                    // "a", 2nd "a.b", 3rd "a.b.c", etc.
568                    if (i != 0) {
569                        attributeStringSoFar = attributeStringSoFar + ".";
570                    }
571                    attributeStringSoFar = attributeStringSoFar + nestedAttributes[i];
572    
573                    clazz = ObjectUtils.getPropertyType( bo, attributeStringSoFar, getPersistenceStructureService() );
574    
575                    if (clazz != null && BusinessObject.class.isAssignableFrom(clazz)) {
576                        try {
577                            childBO = (BusinessObject) ObjectUtils.createNewObjectFromClass(clazz);
578                        }
579                        catch (Exception e) {
580                            return null;
581                        }
582                    }
583                }
584            }
585            return childBO;
586        }
587    
588        public static Class getNestedReferenceClass(BusinessObject businessObject, String attributeName) {
589            BusinessObject bo = getNestedBusinessObject(businessObject, attributeName);
590            return null == bo ? null : bo.getClass();
591        }
592    
593        @Deprecated
594        private static String generateFieldConversions(BusinessObject businessObject, String collectionName, DataObjectRelationship relationship, String propertyPrefix, List displayedFieldNames, String nestedObjectPrefix) {
595            String fieldConversions = "";
596    
597            if ( LOG.isDebugEnabled() ) {
598                LOG.debug( "generateFieldConversions(" + businessObject.getClass().getName() + "," + collectionName + ",\n" + relationship + "\n," + propertyPrefix + "," + displayedFieldNames + "," + nestedObjectPrefix + ")" );
599            }
600    
601            // get the references for the given property
602            for ( Map.Entry<String,String> entry : relationship.getParentToChildReferences().entrySet() ) {
603                String fromField = entry.getValue();
604                String toField = entry.getKey();
605    
606                // find the displayed to field mapping
607                if (!displayedFieldNames.contains(toField)) {
608                    toField = translateToDisplayedField(businessObject.getClass(), toField, displayedFieldNames);
609                }
610    
611                if (StringUtils.isNotBlank(fieldConversions)) {
612                    fieldConversions += ",";
613                }
614    
615                if ( StringUtils.isNotEmpty( propertyPrefix ) ) {
616                    toField = propertyPrefix + "." + toField;
617                }
618    
619                if ( StringUtils.isNotEmpty( collectionName ) ) {
620                    toField = collectionName + toField;
621                }
622    
623                fieldConversions += fromField + ":" + toField;
624            }
625    
626            return fieldConversions;
627        }
628    
629        @Deprecated
630        private static String generateLookupParameters(BusinessObject businessObject, String collectionName, DataObjectRelationship relationship, String propertyPrefix, List displayedFieldNames, String nestedObjectPrefix) {
631    
632            String lookupParameters = "";
633    
634            List displayedQFFieldNames = getBusinessObjectDictionaryService().getLookupFieldNames(relationship.getRelatedClass());
635            for ( Map.Entry<String,String> entry : relationship.getParentToChildReferences().entrySet() ) {
636                String fromField = entry.getKey();
637                String toField = entry.getValue();
638    
639                if ( relationship.getUserVisibleIdentifierKey() == null || relationship.getUserVisibleIdentifierKey().equals( fromField ) ) {
640                    // find the displayed from field mapping
641                    if (!displayedFieldNames.contains(fromField)) {
642                        fromField = translateToDisplayedField(businessObject.getClass(), fromField, displayedFieldNames);
643                    }
644    
645                    // translate to field
646                    if (displayedQFFieldNames != null && !displayedQFFieldNames.contains(toField)) {
647                        toField = translateToDisplayedField(relationship.getRelatedClass(), toField, displayedQFFieldNames);
648                    }
649    
650                    if (StringUtils.isNotBlank(lookupParameters)) {
651                        lookupParameters += ",";
652                    }
653    
654                    if (propertyPrefix != null && !propertyPrefix.equals("")) {
655                        fromField = propertyPrefix + "." + fromField;
656                    }
657    
658                    if ( StringUtils.isNotEmpty( collectionName ) ) {
659                        fromField = collectionName + fromField;
660                    }
661    
662                    lookupParameters += fromField + ":" + toField;
663                }
664            }
665    
666            return lookupParameters;
667        }
668    
669        @Deprecated
670        private static String translateToDisplayedField(Class businessObjectClass, String fieldName, List displayedFieldNames) {        
671            if ( getPersistenceStructureService().isPersistable(businessObjectClass) ) {
672                Map nestedFkMap = getPersistenceStructureService().getNestedForeignKeyMap(businessObjectClass);
673    
674                // translate to primitive fk if nested
675                /*
676                 * if (ObjectUtils.isNestedAttribute(fieldName) && nestedFkMap.containsKey(fieldName)) { fieldName = (String)
677                 * nestedFkMap.get(fieldName); }
678                 */
679    
680                if (!displayedFieldNames.contains(fieldName)) {
681                    for (Iterator iterator = displayedFieldNames.iterator(); iterator.hasNext();) {
682                        String dispField = (String) iterator.next();
683    
684                        if (nestedFkMap.containsKey(dispField) && nestedFkMap.get(dispField).equals(fieldName)) {
685                            fieldName = dispField;
686                        }
687                    }
688                }
689            }
690    
691            return fieldName;
692        }
693    
694        public static String convertReferencesToSelectCollectionToString(Collection<String> referencesToRefresh) {
695            StringBuilder buf = new StringBuilder();
696            for (String reference : referencesToRefresh) {
697                buf.append(reference).append(KRADConstants.REFERENCES_TO_REFRESH_SEPARATOR);
698            }
699            if (!referencesToRefresh.isEmpty()) {
700                // we appended one too many separators, remove it
701                buf.delete(buf.length() - KRADConstants.REFERENCES_TO_REFRESH_SEPARATOR.length(), buf.length());
702            }
703            return buf.toString();
704        }
705    
706        public static String convertSetOfObjectIdsToString(Set<String> objectIds) {
707            if (objectIds.isEmpty()) {
708                return "";
709            }
710            StringBuilder buf = new StringBuilder();
711            for (String objectId : objectIds) {
712                if (objectId.contains(KRADConstants.MULTIPLE_VALUE_LOOKUP_OBJ_IDS_SEPARATOR)) {
713                    throw new RuntimeException("object ID " + objectId + " contains the selected obj ID separator");
714                }
715                buf.append(objectId).append(KRADConstants.MULTIPLE_VALUE_LOOKUP_OBJ_IDS_SEPARATOR);
716            }
717            // added one extra separator, remove it
718            buf.delete(buf.length() - KRADConstants.MULTIPLE_VALUE_LOOKUP_OBJ_IDS_SEPARATOR.length(), buf.length());
719    
720            return buf.toString();
721        }
722    
723        public static Set<String> convertStringOfObjectIdsToSet(String objectIdsString) {
724            Set<String> set = new HashSet<String>();
725    
726            if (StringUtils.isNotBlank(objectIdsString)) {
727                String[] objectIds = StringUtils.splitByWholeSeparator(objectIdsString, KRADConstants.MULTIPLE_VALUE_LOOKUP_OBJ_IDS_SEPARATOR);
728                for (String objectId : objectIds) {
729                    set.add(objectId);
730                }
731            }
732            return set;
733        }
734    
735        /**
736         * Given a list of results from a lookup, determines the best comparator to use on the String values of each of these columns
737         *
738         * This method exists because each cell (represented by the Column object) lists the comparator that should be used within it based on the property value class,
739         * so we gotta go thru the whole list and determine the best comparator to use
740         *
741         * @param resultsTable
742         * @param column
743         * @return
744         */
745        public static Comparator findBestValueComparatorForColumn(List<ResultRow> resultTable, int column) {
746            // BIG HACK
747            Comparator comp = NullValueComparator.getInstance();
748            for (ResultRow row : resultTable) {
749                Comparator tempComp = row.getColumns().get(column).getValueComparator();
750                if (tempComp != null && !NullValueComparator.class.equals(tempComp.getClass())) {
751                    return tempComp;
752                }
753            }
754            return comp;
755        }
756        /**
757         * Changes ranged search fields like from/to dates into the range operators the lookupable dao expects
758         * ("..",">" etc) this method modifies the passed in map and returns a list containing only the modified fields
759         *
760         * This method does not handle document searchable attributes.  This is handled in a second pass by the docsearch-specific
761         * DocumentSearchCriteriaTranslator
762         */
763        public static Map<String, String> preProcessRangeFields(Map<String, String> lookupFormFields) {
764            Map<String, String> fieldsToUpdate = new HashMap<String, String>();
765            Set<String> fieldsForLookup = lookupFormFields.keySet();
766            for (String propName : fieldsForLookup) {
767                if (propName.startsWith(KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
768                    String rangedLowerBoundValue = lookupFormFields.get(propName);
769                    String rangedFieldName = StringUtils.remove(propName, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX);
770                    String rangedValue = lookupFormFields.get(rangedFieldName);
771    
772                    Range range = new Range();
773                    // defaults for general lookup/search
774                    range.setLowerBoundInclusive(true);
775                    range.setUpperBoundInclusive(true);
776                    range.setLowerBoundValue(rangedLowerBoundValue);
777                    range.setUpperBoundValue(rangedValue);
778    
779                     String expr = range.toString();
780                    if (StringUtils.isEmpty(expr)) {
781                        expr = rangedValue;
782                    }
783    
784                    fieldsToUpdate.put(rangedFieldName, expr);
785                }
786            }
787            //update lookup values from found ranged values to update
788            Set<String> keysToUpdate = fieldsToUpdate.keySet();
789            for (String updateKey : keysToUpdate) {
790                lookupFormFields.put(updateKey, fieldsToUpdate.get(updateKey));
791            }
792            return fieldsToUpdate;
793        }
794    
795        /**
796         * Given 3 sets of object IDs: the set of selected object IDs before rendering the current page,
797         * the set of object IDs rendered on the page, and the set of object IDs selected on the page, computes
798         * the total set of selected object IDs.
799         *
800         * Instead of storing it in a set, returns it in a map with the selected object ID as both the key and value
801         * @param previouslySelectedObjectIds
802         * @param displayedObjectIds
803         * @param selectedObjectIds
804         * @return
805         */
806        public static Map<String, String> generateCompositeSelectedObjectIds(Set<String> previouslySelectedObjectIds, Set<String> displayedObjectIds, Set<String> selectedObjectIds) {
807            Map<String, String> tempMap = new HashMap<String, String>();
808            // Equivalent to the set operation:
809            // (P - D) union C, where - is the set difference operator
810            // P is the list of object IDs previously passed in, D is the set of displayed object IDs, and C is the set of checked obj IDs
811            // since HTML does not pass a value for non-selected dcheckboxes
812    
813            // first build a map w/ all the previouslySelectedObjectIds as keys
814            for (String previouslySelectedObjectId : previouslySelectedObjectIds) {
815                tempMap.put(previouslySelectedObjectId, previouslySelectedObjectId);
816            }
817            // then remove all the displayed elements (any selected displayed elements will be added back in the next loop)
818            for (String displayedObjectId : displayedObjectIds) {
819                tempMap.remove(displayedObjectId);
820            }
821            // put back the selected IDs
822            for (String selectedObjectId : selectedObjectIds) {
823                tempMap.put(selectedObjectId, selectedObjectId);
824            }
825            return tempMap;
826        }
827    
828        public static DataDictionaryService getDataDictionaryService() {
829            return KRADServiceLocatorWeb.getDataDictionaryService();
830        }
831    
832        public static PersistenceStructureService getPersistenceStructureService() {
833            return KRADServiceLocator.getPersistenceStructureService();
834        }
835    
836        public static BusinessObjectDictionaryService getBusinessObjectDictionaryService() {
837            return KNSServiceLocator.getBusinessObjectDictionaryService();
838        }
839    
840        public static BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
841            return KNSServiceLocator.getBusinessObjectMetaDataService();
842        }
843    
844        public static DateTimeService getDateTimeService() {
845            return CoreApiServiceLocator.getDateTimeService();
846        }
847    }