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