Coverage Report - org.kuali.rice.krad.workflow.service.impl.WorkflowAttributePropertyResolutionServiceImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
WorkflowAttributePropertyResolutionServiceImpl
0%
0/154
0%
0/108
3.406
WorkflowAttributePropertyResolutionServiceImpl$RoutingAttributeTracker
0%
0/14
N/A
3.406
 
 1  
 /**
 2  
  * Copyright 2005-2011 The Kuali Foundation
 3  
  *
 4  
  * Licensed under the Educational Community License, Version 2.0 (the "License");
 5  
  * you may not use this file except in compliance with the License.
 6  
  * You may obtain a copy of the License at
 7  
  *
 8  
  * http://www.opensource.org/licenses/ecl2.php
 9  
  *
 10  
  * Unless required by applicable law or agreed to in writing, software
 11  
  * distributed under the License is distributed on an "AS IS" BASIS,
 12  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  
  * See the License for the specific language governing permissions and
 14  
  * limitations under the License.
 15  
  */
 16  
 package org.kuali.rice.krad.workflow.service.impl;
 17  
 
 18  
 import org.joda.time.DateTime;
 19  
 import org.kuali.rice.core.api.util.type.KualiDecimal;
 20  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
 21  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDateTime;
 22  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDecimal;
 23  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
 24  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeInteger;
 25  
 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeString;
 26  
 import org.kuali.rice.kew.api.KewApiConstants;
 27  
 import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
 28  
 import org.kuali.rice.kns.service.KNSServiceLocator;
 29  
 import org.kuali.rice.krad.bo.BusinessObject;
 30  
 import org.kuali.rice.krad.bo.PersistableBusinessObject;
 31  
 import org.kuali.rice.krad.datadictionary.DocumentCollectionPath;
 32  
 import org.kuali.rice.krad.datadictionary.DocumentValuePathGroup;
 33  
 import org.kuali.rice.krad.datadictionary.RoutingAttribute;
 34  
 import org.kuali.rice.krad.datadictionary.RoutingTypeDefinition;
 35  
 import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
 36  
 import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
 37  
 import org.kuali.rice.krad.document.Document;
 38  
 import org.kuali.rice.krad.service.PersistenceStructureService;
 39  
 import org.kuali.rice.krad.util.DataTypeUtil;
 40  
 import org.kuali.rice.krad.util.ObjectUtils;
 41  
 import org.kuali.rice.krad.workflow.attribute.DataDictionarySearchableAttribute;
 42  
 import org.kuali.rice.krad.workflow.service.WorkflowAttributePropertyResolutionService;
 43  
 
 44  
 import java.math.BigDecimal;
 45  
 import java.math.BigInteger;
 46  
 import java.util.ArrayList;
 47  
 import java.util.Collection;
 48  
 import java.util.HashMap;
 49  
 import java.util.HashSet;
 50  
 import java.util.List;
 51  
 import java.util.Map;
 52  
 import java.util.Set;
 53  
 import java.util.Stack;
 54  
 
 55  
 /**
 56  
  * The default implementation of the WorkflowAttributePropertyResolutionServiceImpl
 57  
  */
 58  0
 public class WorkflowAttributePropertyResolutionServiceImpl implements WorkflowAttributePropertyResolutionService {
 59  
     
 60  
     private PersistenceStructureService persistenceStructureService;
 61  
     private BusinessObjectMetaDataService businessObjectMetaDataService;
 62  
 
 63  
     /**
 64  
      * Using the proper RoutingTypeDefinition for the current routing node of the document, aardvarks out the proper routing type qualifiers
 65  
      */
 66  
     public List<Map<String, String>> resolveRoutingTypeQualifiers(Document document, RoutingTypeDefinition routingTypeDefinition) {
 67  0
         List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
 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 Map<String, String>s
 85  
      */
 86  
     protected List<Map<String, String>> resolveDocumentValuePath(BusinessObject businessObject, DocumentValuePathGroup group, RoutingAttributeTracker routingAttributeTracker) {
 87  
         List<Map<String, String>> qualifiers;
 88  0
         Map<String, String> qualifier = new HashMap<String, String>();
 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 (Map<String, String> collectionElementQualifier : qualifiers) {
 99  0
                 copyQualifications(qualifier, collectionElementQualifier);
 100  
             }
 101  
         } else {
 102  0
             qualifiers = new ArrayList<Map<String, String>>();
 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 Map<String, String>s
 113  
      */
 114  
     protected List<Map<String, String>> resolveDocumentCollectionPath(BusinessObject businessObject, DocumentCollectionPath collectionPath, RoutingAttributeTracker routingAttributeTracker) {
 115  0
         List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
 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<Map<String, String>> childQualifiers = resolveDocumentCollectionPath((BusinessObject)collectionElement, collectionPath.getNestedCollection(), routingAttributeTracker);
 124  0
                         for (Map<String, String> childQualifier : childQualifiers) {
 125  0
                             Map<String, String> qualifier = new HashMap<String, String>();
 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
                     Map<String, String> qualifier = new HashMap<String, String>();
 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 Map<String, String>, 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 routingAttributes the RoutingAttribute associated with this qualifier's document value
 165  
      * @param qualifier the qualifier to put values into
 166  
      */
 167  
     protected void addPathValuesToQualifier(Object businessObject, List<String> paths, RoutingAttributeTracker routingAttributes, Map<String, String> qualifier) {
 168  0
         if (ObjectUtils.isNotNull(paths)) {
 169  0
             for (String path : paths) {
 170  
                 // get the values for the paths of each element of the collection
 171  0
                 final Object value = getPropertyByPath(businessObject, path.trim());
 172  0
                 if (value != null) {
 173  0
                     qualifier.put(routingAttributes.getCurrentRoutingAttribute().getQualificationAttributeName(), value.toString());
 174  
                 }
 175  0
                 routingAttributes.moveToNext();
 176  0
             }
 177  
         }
 178  0
     }
 179  
     
 180  
     /**
 181  
      * Copies all the values from one qualifier to another
 182  
      * @param source the source of values
 183  
      * @param target the place to write all the values to
 184  
      */
 185  
     protected void copyQualifications(Map<String, String> source, Map<String, String> target) {
 186  0
         for (String key : source.keySet()) {
 187  0
             target.put(key, source.get(key));
 188  
         }
 189  0
     }
 190  
 
 191  
     /**
 192  
      * Resolves all of the searching values to index for the given document, returning a list of SearchableAttributeValue implementations
 193  
      *
 194  
      */
 195  
     public List<DocumentAttribute> resolveSearchableAttributeValues(Document document, WorkflowAttributes workflowAttributes) {
 196  0
         List<DocumentAttribute> valuesToIndex = new ArrayList<DocumentAttribute>();
 197  0
         if (workflowAttributes != null && workflowAttributes.getSearchingTypeDefinitions() != null) {
 198  0
             for (SearchingTypeDefinition definition : workflowAttributes.getSearchingTypeDefinitions()) {
 199  0
                 valuesToIndex.addAll(aardvarkValuesForSearchingTypeDefinition(document, definition));
 200  
             }
 201  
         }
 202  0
         return valuesToIndex;
 203  
     }
 204  
     
 205  
     /**
 206  
      * Pulls SearchableAttributeValue values from the given document for the given searchingTypeDefinition
 207  
      * @param document the document to get search values from
 208  
      * @param searchingTypeDefinition the current SearchingTypeDefinition to find values for
 209  
      * @return a List of SearchableAttributeValue implementations
 210  
      */
 211  
     protected List<DocumentAttribute> aardvarkValuesForSearchingTypeDefinition(Document document, SearchingTypeDefinition searchingTypeDefinition) {
 212  0
         List<DocumentAttribute> searchAttributes = new ArrayList<DocumentAttribute>();
 213  
         
 214  0
         final List<Object> searchValues = aardvarkSearchValuesForPaths(document, searchingTypeDefinition.getDocumentValues());
 215  0
         for (Object value : searchValues) {
 216  
             try {
 217  0
                 final DocumentAttribute searchableAttributeValue = buildSearchableAttribute(((Class<? extends BusinessObject>)Class.forName(searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName())), searchingTypeDefinition.getSearchingAttribute().getAttributeName(), value);
 218  0
                 if (searchableAttributeValue != null) {
 219  0
                     searchAttributes.add(searchableAttributeValue);
 220  
                 }
 221  
             }
 222  0
             catch (ClassNotFoundException cnfe) {
 223  0
                 throw new RuntimeException("Could not find instance of class "+searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName(), cnfe);
 224  0
             }
 225  
         }
 226  0
         return searchAttributes;
 227  
     }
 228  
     
 229  
     /**
 230  
      * Pulls values as objects from the document for the given paths
 231  
      * @param document the document to pull values from
 232  
      * @param paths the property paths to pull values
 233  
      * @return a List of values as Objects
 234  
      */
 235  
     protected List<Object> aardvarkSearchValuesForPaths(Document document, List<String> paths) {
 236  0
         List<Object> searchValues = new ArrayList<Object>();
 237  0
         for (String path : paths) {
 238  0
             flatAdd(searchValues, getPropertyByPath(document, path.trim()));
 239  
         }
 240  0
         return searchValues;
 241  
     }
 242  
     
 243  
     /**
 244  
      * Removes empty Map<String, String>s from the given List of qualifiers
 245  
      * @param qualifiers a List of Map<String, String>s holding qualifiers for responsibilities
 246  
      * @return a cleaned up list of qualifiers
 247  
      */
 248  
     protected List<Map<String, String>> cleanCollectionQualifiers(List<Map<String, String>> qualifiers) {
 249  0
        List<Map<String, String>> cleanedQualifiers = new ArrayList<Map<String, String>>();
 250  0
        for (Map<String, String> qualifier : qualifiers) {
 251  0
            if (qualifier.size() > 0) {
 252  0
                cleanedQualifiers.add(qualifier);
 253  
            }
 254  
        }
 255  0
        return cleanedQualifiers;
 256  
     }
 257  
 
 258  
     public String determineFieldDataType(Class<? extends BusinessObject> businessObjectClass, String attributeName) {
 259  0
         return DataTypeUtil.determineFieldDataType(businessObjectClass, attributeName);
 260  
     }
 261  
 
 262  
     /**
 263  
      * Using the type of the sent in value, determines what kind of SearchableAttributeValue implementation should be passed back 
 264  
      * @param attributeKey
 265  
      * @param value
 266  
      * @return
 267  
      */
 268  
     public DocumentAttribute buildSearchableAttribute(Class<? extends BusinessObject> businessObjectClass, String attributeKey, Object value) {
 269  0
         if (value == null) return null;
 270  0
         final String fieldDataType = determineFieldDataType(businessObjectClass, attributeKey);
 271  0
         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING)) return buildSearchableStringAttribute(attributeKey, value); // our most common case should go first
 272  0
         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT) && DataTypeUtil.isDecimaltastic(value.getClass())) return buildSearchableRealAttribute(attributeKey, value);
 273  0
         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE) && DataTypeUtil.isDateLike(value.getClass())) return buildSearchableDateTimeAttribute(attributeKey, value);
 274  0
         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG) && DataTypeUtil.isIntsy(value.getClass())) return buildSearchableFixnumAttribute(attributeKey, value);
 275  0
         if (fieldDataType.equals(DataDictionarySearchableAttribute.DATA_TYPE_BOOLEAN) && DataTypeUtil.isBooleanable(value.getClass())) return buildSearchableYesNoAttribute(attributeKey, value);
 276  0
         return buildSearchableStringAttribute(attributeKey, value);
 277  
     }
 278  
     
 279  
     /**
 280  
      * Builds a date time SearchableAttributeValue for the given key and value
 281  
      * @param attributeKey the key for the searchable attribute
 282  
      * @param value the value that will be coerced to date/time data
 283  
      * @return the generated SearchableAttributeDateTimeValue
 284  
      */
 285  
     protected DocumentAttributeDateTime buildSearchableDateTimeAttribute(String attributeKey, Object value) {
 286  0
         return DocumentAttributeFactory.createDateTimeAttribute(attributeKey, new DateTime(value));
 287  
     }
 288  
     
 289  
     /**
 290  
      * Builds a "float" SearchableAttributeValue for the given key and value
 291  
      * @param attributeKey the key for the searchable attribute
 292  
      * @param value the value that will be coerced to "float" data
 293  
      * @return the generated SearchableAttributeFloatValue
 294  
      */
 295  
     protected DocumentAttributeDecimal buildSearchableRealAttribute(String attributeKey, Object value) {
 296  0
         BigDecimal decimalValue = null;
 297  0
         if (value instanceof BigDecimal) {
 298  0
             decimalValue = (BigDecimal)value;
 299  0
         } else if (value instanceof KualiDecimal) {
 300  0
             decimalValue = ((KualiDecimal)value).bigDecimalValue();
 301  
         } else {
 302  0
             decimalValue = new BigDecimal(((Number)value).doubleValue());
 303  
         }
 304  0
         return DocumentAttributeFactory.createDecimalAttribute(attributeKey, decimalValue);
 305  
     }
 306  
     
 307  
     /**
 308  
      * Builds a "integer" SearchableAttributeValue for the given key and value
 309  
      * @param attributeKey the key for the searchable attribute
 310  
      * @param value the value that will be coerced to "integer" type data
 311  
      * @return the generated SearchableAttributeLongValue
 312  
      */
 313  
     protected DocumentAttributeInteger buildSearchableFixnumAttribute(String attributeKey, Object value) {
 314  0
         BigInteger integerValue = null;
 315  0
         if (value instanceof BigInteger) {
 316  0
             integerValue = (BigInteger)value;
 317  
         } else {
 318  0
             integerValue = BigInteger.valueOf(((Number)value).longValue());
 319  
         }
 320  0
         return DocumentAttributeFactory.createIntegerAttribute(attributeKey, integerValue);
 321  
     }
 322  
     
 323  
     /**
 324  
      * Our last ditch attempt, this builds a String SearchableAttributeValue for the given key and value
 325  
      * @param attributeKey the key for the searchable attribute
 326  
      * @param value the value that will be coerced to a String
 327  
      * @return the generated SearchableAttributeStringValue
 328  
      */
 329  
     protected DocumentAttributeString buildSearchableStringAttribute(String attributeKey, Object value) {
 330  0
         return DocumentAttributeFactory.createStringAttribute(attributeKey, value.toString());
 331  
     }
 332  
     
 333  
     /**
 334  
      * This builds a String SearchableAttributeValue for the given key and value, correctly correlating booleans
 335  
      * @param attributeKey the key for the searchable attribute
 336  
      * @param value the value that will be coerced to a String
 337  
      * @return the generated SearchableAttributeStringValue
 338  
      */
 339  
     protected DocumentAttributeString buildSearchableYesNoAttribute(String attributeKey, Object value) {
 340  0
         final String boolValueAsString = booleanValueAsString((Boolean)value);
 341  0
         return DocumentAttributeFactory.createStringAttribute(attributeKey, boolValueAsString);
 342  
    }
 343  
     
 344  
     /**
 345  
      * Converts the given boolean value to "" for null, "Y" for true, "N" for false
 346  
      * @param booleanValue the boolean value to convert
 347  
      * @return the corresponding String "Y","N", or ""
 348  
      */
 349  
     private String booleanValueAsString(Boolean booleanValue) {
 350  0
         if (booleanValue == null) return "";
 351  0
         if (booleanValue.booleanValue()) return "Y";
 352  0
         return "N";
 353  
     }
 354  
 
 355  
     public Object getPropertyByPath(Object object, String path) {
 356  0
         if (object instanceof Collection) return getPropertyOfCollectionByPath((Collection)object, path);
 357  
 
 358  0
         final String[] splitPath = headAndTailPath(path);
 359  0
         final String head = splitPath[0];
 360  0
         final String tail = splitPath[1];
 361  
         
 362  0
         if (object instanceof PersistableBusinessObject && tail != null) {
 363  0
             if (getBusinessObjectMetaDataService().getBusinessObjectRelationship((BusinessObject) object, head) != null) {
 364  0
                 ((PersistableBusinessObject)object).refreshReferenceObject(head);
 365  
 
 366  
             }
 367  
         }
 368  0
         final Object headValue = ObjectUtils.getPropertyValue(object, head);
 369  0
         if (!ObjectUtils.isNull(headValue)) {
 370  0
             if (tail == null) {
 371  0
                 return headValue;
 372  
             } else {
 373  
                 // we've still got path left...
 374  0
                 if (headValue instanceof Collection) {
 375  
                     // oh dear, a collection; we've got to loop through this
 376  0
                     Collection values = makeNewCollectionOfSameType((Collection)headValue);
 377  0
                     for (Object currentElement : (Collection)headValue) {
 378  0
                         flatAdd(values, getPropertyByPath(currentElement, tail));
 379  
                     }
 380  0
                     return values;
 381  
                 } else {
 382  0
                     return getPropertyByPath(headValue, tail);
 383  
                 }
 384  
             }
 385  
         }
 386  0
         return null;
 387  
     }
 388  
     
 389  
     /**
 390  
      * Finds a child object, specified by the given path, on each object of the given collection
 391  
      * @param collection the collection of objects
 392  
      * @param path the path of the property to retrieve
 393  
      * @return a Collection of the values culled from each child
 394  
      */
 395  
     public Collection getPropertyOfCollectionByPath(Collection collection, String path) {
 396  0
         Collection values = makeNewCollectionOfSameType(collection);
 397  0
         for (Object o : collection) {
 398  0
             flatAdd(values, getPropertyByPath(o, path));
 399  
         }
 400  0
         return values;
 401  
     }
 402  
     
 403  
     /**
 404  
      * Makes a new collection of exactly the same type of the collection that was handed to it
 405  
      * @param collection the collection to make a new collection of the same type as
 406  
      * @return a new collection.  Of the same type.
 407  
      */
 408  
     public Collection makeNewCollectionOfSameType(Collection collection) {
 409  0
         if (collection instanceof List) return new ArrayList();
 410  0
         if (collection instanceof Set) return new HashSet();
 411  
         try {
 412  0
             return collection.getClass().newInstance();
 413  
         }
 414  0
         catch (InstantiationException ie) {
 415  0
             throw new RuntimeException("Couldn't instantiate class of collection we'd already instantiated??", ie);
 416  
         }
 417  0
         catch (IllegalAccessException iae) {
 418  0
             throw new RuntimeException("Illegal Access on class of collection we'd already accessed??", iae);
 419  
         }
 420  
     }
 421  
     
 422  
     /**
 423  
      * Splits the first property off from a path, leaving the tail
 424  
      * @param path the path to split
 425  
      * @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
 426  
      */
 427  
     protected String[] headAndTailPath(String path) {
 428  0
         final int firstDot = path.indexOf('.');
 429  0
         if (firstDot < 0) {
 430  0
             return new String[] { path, null };
 431  
         }
 432  0
         return new String[] { path.substring(0, firstDot), path.substring(firstDot + 1) };
 433  
     }
 434  
     
 435  
     /**
 436  
      * Convenience method which makes sure that if the given object is a collection, it is added to the given collection flatly
 437  
      * @param c a collection, ready to be added to
 438  
      * @param o an object of dubious type
 439  
      */
 440  
     protected void flatAdd(Collection c, Object o) {
 441  0
         if (o instanceof Collection) {
 442  0
             c.addAll((Collection) o);
 443  
         } else {
 444  0
             c.add(o);
 445  
         }
 446  0
     }
 447  
 
 448  
     /**
 449  
      * Gets the persistenceStructureService attribute. 
 450  
      * @return Returns the persistenceStructureService.
 451  
      */
 452  
     public PersistenceStructureService getPersistenceStructureService() {
 453  0
         return persistenceStructureService;
 454  
     }
 455  
 
 456  
     /**
 457  
      * Sets the persistenceStructureService attribute value.
 458  
      * @param persistenceStructureService The persistenceStructureService to set.
 459  
      */
 460  
     public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
 461  0
         this.persistenceStructureService = persistenceStructureService;
 462  0
     }
 463  
     
 464  
     /**
 465  
      * Inner helper class which will track which routing attributes have been used
 466  
      */
 467  0
     class RoutingAttributeTracker {
 468  
         
 469  
         private List<RoutingAttribute> routingAttributes;
 470  
         private int currentRoutingAttributeIndex;
 471  
         private Stack<Integer> checkPoints;
 472  
         
 473  
         /**
 474  
          * Constructs a WorkflowAttributePropertyResolutionServiceImpl
 475  
          * @param routingAttributes the routing attributes to track
 476  
          */
 477  0
         public RoutingAttributeTracker(List<RoutingAttribute> routingAttributes) {
 478  0
             this.routingAttributes = routingAttributes;
 479  0
             checkPoints = new Stack<Integer>();
 480  0
         }
 481  
         
 482  
         /**
 483  
          * @return the routing attribute hopefully associated with the current qualifier
 484  
          */
 485  
         public RoutingAttribute getCurrentRoutingAttribute() {
 486  0
             return routingAttributes.get(currentRoutingAttributeIndex);
 487  
         }
 488  
         
 489  
         /**
 490  
          * Moves this routing attribute tracker to its next routing attribute
 491  
          */
 492  
         public void moveToNext() {
 493  0
             currentRoutingAttributeIndex += 1;
 494  0
         }
 495  
         
 496  
         /**
 497  
          * Check points at the current routing attribute, so that this position is saved
 498  
          */
 499  
         public void checkPoint() {
 500  0
             checkPoints.push(new Integer(currentRoutingAttributeIndex));
 501  0
         }
 502  
         
 503  
         /**
 504  
          * Returns to the point of the last check point
 505  
          */
 506  
         public void backUpToCheckPoint() {
 507  0
             currentRoutingAttributeIndex = checkPoints.pop().intValue();
 508  0
         }
 509  
         
 510  
         /**
 511  
          * Resets this RoutingAttributeTracker, setting the current RoutingAttribute back to the top one and
 512  
          * clearing the check point stack
 513  
          */
 514  
         public void reset() {
 515  0
             currentRoutingAttributeIndex = 0;
 516  0
             checkPoints.clear();
 517  0
         }
 518  
     }
 519  
 
 520  
     protected BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
 521  0
         if ( businessObjectMetaDataService == null ) {
 522  0
             businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService();
 523  
         }
 524  0
         return businessObjectMetaDataService;
 525  
     }
 526  
 }