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