001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kns.workflow.service.impl;
017
018import org.joda.time.DateTime;
019import org.kuali.rice.core.api.util.type.KualiDecimal;
020import org.kuali.rice.kew.api.document.attribute.DocumentAttribute;
021import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDateTime;
022import org.kuali.rice.kew.api.document.attribute.DocumentAttributeDecimal;
023import org.kuali.rice.kew.api.document.attribute.DocumentAttributeFactory;
024import org.kuali.rice.kew.api.document.attribute.DocumentAttributeInteger;
025import org.kuali.rice.kew.api.document.attribute.DocumentAttributeString;
026import org.kuali.rice.kew.api.KewApiConstants;
027import org.kuali.rice.kns.service.BusinessObjectMetaDataService;
028import org.kuali.rice.kns.service.KNSServiceLocator;
029import org.kuali.rice.kns.workflow.attribute.DataDictionarySearchableAttribute;
030import org.kuali.rice.krad.bo.BusinessObject;
031import org.kuali.rice.krad.bo.PersistableBusinessObject;
032import org.kuali.rice.krad.datadictionary.DocumentCollectionPath;
033import org.kuali.rice.krad.datadictionary.DocumentValuePathGroup;
034import org.kuali.rice.krad.datadictionary.RoutingAttribute;
035import org.kuali.rice.krad.datadictionary.RoutingTypeDefinition;
036import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
037import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
038import org.kuali.rice.krad.document.Document;
039import org.kuali.rice.krad.service.PersistenceStructureService;
040import org.kuali.rice.krad.util.DataTypeUtil;
041import org.kuali.rice.krad.util.ObjectUtils;
042import org.kuali.rice.kns.service.WorkflowAttributePropertyResolutionService;
043
044import java.math.BigDecimal;
045import java.math.BigInteger;
046import java.util.ArrayList;
047import java.util.Collection;
048import java.util.HashMap;
049import java.util.HashSet;
050import java.util.List;
051import java.util.Map;
052import java.util.Set;
053import java.util.Stack;
054
055/**
056 * The default implementation of the WorkflowAttributePropertyResolutionServiceImpl
057 *
058 * @author Kuali Rice Team (rice.collab@kuali.org)
059 */
060public class WorkflowAttributePropertyResolutionServiceImpl implements WorkflowAttributePropertyResolutionService {
061    
062    private PersistenceStructureService persistenceStructureService;
063    private BusinessObjectMetaDataService businessObjectMetaDataService;
064
065    /**
066     * Using the proper RoutingTypeDefinition for the current routing node of the document, aardvarks out the proper routing type qualifiers
067     */
068    public List<Map<String, String>> resolveRoutingTypeQualifiers(Document document, RoutingTypeDefinition routingTypeDefinition) {
069        List<Map<String, String>> qualifiers = new ArrayList<Map<String, String>>();
070        
071        if (routingTypeDefinition != null) {
072            document.populateDocumentForRouting();
073            RoutingAttributeTracker routingAttributeTracker = new RoutingAttributeTracker(routingTypeDefinition.getRoutingAttributes());
074            for (DocumentValuePathGroup documentValuePathGroup : routingTypeDefinition.getDocumentValuePathGroups()) {
075                qualifiers.addAll(resolveDocumentValuePath(document, documentValuePathGroup, routingAttributeTracker));
076                routingAttributeTracker.reset();
077            }
078        }
079        return qualifiers;
080    }
081    
082    /**
083     * Resolves all of the values in the given DocumentValuePathGroup from the given BusinessObject
084     * @param businessObject the business object which is the source of values
085     * @param group the DocumentValuePathGroup which tells us which values we want
086     * @return a List of Map<String, String>s
087     */
088    protected List<Map<String, String>> resolveDocumentValuePath(BusinessObject businessObject, DocumentValuePathGroup group, RoutingAttributeTracker routingAttributeTracker) {
089        List<Map<String, String>> qualifiers;
090        Map<String, String> qualifier = new HashMap<String, String>();
091        if (group.getDocumentValues() == null && group.getDocumentCollectionPath() == null) {
092            throw new IllegalStateException("A document value path group must have the documentValues property set, the documentCollectionPath property set, or both.");
093        }
094        if (group.getDocumentValues() != null) {
095            addPathValuesToQualifier(businessObject, group.getDocumentValues(), routingAttributeTracker, qualifier);
096        }
097        if (group.getDocumentCollectionPath() != null) {
098            qualifiers = resolveDocumentCollectionPath(businessObject, group.getDocumentCollectionPath(), routingAttributeTracker);
099            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}