Coverage Report - org.kuali.rice.kns.workflow.service.impl.WorkflowAttributePropertyResolutionServiceImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
WorkflowAttributePropertyResolutionServiceImpl
0%
0/189
0%
0/156
3.816
WorkflowAttributePropertyResolutionServiceImpl$RoutingAttributeTracker
0%
0/14
N/A
3.816
 
 1  
 /*
 2  
  * Copyright 2008-2009 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.kns.workflow.service.impl;
 17  
 
 18  
 import java.beans.BeanInfo;
 19  
 import java.beans.IntrospectionException;
 20  
 import java.beans.Introspector;
 21  
 import java.beans.PropertyDescriptor;
 22  
 import java.math.BigDecimal;
 23  
 import java.sql.Timestamp;
 24  
 import java.util.ArrayList;
 25  
 import java.util.Collection;
 26  
 import java.util.HashSet;
 27  
 import java.util.List;
 28  
 import java.util.Set;
 29  
 import java.util.Stack;
 30  
 
 31  
 import org.kuali.rice.kew.docsearch.SearchableAttribute;
 32  
 import org.kuali.rice.kew.docsearch.SearchableAttributeDateTimeValue;
 33  
 import org.kuali.rice.kew.docsearch.SearchableAttributeFloatValue;
 34  
 import org.kuali.rice.kew.docsearch.SearchableAttributeLongValue;
 35  
 import org.kuali.rice.kew.docsearch.SearchableAttributeStringValue;
 36  
 import org.kuali.rice.kew.docsearch.SearchableAttributeValue;
 37  
 import org.kuali.rice.kim.bo.types.dto.AttributeSet;
 38  
 import org.kuali.rice.kns.bo.BusinessObject;
 39  
 import org.kuali.rice.kns.bo.PersistableBusinessObject;
 40  
 import org.kuali.rice.kns.datadictionary.DocumentCollectionPath;
 41  
 import org.kuali.rice.kns.datadictionary.DocumentValuePathGroup;
 42  
 import org.kuali.rice.kns.datadictionary.RoutingAttribute;
 43  
 import org.kuali.rice.kns.datadictionary.RoutingTypeDefinition;
 44  
 import org.kuali.rice.kns.datadictionary.SearchingTypeDefinition;
 45  
 import org.kuali.rice.kns.datadictionary.WorkflowAttributes;
 46  
 import org.kuali.rice.kns.document.Document;
 47  
 import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
 48  
 import org.kuali.rice.kns.service.KNSServiceLocator;
 49  
 import org.kuali.rice.kns.service.PersistenceStructureService;
 50  
 import org.kuali.rice.kns.util.KualiDecimal;
 51  
 import org.kuali.rice.kns.util.ObjectUtils;
 52  
 import org.kuali.rice.kns.workflow.attribute.DataDictionarySearchableAttribute;
 53  
 import org.kuali.rice.kns.workflow.service.WorkflowAttributePropertyResolutionService;
 54  
 
 55  
 /**
 56  
  * The default implementation of the WorkflowAttributePropertyResolutionServiceImpl
 57  
  */
 58  0
 public class WorkflowAttributePropertyResolutionServiceImpl implements WorkflowAttributePropertyResolutionService {
 59  
     private PersistenceStructureService persistenceStructureService;
 60  
     private BusinessObjectMetaDataService businessObjectMetaDataService;
 61  
 
 62  
     /**
 63  
      * Using the proper RoutingTypeDefinition for the current routing node of the document, aardvarks out the proper routing type qualifiers
 64  
      * @see org.kuali.kfs.sys.document.service.WorkflowAttributePropertyResolutionService#resolveRoutingTypeQualifiers(Document, RoutingTypeDefinition)
 65  
      */
 66  
     public List<AttributeSet> resolveRoutingTypeQualifiers(Document document, RoutingTypeDefinition routingTypeDefinition) {
 67  0
         List<AttributeSet> qualifiers = new ArrayList<AttributeSet>();
 68  
         
 69  0
         if (routingTypeDefinition != null) {
 70  0
             document.populateDocumentForRouting();
 71  0
             RoutingAttributeTracker routingAttributeTracker = new RoutingAttributeTracker(routingTypeDefinition.getRoutingAttributes());
 72  0
             for (DocumentValuePathGroup documentValuePathGroup : routingTypeDefinition.getDocumentValuePathGroups()) {
 73  0
                 qualifiers.addAll(resolveDocumentValuePath(document, documentValuePathGroup, routingAttributeTracker));
 74  0
                 routingAttributeTracker.reset();
 75  
             }
 76  
         }
 77  0
         return qualifiers;
 78  
     }
 79  
     
 80  
     /**
 81  
      * Resolves all of the values in the given DocumentValuePathGroup from the given BusinessObject
 82  
      * @param businessObject the business object which is the source of values
 83  
      * @param group the DocumentValuePathGroup which tells us which values we want
 84  
      * @return a List of AttributeSets
 85  
      */
 86  
     protected List<AttributeSet> resolveDocumentValuePath(BusinessObject businessObject, DocumentValuePathGroup group, RoutingAttributeTracker routingAttributeTracker) {
 87  
         List<AttributeSet> qualifiers;
 88  0
         AttributeSet qualifier = new AttributeSet();
 89  0
         if (group.getDocumentValues() == null && group.getDocumentCollectionPath() == null) {
 90  0
             throw new IllegalStateException("A document value path group must have the documentValues property set, the documentCollectionPath property set, or both.");
 91  
         }
 92  0
         if (group.getDocumentValues() != null) {
 93  0
             addPathValuesToQualifier(businessObject, group.getDocumentValues(), routingAttributeTracker, qualifier);
 94  
         }
 95  0
         if (group.getDocumentCollectionPath() != null) {
 96  0
             qualifiers = resolveDocumentCollectionPath(businessObject, group.getDocumentCollectionPath(), routingAttributeTracker);
 97  0
             qualifiers = cleanCollectionQualifiers(qualifiers);
 98  0
             for (AttributeSet collectionElementQualifier : qualifiers) {
 99  0
                 copyQualifications(qualifier, collectionElementQualifier);
 100  
             }
 101  
         } else {
 102  0
             qualifiers = new ArrayList<AttributeSet>();
 103  0
             qualifiers.add(qualifier);
 104  
         }
 105  0
         return qualifiers;
 106  
     }
 107  
     
 108  
     /**
 109  
      * Resolves document values from a collection path on a given business object
 110  
      * @param businessObject the business object which has a collection, each element of which is a source of values
 111  
      * @param collectionPath the information about what values to pull from each element of the collection
 112  
      * @return a List of AttributeSets
 113  
      */
 114  
     protected List<AttributeSet> resolveDocumentCollectionPath(BusinessObject businessObject, DocumentCollectionPath collectionPath, RoutingAttributeTracker routingAttributeTracker) {
 115  0
         List<AttributeSet> qualifiers = new ArrayList<AttributeSet>();
 116  0
         final Collection collectionByPath = getCollectionByPath(businessObject, collectionPath.getCollectionPath());
 117  0
         if (!ObjectUtils.isNull(collectionByPath)) {
 118  0
             if (collectionPath.getNestedCollection() != null) {
 119  
                 // we need to go through the collection...
 120  0
                 for (Object collectionElement : collectionByPath) {
 121  
                     // for each element, we need to get the child qualifiers
 122  0
                     if (collectionElement instanceof BusinessObject) {
 123  0
                         List<AttributeSet> childQualifiers = resolveDocumentCollectionPath((BusinessObject)collectionElement, collectionPath.getNestedCollection(), routingAttributeTracker);
 124  0
                         for (AttributeSet childQualifier : childQualifiers) {
 125  0
                             AttributeSet qualifier = new AttributeSet();
 126  0
                             routingAttributeTracker.checkPoint();
 127  
                             // now we need to get the values for the current element of the collection
 128  0
                             addPathValuesToQualifier(collectionElement, collectionPath.getDocumentValues(), routingAttributeTracker, qualifier);
 129  
                             // and move all the child keys to the qualifier
 130  0
                             copyQualifications(childQualifier, qualifier);
 131  0
                             qualifiers.add(qualifier);
 132  0
                             routingAttributeTracker.backUpToCheckPoint();
 133  0
                         }
 134  0
                     }
 135  
                 }
 136  
             } else {
 137  
                 // go through each element in the collection
 138  0
                 for (Object collectionElement : collectionByPath) {
 139  0
                     AttributeSet qualifier = new AttributeSet();
 140  0
                     routingAttributeTracker.checkPoint();
 141  0
                     addPathValuesToQualifier(collectionElement, collectionPath.getDocumentValues(), routingAttributeTracker, qualifier);
 142  0
                     qualifiers.add(qualifier);
 143  0
                     routingAttributeTracker.backUpToCheckPoint();
 144  0
                 }
 145  
             }
 146  
         }
 147  0
         return qualifiers;
 148  
     }
 149  
     
 150  
     /**
 151  
      * Returns a collection from a path on a business object
 152  
      * @param businessObject the business object to get values from
 153  
      * @param collectionPath the path to that collection
 154  
      * @return hopefully, a collection of objects
 155  
      */
 156  
     protected Collection getCollectionByPath(BusinessObject businessObject, String collectionPath) {
 157  0
         return (Collection)getPropertyByPath(businessObject, collectionPath.trim());
 158  
     }
 159  
     
 160  
     /**
 161  
      * Aardvarks values out of a business object and puts them into an AttributeSet, based on a List of paths
 162  
      * @param businessObject the business object to get values from
 163  
      * @param paths the paths of values to get from the qualifier
 164  
      * @param routingAttribute the RoutingAttribute associated with this qualifier's document value
 165  
      * @param currentRoutingAttributeIndex - the current index of the routing attribute
 166  
      * @param qualifier the qualifier to put values into
 167  
      */
 168  
     protected void addPathValuesToQualifier(Object businessObject, List<String> paths, RoutingAttributeTracker routingAttributes, AttributeSet qualifier) {
 169  0
         if (ObjectUtils.isNotNull(paths)) {
 170  0
             for (String path : paths) {
 171  
                 // get the values for the paths of each element of the collection
 172  0
                 final Object value = getPropertyByPath(businessObject, path.trim());
 173  0
                 if (value != null) {
 174  0
                     qualifier.put(routingAttributes.getCurrentRoutingAttribute().getQualificationAttributeName(), value.toString());
 175  
                 }
 176  0
                 routingAttributes.moveToNext();
 177  0
             }
 178  
         }
 179  0
     }
 180  
     
 181  
     /**
 182  
      * Copies all the values from one qualifier to another
 183  
      * @param source the source of values
 184  
      * @param target the place to write all the values to
 185  
      */
 186  
     protected void copyQualifications(AttributeSet source, AttributeSet target) {
 187  0
         for (String key : source.keySet()) {
 188  0
             target.put(key, source.get(key));
 189  
         }
 190  0
     }
 191  
 
 192  
     /**
 193  
      * Resolves all of the searching values to index for the given document, returning a list of SearchableAttributeValue implementations
 194  
      * @see org.kuali.kfs.sys.document.service.WorkflowAttributePropertyResolutionService#resolveSearchableAttributeValues(org.kuali.rice.kns.document.Document, org.kuali.rice.kns.datadictionary.WorkflowAttributes)
 195  
      */
 196  
     public List<SearchableAttributeValue> resolveSearchableAttributeValues(Document document, WorkflowAttributes workflowAttributes) {
 197  0
         List<SearchableAttributeValue> valuesToIndex = new ArrayList<SearchableAttributeValue>();
 198  0
         if (workflowAttributes != null && workflowAttributes.getSearchingTypeDefinitions() != null) {
 199  0
             for (SearchingTypeDefinition definition : workflowAttributes.getSearchingTypeDefinitions()) {
 200  0
                 valuesToIndex.addAll(aardvarkValuesForSearchingTypeDefinition(document, definition));
 201  
             }
 202  
         }
 203  0
         return valuesToIndex;
 204  
     }
 205  
     
 206  
     /**
 207  
      * Pulls SearchableAttributeValue values from the given document for the given searchingTypeDefinition
 208  
      * @param document the document to get search values from
 209  
      * @param searchingTypeDefinition the current SearchingTypeDefinition to find values for
 210  
      * @return a List of SearchableAttributeValue implementations
 211  
      */
 212  
     protected List<SearchableAttributeValue> aardvarkValuesForSearchingTypeDefinition(Document document, SearchingTypeDefinition searchingTypeDefinition) {
 213  0
         List<SearchableAttributeValue> searchAttributes = new ArrayList<SearchableAttributeValue>();
 214  
         
 215  0
         final List<Object> searchValues = aardvarkSearchValuesForPaths(document, searchingTypeDefinition.getDocumentValues());
 216  0
         for (Object value : searchValues) {
 217  
             try {
 218  0
                 final SearchableAttributeValue searchableAttributeValue = buildSearchableAttribute(((Class<? extends BusinessObject>)Class.forName(searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName())), searchingTypeDefinition.getSearchingAttribute().getAttributeName(), value);
 219  0
                 if (searchableAttributeValue != null) {
 220  0
                     searchAttributes.add(searchableAttributeValue);
 221  
                 }
 222  
             }
 223  0
             catch (ClassNotFoundException cnfe) {
 224  0
                 throw new RuntimeException("Could not find instance of class "+searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName(), cnfe);
 225  0
             }
 226  
         }
 227  0
         return searchAttributes;
 228  
     }
 229  
     
 230  
     /**
 231  
      * Pulls values as objects from the document for the given paths
 232  
      * @param document the document to pull values from
 233  
      * @param paths the property paths to pull values
 234  
      * @return a List of values as Objects
 235  
      */
 236  
     protected List<Object> aardvarkSearchValuesForPaths(Document document, List<String> paths) {
 237  0
         List<Object> searchValues = new ArrayList<Object>();
 238  0
         for (String path : paths) {
 239  0
             flatAdd(searchValues, getPropertyByPath(document, path.trim()));
 240  
         }
 241  0
         return searchValues;
 242  
     }
 243  
     
 244  
     /**
 245  
      * Removes empty AttributeSets from the given List of qualifiers
 246  
      * @param qualifiers a List of AttributeSets holding qualifiers for responsibilities
 247  
      * @return a cleaned up list of qualifiers
 248  
      */
 249  
     protected List<AttributeSet> cleanCollectionQualifiers(List<AttributeSet> qualifiers) {
 250  0
        List<AttributeSet> cleanedQualifiers = new ArrayList<AttributeSet>();
 251  0
        for (AttributeSet qualifier : qualifiers) {
 252  0
            if (qualifier.size() > 0) {
 253  0
                cleanedQualifiers.add(qualifier);
 254  
            }
 255  
        }
 256  0
        return cleanedQualifiers;
 257  
     }
 258  
     
 259  
     /**
 260  
      * @see org.kuali.kfs.sys.document.service.WorkflowAttributePropertyResolutionService#determineFieldDataType(java.lang.Class, java.lang.String)
 261  
      */
 262  
     public String determineFieldDataType(Class<? extends BusinessObject> businessObjectClass, String attributeName) {
 263  0
         final Class attributeClass = thieveAttributeClassFromBusinessObjectClass(businessObjectClass, attributeName);
 264  0
         if (isStringy(attributeClass)) return SearchableAttribute.DATA_TYPE_STRING; // our most common case should go first
 265  0
         if (isDecimaltastic(attributeClass)) return SearchableAttribute.DATA_TYPE_FLOAT;
 266  0
         if (isDateLike(attributeClass)) return SearchableAttribute.DATA_TYPE_DATE;
 267  0
         if (isIntsy(attributeClass)) return SearchableAttribute.DATA_TYPE_LONG;
 268  0
         if (isBooleanable(attributeClass)) return DataDictionarySearchableAttribute.DATA_TYPE_BOOLEAN;
 269  0
         return SearchableAttribute.DATA_TYPE_STRING; // default to String
 270  
     }
 271  
 
 272  
     /**
 273  
      * Using the type of the sent in value, determines what kind of SearchableAttributeValue implementation should be passed back 
 274  
      * @param attributeKey
 275  
      * @param value
 276  
      * @return
 277  
      */
 278  
     public SearchableAttributeValue buildSearchableAttribute(Class<? extends BusinessObject> businessObjectClass, String attributeKey, Object value) {
 279  0
         if (value == null) return null;
 280  0
         final String fieldDataType = determineFieldDataType(businessObjectClass, attributeKey);
 281  0
         if (fieldDataType.equals(SearchableAttribute.DATA_TYPE_STRING)) return buildSearchableStringAttribute(attributeKey, value); // our most common case should go first
 282  0
         if (fieldDataType.equals(SearchableAttribute.DATA_TYPE_FLOAT) && isDecimaltastic(value.getClass())) return buildSearchableRealAttribute(attributeKey, value);
 283  0
         if (fieldDataType.equals(SearchableAttribute.DATA_TYPE_DATE) && isDateLike(value.getClass())) return buildSearchableDateTimeAttribute(attributeKey, value);
 284  0
         if (fieldDataType.equals(SearchableAttribute.DATA_TYPE_LONG) && isIntsy(value.getClass())) return buildSearchableFixnumAttribute(attributeKey, value);
 285  0
         if (fieldDataType.equals(DataDictionarySearchableAttribute.DATA_TYPE_BOOLEAN) && isBooleanable(value.getClass())) return buildSearchableYesNoAttribute(attributeKey, value);
 286  0
         return buildSearchableStringAttribute(attributeKey, value);
 287  
     }
 288  
     
 289  
     /**
 290  
      * Determines if the given Class is a String
 291  
      * @param clazz the class to check for Stringiness
 292  
      * @return true if the Class is a String, false otherwise
 293  
      */
 294  
     protected boolean isStringy(Class clazz) {
 295  0
         return java.lang.String.class.isAssignableFrom(clazz);
 296  
     }
 297  
 
 298  
     /**
 299  
      * Determines if the given class is enough like a date to store values of it as a SearchableAttributeDateTimeValue
 300  
      * @param class the class to determine the type of
 301  
      * @return true if it is like a date, false otherwise
 302  
      */
 303  
     protected boolean isDateLike(Class clazz) {
 304  0
         return java.util.Date.class.isAssignableFrom(clazz);
 305  
     }
 306  
     
 307  
     /**
 308  
      * Determines if the given class is enough like a Float to store values of it as a SearchableAttributeFloatValue
 309  
      * @param value the class to determine of the type of
 310  
      * @return true if it is like a "float", false otherwise
 311  
      */
 312  
     protected boolean isDecimaltastic(Class clazz) {
 313  0
         return java.lang.Double.class.isAssignableFrom(clazz) || java.lang.Float.class.isAssignableFrom(clazz) || clazz.equals(Double.TYPE) || clazz.equals(Float.TYPE) || java.math.BigDecimal.class.isAssignableFrom(clazz) || org.kuali.rice.kns.util.KualiDecimal.class.isAssignableFrom(clazz);
 314  
     }
 315  
     
 316  
     /**
 317  
      * Determines if the given class is enough like a "long" to store values of it as a SearchableAttributeLongValue
 318  
      * @param value the class to determine the type of
 319  
      * @return true if it is like a "long", false otherwise
 320  
      */
 321  
     protected boolean isIntsy(Class clazz) {
 322  0
         return java.lang.Integer.class.isAssignableFrom(clazz) || java.lang.Long.class.isAssignableFrom(clazz) || java.lang.Short.class.isAssignableFrom(clazz) || java.lang.Byte.class.isAssignableFrom(clazz) || java.math.BigInteger.class.isAssignableFrom(clazz) || clazz.equals(Integer.TYPE) || clazz.equals(Long.TYPE) || clazz.equals(Short.TYPE) || clazz.equals(Byte.TYPE);
 323  
     }
 324  
 
 325  
     /**
 326  
      * Determines if the given class is enough like a boolean, to index it as a String "Y" or "N"
 327  
      * @param value the class to determine the type of
 328  
      * @return true if it is like a boolean, false otherwise
 329  
      */
 330  
     protected boolean isBooleanable(Class clazz) {
 331  0
         return java.lang.Boolean.class.isAssignableFrom(clazz) || clazz.equals(Boolean.TYPE);
 332  
     }
 333  
     
 334  
     /**
 335  
      * Given a BusinessObject class and an attribute name, determines the class of that attribute on the BusinessObject class
 336  
      * @param boClass a class extending BusinessObject
 337  
      * @param attributeKey the name of a field on that class
 338  
      * @return the Class of the given attribute
 339  
      */
 340  
     private Class thieveAttributeClassFromBusinessObjectClass(Class<? extends BusinessObject> boClass, String attributeKey) {
 341  0
         Class attributeFieldClass = null;
 342  
         try {
 343  0
             final BeanInfo beanInfo = Introspector.getBeanInfo(boClass);
 344  0
             int i = 0;
 345  0
             while (attributeFieldClass == null && i < beanInfo.getPropertyDescriptors().length) {
 346  0
                 final PropertyDescriptor prop = beanInfo.getPropertyDescriptors()[i];
 347  0
                 if (prop.getName().equals(attributeKey)) {
 348  0
                     attributeFieldClass = prop.getPropertyType();
 349  
                 }
 350  0
                 i += 1;
 351  0
             }
 352  
         }
 353  0
         catch (SecurityException se) {
 354  0
             throw new RuntimeException("Could not determine type of attribute "+attributeKey+" of BusinessObject class "+boClass.getName(), se);
 355  
         }
 356  0
         catch (IntrospectionException ie) {
 357  0
             throw new RuntimeException("Could not determine type of attribute "+attributeKey+" of BusinessObject class "+boClass.getName(), ie);
 358  0
         }
 359  0
         return attributeFieldClass;
 360  
     }
 361  
     
 362  
     /**
 363  
      * Builds a date time SearchableAttributeValue for the given key and value
 364  
      * @param attributeKey the key for the searchable attribute
 365  
      * @param value the value that will be coerced to date/time data
 366  
      * @return the generated SearchableAttributeDateTimeValue
 367  
      */
 368  
     protected SearchableAttributeDateTimeValue buildSearchableDateTimeAttribute(String attributeKey, Object value) {
 369  0
         SearchableAttributeDateTimeValue attribute = new SearchableAttributeDateTimeValue();
 370  0
         attribute.setSearchableAttributeKey(attributeKey);
 371  0
         attribute.setSearchableAttributeValue(new Timestamp(((java.util.Date)value).getTime()));
 372  0
         return attribute;
 373  
     }
 374  
     
 375  
     /**
 376  
      * Builds a "float" SearchableAttributeValue for the given key and value
 377  
      * @param attributeKey the key for the searchable attribute
 378  
      * @param value the value that will be coerced to "float" data
 379  
      * @return the generated SearchableAttributeFloatValue
 380  
      */
 381  
     protected SearchableAttributeFloatValue buildSearchableRealAttribute(String attributeKey, Object value) {
 382  0
         SearchableAttributeFloatValue attribute = new SearchableAttributeFloatValue();
 383  0
         attribute.setSearchableAttributeKey(attributeKey);
 384  0
         if (value instanceof BigDecimal) {
 385  0
             attribute.setSearchableAttributeValue((BigDecimal)value);
 386  0
         } else if (value instanceof KualiDecimal) {
 387  0
             attribute.setSearchableAttributeValue(((KualiDecimal)value).bigDecimalValue());
 388  
         } else {
 389  0
             attribute.setSearchableAttributeValue(new BigDecimal(((Number)value).doubleValue()));
 390  
         }
 391  0
         return attribute;
 392  
     }
 393  
     
 394  
     /**
 395  
      * Builds a "integer" SearchableAttributeValue for the given key and value
 396  
      * @param attributeKey the key for the searchable attribute
 397  
      * @param value the value that will be coerced to "integer" type data
 398  
      * @return the generated SearchableAttributeLongValue
 399  
      */
 400  
     protected SearchableAttributeLongValue buildSearchableFixnumAttribute(String attributeKey, Object value) {
 401  0
         SearchableAttributeLongValue attribute = new SearchableAttributeLongValue();
 402  0
         attribute.setSearchableAttributeKey(attributeKey);
 403  0
         attribute.setSearchableAttributeValue(new Long(((Number)value).longValue()));
 404  0
         return attribute;
 405  
     }
 406  
     
 407  
     /**
 408  
      * Our last ditch attempt, this builds a String SearchableAttributeValue for the given key and value
 409  
      * @param attributeKey the key for the searchable attribute
 410  
      * @param value the value that will be coerced to a String
 411  
      * @return the generated SearchableAttributeStringValue
 412  
      */
 413  
     protected SearchableAttributeStringValue buildSearchableStringAttribute(String attributeKey, Object value) {
 414  0
         SearchableAttributeStringValue attribute = new SearchableAttributeStringValue();
 415  0
         attribute.setSearchableAttributeKey(attributeKey);
 416  0
         attribute.setSearchableAttributeValue(value.toString());
 417  0
         return attribute;
 418  
     }
 419  
     
 420  
     /**
 421  
      * This builds a String SearchableAttributeValue for the given key and value, correctly correlating booleans
 422  
      * @param attributeKey the key for the searchable attribute
 423  
      * @param value the value that will be coerced to a String
 424  
      * @return the generated SearchableAttributeStringValue
 425  
      */
 426  
     protected SearchableAttributeStringValue buildSearchableYesNoAttribute(String attributeKey, Object value) {
 427  0
         SearchableAttributeStringValue attribute = new SearchableAttributeStringValue();
 428  0
         attribute.setSearchableAttributeKey(attributeKey);
 429  0
         final String boolValueAsString = booleanValueAsString((Boolean)value);
 430  0
         attribute.setSearchableAttributeValue(boolValueAsString);
 431  0
         return attribute;
 432  
     }
 433  
     
 434  
     /**
 435  
      * Converts the given boolean value to "" for null, "Y" for true, "N" for false
 436  
      * @param booleanValue the boolean value to convert
 437  
      * @return the corresponding String "Y","N", or ""
 438  
      */
 439  
     private String booleanValueAsString(Boolean booleanValue) {
 440  0
         if (booleanValue == null) return "";
 441  0
         if (booleanValue.booleanValue()) return "Y";
 442  0
         return "N";
 443  
     }
 444  
 
 445  
     /**
 446  
      * @see org.kuali.kfs.sys.document.service.WorkflowAttributePropertyResolutionService#getPropertyByPath(java.lang.Object, java.lang.String)
 447  
      */
 448  
     public Object getPropertyByPath(Object object, String path) {
 449  0
         if (object instanceof Collection) return getPropertyOfCollectionByPath((Collection)object, path);
 450  
 
 451  0
         final String[] splitPath = headAndTailPath(path);
 452  0
         final String head = splitPath[0];
 453  0
         final String tail = splitPath[1];
 454  
         
 455  0
         if (object instanceof PersistableBusinessObject && tail != null) {
 456  0
             if (getBusinessObjectMetaDataService().getBusinessObjectRelationship((BusinessObject) object, head) != null) {
 457  0
                 ((PersistableBusinessObject)object).refreshReferenceObject(head);
 458  
 
 459  
             }
 460  
         }
 461  0
         final Object headValue = ObjectUtils.getPropertyValue(object, head);
 462  0
         if (!ObjectUtils.isNull(headValue)) {
 463  0
             if (tail == null) {
 464  0
                 return headValue;
 465  
             } else {
 466  
                 // we've still got path left...
 467  0
                 if (headValue instanceof Collection) {
 468  
                     // oh dear, a collection; we've got to loop through this
 469  0
                     Collection values = makeNewCollectionOfSameType((Collection)headValue);
 470  0
                     for (Object currentElement : (Collection)headValue) {
 471  0
                         flatAdd(values, getPropertyByPath(currentElement, tail));
 472  
                     }
 473  0
                     return values;
 474  
                 } else {
 475  0
                     return getPropertyByPath(headValue, tail);
 476  
                 }
 477  
             }
 478  
         }
 479  0
         return null;
 480  
     }
 481  
     
 482  
     /**
 483  
      * Finds a child object, specified by the given path, on each object of the given collection
 484  
      * @param collection the collection of objects
 485  
      * @param path the path of the property to retrieve
 486  
      * @return a Collection of the values culled from each child
 487  
      */
 488  
     public Collection getPropertyOfCollectionByPath(Collection collection, String path) {
 489  0
         Collection values = makeNewCollectionOfSameType(collection);
 490  0
         for (Object o : collection) {
 491  0
             flatAdd(values, getPropertyByPath(o, path));
 492  
         }
 493  0
         return values;
 494  
     }
 495  
     
 496  
     /**
 497  
      * Makes a new collection of exactly the same type of the collection that was handed to it
 498  
      * @param collection the collection to make a new collection of the same type as
 499  
      * @return a new collection.  Of the same type.
 500  
      */
 501  
     public Collection makeNewCollectionOfSameType(Collection collection) {
 502  0
         if (collection instanceof List) return new ArrayList();
 503  0
         if (collection instanceof Set) return new HashSet();
 504  
         try {
 505  0
             return collection.getClass().newInstance();
 506  
         }
 507  0
         catch (InstantiationException ie) {
 508  0
             throw new RuntimeException("Couldn't instantiate class of collection we'd already instantiated??", ie);
 509  
         }
 510  0
         catch (IllegalAccessException iae) {
 511  0
             throw new RuntimeException("Illegal Access on class of collection we'd already accessed??", iae);
 512  
         }
 513  
     }
 514  
     
 515  
     /**
 516  
      * Splits the first property off from a path, leaving the tail
 517  
      * @param path the path to split
 518  
      * @return an array; if the path is nested, the first element will be the first part of the path up to a "." and second element is the rest of the path while if the path is simple, returns the path as the first element and a null as the second element
 519  
      */
 520  
     protected String[] headAndTailPath(String path) {
 521  0
         final int firstDot = path.indexOf('.');
 522  0
         if (firstDot < 0) {
 523  0
             return new String[] { path, null };
 524  
         }
 525  0
         return new String[] { path.substring(0, firstDot), path.substring(firstDot + 1) };
 526  
     }
 527  
     
 528  
     /**
 529  
      * Convenience method which makes sure that if the given object is a collection, it is added to the given collection flatly
 530  
      * @param c a collection, ready to be added to
 531  
      * @param o an object of dubious type
 532  
      */
 533  
     protected void flatAdd(Collection c, Object o) {
 534  0
         if (o instanceof Collection) {
 535  0
             c.addAll((Collection) o);
 536  
         } else {
 537  0
             c.add(o);
 538  
         }
 539  0
     }
 540  
 
 541  
     /**
 542  
      * Gets the persistenceStructureService attribute. 
 543  
      * @return Returns the persistenceStructureService.
 544  
      */
 545  
     public PersistenceStructureService getPersistenceStructureService() {
 546  0
         return persistenceStructureService;
 547  
     }
 548  
 
 549  
     /**
 550  
      * Sets the persistenceStructureService attribute value.
 551  
      * @param persistenceStructureService The persistenceStructureService to set.
 552  
      */
 553  
     public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
 554  0
         this.persistenceStructureService = persistenceStructureService;
 555  0
     }
 556  
     
 557  
     /**
 558  
      * Inner helper class which will track which routing attributes have been used
 559  
      */
 560  0
     class RoutingAttributeTracker {
 561  
         
 562  
         private List<RoutingAttribute> routingAttributes;
 563  
         private int currentRoutingAttributeIndex;
 564  
         private Stack<Integer> checkPoints;
 565  
         
 566  
         /**
 567  
          * Constructs a WorkflowAttributePropertyResolutionServiceImpl
 568  
          * @param routingAttributes the routing attributes to track
 569  
          */
 570  0
         public RoutingAttributeTracker(List<RoutingAttribute> routingAttributes) {
 571  0
             this.routingAttributes = routingAttributes;
 572  0
             checkPoints = new Stack<Integer>();
 573  0
         }
 574  
         
 575  
         /**
 576  
          * @return the routing attribute hopefully associated with the current qualifier
 577  
          */
 578  
         public RoutingAttribute getCurrentRoutingAttribute() {
 579  0
             return routingAttributes.get(currentRoutingAttributeIndex);
 580  
         }
 581  
         
 582  
         /**
 583  
          * Moves this routing attribute tracker to its next routing attribute
 584  
          */
 585  
         public void moveToNext() {
 586  0
             currentRoutingAttributeIndex += 1;
 587  0
         }
 588  
         
 589  
         /**
 590  
          * Check points at the current routing attribute, so that this position is saved
 591  
          */
 592  
         public void checkPoint() {
 593  0
             checkPoints.push(new Integer(currentRoutingAttributeIndex));
 594  0
         }
 595  
         
 596  
         /**
 597  
          * Returns to the point of the last check point
 598  
          */
 599  
         public void backUpToCheckPoint() {
 600  0
             currentRoutingAttributeIndex = checkPoints.pop().intValue();
 601  0
         }
 602  
         
 603  
         /**
 604  
          * Resets this RoutingAttributeTracker, setting the current RoutingAttribute back to the top one and
 605  
          * clearing the check point stack
 606  
          */
 607  
         public void reset() {
 608  0
             currentRoutingAttributeIndex = 0;
 609  0
             checkPoints.clear();
 610  0
         }
 611  
     }
 612  
 
 613  
     protected BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
 614  0
         if ( businessObjectMetaDataService == null ) {
 615  0
             businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService();
 616  
         }
 617  0
         return businessObjectMetaDataService;
 618  
     }
 619  
 }