001    /**
002     * Copyright 2005-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.workflow.service.impl;
017    
018    import org.joda.time.DateTime;
019    import org.kuali.rice.core.api.util.type.KualiDecimal;
020    import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
021    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDateTime;
022    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDecimal;
023    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
024    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeInteger;
025    import org.kuali.rice.kew.api.document.attribute.DocumentAttributeString;
026    import org.kuali.rice.kew.api.KewApiConstants;
027    import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
028    import org.kuali.rice.kns.service.KNSServiceLocator;
029    import org.kuali.rice.krad.bo.BusinessObject;
030    import org.kuali.rice.krad.bo.PersistableBusinessObject;
031    import org.kuali.rice.krad.datadictionary.DocumentCollectionPath;
032    import org.kuali.rice.krad.datadictionary.DocumentValuePathGroup;
033    import org.kuali.rice.krad.datadictionary.RoutingAttribute;
034    import org.kuali.rice.krad.datadictionary.RoutingTypeDefinition;
035    import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
036    import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
037    import org.kuali.rice.krad.document.Document;
038    import org.kuali.rice.krad.service.PersistenceStructureService;
039    import org.kuali.rice.krad.util.DataTypeUtil;
040    import org.kuali.rice.krad.util.ObjectUtils;
041    import org.kuali.rice.krad.workflow.attribute.DataDictionarySearchableAttribute;
042    import org.kuali.rice.krad.workflow.service.WorkflowAttributePropertyResolutionService;
043    
044    import java.math.BigDecimal;
045    import java.math.BigInteger;
046    import java.util.ArrayList;
047    import java.util.Collection;
048    import java.util.HashMap;
049    import java.util.HashSet;
050    import java.util.List;
051    import java.util.Map;
052    import java.util.Set;
053    import java.util.Stack;
054    
055    /**
056     * The default implementation of the WorkflowAttributePropertyResolutionServiceImpl
057     */
058    public class WorkflowAttributePropertyResolutionServiceImpl implements WorkflowAttributePropertyResolutionService {
059        
060        private PersistenceStructureService persistenceStructureService;
061        private BusinessObjectMetaDataService businessObjectMetaDataService;
062    
063        /**
064         * Using the proper RoutingTypeDefinition for the current routing node of the document, aardvarks out the proper routing type qualifiers
065         */
066        public List<Map<String, String>> resolveRoutingTypeQualifiers(Document document, RoutingTypeDefinition routingTypeDefinition) {
067            List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
068            
069            if (routingTypeDefinition != null) {
070                document.populateDocumentForRouting();
071                RoutingAttributeTracker routingAttributeTracker = new RoutingAttributeTracker(routingTypeDefinition.getRoutingAttributes());
072                for (DocumentValuePathGroup documentValuePathGroup : routingTypeDefinition.getDocumentValuePathGroups()) {
073                    qualifiers.addAll(resolveDocumentValuePath(document, documentValuePathGroup, routingAttributeTracker));
074                    routingAttributeTracker.reset();
075                }
076            }
077            return qualifiers;
078        }
079        
080        /**
081         * Resolves all of the values in the given DocumentValuePathGroup from the given BusinessObject
082         * @param businessObject the business object which is the source of values
083         * @param group the DocumentValuePathGroup which tells us which values we want
084         * @return a List of Map<String, String>s
085         */
086        protected List<Map<String, String>> resolveDocumentValuePath(BusinessObject businessObject, DocumentValuePathGroup group, RoutingAttributeTracker routingAttributeTracker) {
087            List<Map<String, String>> qualifiers;
088            Map<String, String> qualifier = new HashMap<String, String>();
089            if (group.getDocumentValues() == null && group.getDocumentCollectionPath() == null) {
090                throw new IllegalStateException("A document value path group must have the documentValues property set, the documentCollectionPath property set, or both.");
091            }
092            if (group.getDocumentValues() != null) {
093                addPathValuesToQualifier(businessObject, group.getDocumentValues(), routingAttributeTracker, qualifier);
094            }
095            if (group.getDocumentCollectionPath() != null) {
096                qualifiers = resolveDocumentCollectionPath(businessObject, group.getDocumentCollectionPath(), routingAttributeTracker);
097                qualifiers = cleanCollectionQualifiers(qualifiers);
098                for (Map<String, String> collectionElementQualifier : qualifiers) {
099                    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    }