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}