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