View Javadoc

1   /**
2    * Copyright 2005-2014 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  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          List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
68          
69          if (routingTypeDefinition != null) {
70              document.populateDocumentForRouting();
71              RoutingAttributeTracker routingAttributeTracker = new RoutingAttributeTracker(routingTypeDefinition.getRoutingAttributes());
72              for (DocumentValuePathGroup documentValuePathGroup : routingTypeDefinition.getDocumentValuePathGroups()) {
73                  qualifiers.addAll(resolveDocumentValuePath(document, documentValuePathGroup, routingAttributeTracker));
74                  routingAttributeTracker.reset();
75              }
76          }
77          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          Map<String, String> qualifier = new HashMap<String, String>();
89          if (group.getDocumentValues() == null && group.getDocumentCollectionPath() == null) {
90              throw new IllegalStateException("A document value path group must have the documentValues property set, the documentCollectionPath property set, or both.");
91          }
92          if (group.getDocumentValues() != null) {
93              addPathValuesToQualifier(businessObject, group.getDocumentValues(), routingAttributeTracker, qualifier);
94          }
95          if (group.getDocumentCollectionPath() != null) {
96              qualifiers = resolveDocumentCollectionPath(businessObject, group.getDocumentCollectionPath(), routingAttributeTracker);
97              qualifiers = cleanCollectionQualifiers(qualifiers);
98              for (Map<String, String> collectionElementQualifier : qualifiers) {
99                  copyQualifications(qualifier, collectionElementQualifier);
100             }
101         } else {
102             qualifiers = new ArrayList<Map<String, String>>();
103             qualifiers.add(qualifier);
104         }
105         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         List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
116         final Collection collectionByPath = getCollectionByPath(businessObject, collectionPath.getCollectionPath());
117         if (!ObjectUtils.isNull(collectionByPath)) {
118             if (collectionPath.getNestedCollection() != null) {
119                 // we need to go through the collection...
120                 for (Object collectionElement : collectionByPath) {
121                     // for each element, we need to get the child qualifiers
122                     if (collectionElement instanceof BusinessObject) {
123                         List<Map<String, String>> childQualifiers = resolveDocumentCollectionPath((BusinessObject)collectionElement, collectionPath.getNestedCollection(), routingAttributeTracker);
124                         for (Map<String, String> childQualifier : childQualifiers) {
125                             Map<String, String> qualifier = new HashMap<String, String>();
126                             routingAttributeTracker.checkPoint();
127                             // now we need to get the values for the current element of the collection
128                             addPathValuesToQualifier(collectionElement, collectionPath.getDocumentValues(), routingAttributeTracker, qualifier);
129                             // and move all the child keys to the qualifier
130                             copyQualifications(childQualifier, qualifier);
131                             qualifiers.add(qualifier);
132                             routingAttributeTracker.backUpToCheckPoint();
133                         }
134                     }
135                 }
136             } else {
137                 // go through each element in the collection
138                 for (Object collectionElement : collectionByPath) {
139                     Map<String, String> qualifier = new HashMap<String, String>();
140                     routingAttributeTracker.checkPoint();
141                     addPathValuesToQualifier(collectionElement, collectionPath.getDocumentValues(), routingAttributeTracker, qualifier);
142                     qualifiers.add(qualifier);
143                     routingAttributeTracker.backUpToCheckPoint();
144                 }
145             }
146         }
147         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         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         if (ObjectUtils.isNotNull(paths)) {
169             for (String path : paths) {
170                 // get the values for the paths of each element of the collection
171                 final Object value = getPropertyByPath(businessObject, path.trim());
172                 if (value != null) {
173                     qualifier.put(routingAttributes.getCurrentRoutingAttribute().getQualificationAttributeName(), value.toString());
174                 }
175                 routingAttributes.moveToNext();
176             }
177         }
178     }
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         for (String key : source.keySet()) {
187             target.put(key, source.get(key));
188         }
189     }
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         List<DocumentAttribute> valuesToIndex = new ArrayList<DocumentAttribute>();
197         if (workflowAttributes != null && workflowAttributes.getSearchingTypeDefinitions() != null) {
198             for (SearchingTypeDefinition definition : workflowAttributes.getSearchingTypeDefinitions()) {
199                 valuesToIndex.addAll(aardvarkValuesForSearchingTypeDefinition(document, definition));
200             }
201         }
202         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         List<DocumentAttribute> searchAttributes = new ArrayList<DocumentAttribute>();
213         
214         final List<Object> searchValues = aardvarkSearchValuesForPaths(document, searchingTypeDefinition.getDocumentValues());
215         for (Object value : searchValues) {
216             try {
217                 final DocumentAttribute searchableAttributeValue = buildSearchableAttribute(((Class<? extends BusinessObject>)Class.forName(searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName())), searchingTypeDefinition.getSearchingAttribute().getAttributeName(), value);
218                 if (searchableAttributeValue != null) {
219                     searchAttributes.add(searchableAttributeValue);
220                 }
221             }
222             catch (ClassNotFoundException cnfe) {
223                 throw new RuntimeException("Could not find instance of class "+searchingTypeDefinition.getSearchingAttribute().getBusinessObjectClassName(), cnfe);
224             }
225         }
226         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         List<Object> searchValues = new ArrayList<Object>();
237         for (String path : paths) {
238             flatAdd(searchValues, getPropertyByPath(document, path.trim()));
239         }
240         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        List<Map<String, String>> cleanedQualifiers = new ArrayList<Map<String, String>>();
250        for (Map<String, String> qualifier : qualifiers) {
251            if (qualifier.size() > 0) {
252                cleanedQualifiers.add(qualifier);
253            }
254        }
255        return cleanedQualifiers;
256     }
257 
258     public String determineFieldDataType(Class<? extends BusinessObject> businessObjectClass, String attributeName) {
259         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         if (value == null) return null;
270         final String fieldDataType = determineFieldDataType(businessObjectClass, attributeKey);
271         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_STRING)) return buildSearchableStringAttribute(attributeKey, value); // our most common case should go first
272         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_FLOAT) && DataTypeUtil.isDecimaltastic(value.getClass())) return buildSearchableRealAttribute(attributeKey, value);
273         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_DATE) && DataTypeUtil.isDateLike(value.getClass())) return buildSearchableDateTimeAttribute(attributeKey, value);
274         if (fieldDataType.equals(KewApiConstants.SearchableAttributeConstants.DATA_TYPE_LONG) && DataTypeUtil.isIntsy(value.getClass())) return buildSearchableFixnumAttribute(attributeKey, value);
275         if (fieldDataType.equals(DataDictionarySearchableAttribute.DATA_TYPE_BOOLEAN) && DataTypeUtil.isBooleanable(value.getClass())) return buildSearchableYesNoAttribute(attributeKey, value);
276         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         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         BigDecimal decimalValue = null;
297         if (value instanceof BigDecimal) {
298             decimalValue = (BigDecimal)value;
299         } else if (value instanceof KualiDecimal) {
300             decimalValue = ((KualiDecimal)value).bigDecimalValue();
301         } else {
302             decimalValue = new BigDecimal(((Number)value).doubleValue());
303         }
304         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         BigInteger integerValue = null;
315         if (value instanceof BigInteger) {
316             integerValue = (BigInteger)value;
317         } else {
318             integerValue = BigInteger.valueOf(((Number)value).longValue());
319         }
320         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         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         final String boolValueAsString = booleanValueAsString((Boolean)value);
341         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         if (booleanValue == null) return "";
351         if (booleanValue.booleanValue()) return "Y";
352         return "N";
353     }
354 
355     public Object getPropertyByPath(Object object, String path) {
356         if (object instanceof Collection) return getPropertyOfCollectionByPath((Collection)object, path);
357 
358         final String[] splitPath = headAndTailPath(path);
359         final String head = splitPath[0];
360         final String tail = splitPath[1];
361         
362         if (object instanceof PersistableBusinessObject && tail != null) {
363             if (getBusinessObjectMetaDataService().getBusinessObjectRelationship((BusinessObject) object, head) != null) {
364                 ((PersistableBusinessObject)object).refreshReferenceObject(head);
365 
366             }
367         }
368         final Object headValue = ObjectUtils.getPropertyValue(object, head);
369         if (!ObjectUtils.isNull(headValue)) {
370             if (tail == null) {
371                 return headValue;
372             } else {
373                 // we've still got path left...
374                 if (headValue instanceof Collection) {
375                     // oh dear, a collection; we've got to loop through this
376                     Collection values = makeNewCollectionOfSameType((Collection)headValue);
377                     for (Object currentElement : (Collection)headValue) {
378                         flatAdd(values, getPropertyByPath(currentElement, tail));
379                     }
380                     return values;
381                 } else {
382                     return getPropertyByPath(headValue, tail);
383                 }
384             }
385         }
386         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         Collection values = makeNewCollectionOfSameType(collection);
397         for (Object o : collection) {
398             flatAdd(values, getPropertyByPath(o, path));
399         }
400         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         if (collection instanceof List) return new ArrayList();
410         if (collection instanceof Set) return new HashSet();
411         try {
412             return collection.getClass().newInstance();
413         }
414         catch (InstantiationException ie) {
415             throw new RuntimeException("Couldn't instantiate class of collection we'd already instantiated??", ie);
416         }
417         catch (IllegalAccessException iae) {
418             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         final int firstDot = path.indexOf('.');
429         if (firstDot < 0) {
430             return new String[] { path, null };
431         }
432         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         if (o instanceof Collection) {
442             c.addAll((Collection) o);
443         } else {
444             c.add(o);
445         }
446     }
447 
448     /**
449      * Gets the persistenceStructureService attribute. 
450      * @return Returns the persistenceStructureService.
451      */
452     public PersistenceStructureService getPersistenceStructureService() {
453         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         this.persistenceStructureService = persistenceStructureService;
462     }
463     
464     /**
465      * Inner helper class which will track which routing attributes have been used
466      */
467     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         public RoutingAttributeTracker(List<RoutingAttribute> routingAttributes) {
478             this.routingAttributes = routingAttributes;
479             checkPoints = new Stack<Integer>();
480         }
481         
482         /**
483          * @return the routing attribute hopefully associated with the current qualifier
484          */
485         public RoutingAttribute getCurrentRoutingAttribute() {
486             return routingAttributes.get(currentRoutingAttributeIndex);
487         }
488         
489         /**
490          * Moves this routing attribute tracker to its next routing attribute
491          */
492         public void moveToNext() {
493             currentRoutingAttributeIndex += 1;
494         }
495         
496         /**
497          * Check points at the current routing attribute, so that this position is saved
498          */
499         public void checkPoint() {
500             checkPoints.push(new Integer(currentRoutingAttributeIndex));
501         }
502         
503         /**
504          * Returns to the point of the last check point
505          */
506         public void backUpToCheckPoint() {
507             currentRoutingAttributeIndex = checkPoints.pop().intValue();
508         }
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             currentRoutingAttributeIndex = 0;
516             checkPoints.clear();
517         }
518     }
519 
520     protected BusinessObjectMetaDataService getBusinessObjectMetaDataService() {
521         if ( businessObjectMetaDataService == null ) {
522             businessObjectMetaDataService = KNSServiceLocator.getBusinessObjectMetaDataService();
523         }
524         return businessObjectMetaDataService;
525     }
526 }