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.krad.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.krad.bo.BusinessObject;
030import org.kuali.rice.krad.bo.PersistableBusinessObject;
031import org.kuali.rice.krad.datadictionary.DocumentCollectionPath;
032import org.kuali.rice.krad.datadictionary.DocumentValuePathGroup;
033import org.kuali.rice.krad.datadictionary.RoutingAttribute;
034import org.kuali.rice.krad.datadictionary.RoutingTypeDefinition;
035import org.kuali.rice.krad.datadictionary.SearchingTypeDefinition;
036import org.kuali.rice.krad.datadictionary.WorkflowAttributes;
037import org.kuali.rice.krad.document.Document;
038import org.kuali.rice.krad.service.PersistenceStructureService;
039import org.kuali.rice.krad.util.DataTypeUtil;
040import org.kuali.rice.krad.util.ObjectUtils;
041import org.kuali.rice.krad.workflow.attribute.DataDictionarySearchableAttribute;
042import org.kuali.rice.krad.workflow.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 */
058public 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}