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