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}