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}