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.data.provider.impl; 017 018import java.beans.PropertyDescriptor; 019import java.beans.PropertyEditor; 020import java.lang.reflect.Field; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.commons.lang.ArrayUtils; 030import org.apache.commons.lang.StringUtils; 031import org.kuali.rice.core.api.criteria.QueryByCriteria; 032import org.kuali.rice.krad.data.CompoundKey; 033import org.kuali.rice.krad.data.DataObjectService; 034import org.kuali.rice.krad.data.DataObjectWrapper; 035import org.kuali.rice.krad.data.MaterializeOption; 036import org.kuali.rice.krad.data.metadata.DataObjectAttribute; 037import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship; 038import org.kuali.rice.krad.data.metadata.DataObjectCollection; 039import org.kuali.rice.krad.data.metadata.DataObjectMetadata; 040import org.kuali.rice.krad.data.metadata.DataObjectRelationship; 041import org.kuali.rice.krad.data.metadata.MetadataChild; 042import org.kuali.rice.krad.data.util.ReferenceLinker; 043import org.springframework.beans.BeanWrapper; 044import org.springframework.beans.BeansException; 045import org.springframework.beans.InvalidPropertyException; 046import org.springframework.beans.NullValueInNestedPathException; 047import org.springframework.beans.PropertyAccessorFactory; 048import org.springframework.beans.PropertyAccessorUtils; 049import org.springframework.beans.PropertyValue; 050import org.springframework.beans.PropertyValues; 051import org.springframework.beans.TypeMismatchException; 052import org.springframework.core.CollectionFactory; 053import org.springframework.core.MethodParameter; 054import org.springframework.core.convert.ConversionService; 055import org.springframework.core.convert.TypeDescriptor; 056 057import com.google.common.collect.Sets; 058 059/** 060 * The base implementation of {@link DataObjectWrapper}. 061 * 062 * @param <T> the type of the data object to wrap. 063 * 064 * @author Kuali Rice Team (rice.collab@kuali.org) 065 */ 066public abstract class DataObjectWrapperBase<T> implements DataObjectWrapper<T> { 067 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DataObjectWrapperBase.class); 068 069 private final T dataObject; 070 private final DataObjectMetadata metadata; 071 private final BeanWrapper wrapper; 072 private final DataObjectService dataObjectService; 073 private final ReferenceLinker referenceLinker; 074 075 /** 076 * Creates a data object wrapper. 077 * 078 * @param dataObject the data object to wrap. 079 * @param metadata the metadata of the data object. 080 * @param dataObjectService the data object service to use. 081 * @param referenceLinker the reference linker implementation. 082 */ 083 protected DataObjectWrapperBase(T dataObject, DataObjectMetadata metadata, DataObjectService dataObjectService, 084 ReferenceLinker referenceLinker) { 085 this.dataObject = dataObject; 086 this.metadata = metadata; 087 this.dataObjectService = dataObjectService; 088 this.referenceLinker = referenceLinker; 089 this.wrapper = PropertyAccessorFactory.forBeanPropertyAccess(dataObject); 090 // note that we do *not* want to set auto grow to be true here since we are using this primarily for 091 // access to the data, we will expose getPropertyValueNullSafe instead because it prevents a a call to 092 // getPropertyValue from modifying the internal state of the object by growing intermediate nested paths 093 } 094 095 /** 096 * {@inheritDoc} 097 */ 098 @Override 099 public DataObjectMetadata getMetadata() { 100 return metadata; 101 } 102 103 104 /** 105 * {@inheritDoc} 106 */ 107 @Override 108 public T getWrappedInstance() { 109 return dataObject; 110 } 111 112 /** 113 * {@inheritDoc} 114 */ 115 @Override 116 public Object getPropertyValueNullSafe(String propertyName) throws BeansException { 117 try { 118 return getPropertyValue(propertyName); 119 } catch (NullValueInNestedPathException e) { 120 return null; 121 } 122 } 123 124 /** 125 * {@inheritDoc} 126 */ 127 @SuppressWarnings("unchecked") 128 @Override 129 public Class<T> getWrappedClass() { 130 return (Class<T>) wrapper.getWrappedClass(); 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override 137 public PropertyDescriptor[] getPropertyDescriptors() { 138 return wrapper.getPropertyDescriptors(); 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override 145 public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException { 146 return wrapper.getPropertyDescriptor(propertyName); 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 @Override 153 public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { 154 wrapper.setAutoGrowNestedPaths(autoGrowNestedPaths); 155 } 156 157 /** 158 * {@inheritDoc} 159 */ 160 @Override 161 public boolean isAutoGrowNestedPaths() { 162 return wrapper.isAutoGrowNestedPaths(); 163 } 164 165 /** 166 * {@inheritDoc} 167 */ 168 @Override 169 public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { 170 wrapper.setAutoGrowCollectionLimit(autoGrowCollectionLimit); 171 } 172 173 /** 174 * {@inheritDoc} 175 */ 176 @Override 177 public int getAutoGrowCollectionLimit() { 178 return wrapper.getAutoGrowCollectionLimit(); 179 } 180 181 /** 182 * {@inheritDoc} 183 */ 184 @Override 185 public void setConversionService(ConversionService conversionService) { 186 wrapper.setConversionService(conversionService); 187 } 188 189 /** 190 * {@inheritDoc} 191 */ 192 @Override 193 public ConversionService getConversionService() { 194 return wrapper.getConversionService(); 195 } 196 197 /** 198 * {@inheritDoc} 199 */ 200 @Override 201 public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { 202 wrapper.setExtractOldValueForEditor(extractOldValueForEditor); 203 } 204 205 /** 206 * {@inheritDoc} 207 */ 208 @Override 209 public boolean isExtractOldValueForEditor() { 210 return wrapper.isExtractOldValueForEditor(); 211 } 212 213 /** 214 * {@inheritDoc} 215 */ 216 @Override 217 public boolean isReadableProperty(String propertyName) { 218 return wrapper.isReadableProperty(propertyName); 219 } 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 public boolean isWritableProperty(String propertyName) { 226 return wrapper.isWritableProperty(propertyName); 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 @Override 233 public Class<?> getPropertyType(String propertyName) throws BeansException { 234 return wrapper.getPropertyType(propertyName); 235 } 236 237 /** 238 * {@inheritDoc} 239 */ 240 @Override 241 public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { 242 return wrapper.getPropertyTypeDescriptor(propertyName); 243 } 244 245 /** 246 * {@inheritDoc} 247 */ 248 @Override 249 public Object getPropertyValue(String propertyName) throws BeansException { 250 return wrapper.getPropertyValue(propertyName); 251 } 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override 257 public void setPropertyValue(String propertyName, Object value) throws BeansException { 258 wrapper.setPropertyValue(propertyName, value); 259 } 260 261 /** 262 * {@inheritDoc} 263 */ 264 @Override 265 public void setPropertyValue(PropertyValue pv) throws BeansException { 266 wrapper.setPropertyValue(pv); 267 } 268 269 /** 270 * {@inheritDoc} 271 */ 272 @Override 273 public void setPropertyValues(Map<?, ?> map) throws BeansException { 274 wrapper.setPropertyValues(map); 275 } 276 277 /** 278 * {@inheritDoc} 279 */ 280 @Override 281 public void setPropertyValues(PropertyValues pvs) throws BeansException { 282 wrapper.setPropertyValues(pvs); 283 } 284 285 /** 286 * {@inheritDoc} 287 */ 288 @Override 289 public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) throws BeansException { 290 wrapper.setPropertyValues(pvs, ignoreUnknown); 291 } 292 293 /** 294 * {@inheritDoc} 295 */ 296 @Override 297 public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, 298 boolean ignoreInvalid) throws BeansException { 299 wrapper.setPropertyValues(pvs, ignoreUnknown, ignoreInvalid); 300 } 301 302 /** 303 * {@inheritDoc} 304 */ 305 @Override 306 public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) { 307 wrapper.registerCustomEditor(requiredType, propertyEditor); 308 } 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override 314 public void registerCustomEditor(Class<?> requiredType, String propertyPath, PropertyEditor propertyEditor) { 315 wrapper.registerCustomEditor(requiredType, propertyPath, propertyEditor); 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 @Override 322 public PropertyEditor findCustomEditor(Class<?> requiredType, String propertyPath) { 323 return wrapper.findCustomEditor(requiredType, propertyPath); 324 } 325 326 /** 327 * {@inheritDoc} 328 */ 329 @Override 330 public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType) throws TypeMismatchException { 331 return wrapper.convertIfNecessary(value, requiredType); 332 } 333 334 /** 335 * {@inheritDoc} 336 */ 337 @Override 338 public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType, 339 MethodParameter methodParam) throws TypeMismatchException { 340 return wrapper.convertIfNecessary(value, requiredType, methodParam); 341 } 342 343 /** 344 * {@inheritDoc} 345 */ 346 @Override 347 public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType, Field field) throws TypeMismatchException { 348 return wrapper.convertIfNecessary(value, requiredType, field); 349 } 350 351 /** 352 * {@inheritDoc} 353 */ 354 @Override 355 public Map<String, Object> getPrimaryKeyValues() { 356 Map<String, Object> primaryKeyValues = new HashMap<String, Object>(); 357 if (metadata != null) { 358 List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames(); 359 if (primaryKeyAttributeNames != null) { 360 for (String primaryKeyAttributeName : primaryKeyAttributeNames) { 361 primaryKeyValues.put(primaryKeyAttributeName, getPropertyValue(primaryKeyAttributeName)); 362 } 363 } 364 } else { 365 LOG.warn("Attempt to retrieve PK fields on object with no metadata: " + dataObject.getClass().getName()); 366 } 367 return primaryKeyValues; 368 } 369 370 /** 371 * {@inheritDoc} 372 */ 373 @Override 374 public Object getPrimaryKeyValue() { 375 if (!areAllPrimaryKeyAttributesPopulated()) { 376 return null; 377 } 378 379 Map<String, Object> primaryKeyValues = getPrimaryKeyValues(); 380 381 if (primaryKeyValues.size() == 1) { 382 return primaryKeyValues.values().iterator().next(); 383 } else { 384 return new CompoundKey(primaryKeyValues); 385 } 386 } 387 388 /** 389 * {@inheritDoc} 390 */ 391 @Override 392 public boolean areAllPrimaryKeyAttributesPopulated() { 393 if (metadata != null) { 394 List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames(); 395 if (primaryKeyAttributeNames != null) { 396 for (String primaryKeyAttributeName : primaryKeyAttributeNames) { 397 Object propValue = getPropertyValue(primaryKeyAttributeName); 398 if (propValue == null || (propValue instanceof String && StringUtils.isBlank((String) propValue))) { 399 return false; 400 } 401 } 402 } 403 return true; 404 } else { 405 LOG.warn("Attempt to check areAllPrimaryKeyAttributesPopulated on object with no metadata: " 406 + dataObject.getClass().getName()); 407 return true; 408 } 409 } 410 411 /** 412 * {@inheritDoc} 413 */ 414 @Override 415 public boolean areAnyPrimaryKeyAttributesPopulated() { 416 if (metadata != null) { 417 List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames(); 418 if (primaryKeyAttributeNames != null) { 419 for (String primaryKeyAttributeName : primaryKeyAttributeNames) { 420 Object propValue = getPropertyValue(primaryKeyAttributeName); 421 if (propValue instanceof String && StringUtils.isNotBlank((String) propValue)) { 422 return true; 423 } else if (propValue != null) { 424 return true; 425 } 426 } 427 } 428 return false; 429 } else { 430 LOG.warn("Attempt to check areAnyPrimaryKeyAttributesPopulated on object with no metadata: " 431 + dataObject.getClass().getName()); 432 return true; 433 } 434 } 435 436 /** 437 * {@inheritDoc} 438 */ 439 @Override 440 public List<String> getUnpopulatedPrimaryKeyAttributeNames() { 441 List<String> emptyKeys = new ArrayList<String>(); 442 if (metadata != null) { 443 List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames(); 444 if (primaryKeyAttributeNames != null) { 445 for (String primaryKeyAttributeName : primaryKeyAttributeNames) { 446 Object propValue = getPropertyValue(primaryKeyAttributeName); 447 if (propValue == null || (propValue instanceof String && StringUtils.isBlank((String) propValue))) { 448 emptyKeys.add(primaryKeyAttributeName); 449 } 450 } 451 } 452 } else { 453 LOG.warn("Attempt to check getUnpopulatedPrimaryKeyAttributeNames on object with no metadata: " 454 + dataObject.getClass().getName()); 455 } 456 return emptyKeys; 457 } 458 459 /** 460 * {@inheritDoc} 461 */ 462 @Override 463 public boolean equalsByPrimaryKey(T object) { 464 if (object == null) { 465 return false; 466 } 467 DataObjectWrapper<T> wrap = dataObjectService.wrap(object); 468 if (!getWrappedClass().isAssignableFrom(wrap.getWrappedClass())) { 469 throw new IllegalArgumentException("The type of the given data object does not match the type of this " + 470 "data object. Given: " + wrap.getWrappedClass() + ", but expected: " + getWrappedClass()); 471 } 472 // since they are the same type, we know they must have the same number of primary keys, 473 Map<String, Object> localPks = getPrimaryKeyValues(); 474 Map<String, Object> givenPks = wrap.getPrimaryKeyValues(); 475 for (String localPk : localPks.keySet()) { 476 Object localPkValue = localPks.get(localPk); 477 if (localPkValue == null || !localPkValue.equals(givenPks.get(localPk))) { 478 return false; 479 } 480 } 481 return true; 482 } 483 484 /** 485 * {@inheritDoc} 486 */ 487 @Override 488 public Object getForeignKeyValue(String relationshipName) { 489 Object foreignKeyAttributeValue = getForeignKeyAttributeValue(relationshipName); 490 if (foreignKeyAttributeValue != null) { 491 return foreignKeyAttributeValue; 492 } 493 // if there are no attribute relationships, or the attribute relationships are not fully populated, fall 494 // back to the actual relationship object 495 Object relationshipObject = getPropertyValue(relationshipName); 496 if (relationshipObject == null) { 497 return null; 498 } 499 return dataObjectService.wrap(relationshipObject).getPrimaryKeyValue(); 500 } 501 502 /** 503 * {@inheritDoc} 504 */ 505 @Override 506 public Object getForeignKeyAttributeValue(String relationshipName) { 507 Map<String, Object> attributeMap = getForeignKeyAttributeMap(relationshipName); 508 if (attributeMap == null) { 509 return null; 510 } 511 return asSingleKey(attributeMap); 512 } 513 514 /** 515 * Gets the map of child attribute names to the parent attribute values. 516 * 517 * @param relationshipName the name of the relationship for which to get the map. 518 * @return the map of child attribute names to the parent attribute values. 519 */ 520 public Map<String, Object> getForeignKeyAttributeMap(String relationshipName) { 521 MetadataChild relationship = findAndValidateRelationship(relationshipName); 522 List<DataObjectAttributeRelationship> attributeRelationships = relationship.getAttributeRelationships(); 523 524 if (!attributeRelationships.isEmpty()) { 525 Map<String, Object> attributeMap = new LinkedHashMap<String, Object>(); 526 527 for (DataObjectAttributeRelationship attributeRelationship : attributeRelationships) { 528 // obtain the property value on the current parent object 529 String parentAttributeName = attributeRelationship.getParentAttributeName(); 530 Object parentAttributeValue = null; 531 532 try { 533 parentAttributeValue = getPropertyValue(parentAttributeName); 534 } catch (BeansException be) { 535 // exception thrown may be a db property which may not be defined on class (JPA foreign keys) 536 // use null value for parentAttributeValue 537 } 538 539 // not all of our relationships are populated, so we cannot obtain a valid foreign key 540 if (parentAttributeValue == null) { 541 return null; 542 } 543 544 // store the mapping with the child attribute name to fetch on the referenced child object 545 String childAttributeName = attributeRelationship.getChildAttributeName(); 546 if (childAttributeName != null) { 547 attributeMap.put(childAttributeName, parentAttributeValue); 548 } 549 } 550 551 return attributeMap; 552 } 553 554 return null; 555 } 556 557 /** 558 * Gets a single key from a map of keys, either by grabbing the first value from a map size of 1 or by creating a 559 * {@link CompoundKey}. 560 * 561 * @param keyValues the map of keys to process. 562 * @return a single key from a set map of keys. 563 */ 564 private Object asSingleKey(Map<String, Object> keyValues) { 565 if (keyValues.size() == 1) { 566 return keyValues.values().iterator().next(); 567 } 568 569 return new CompoundKey(keyValues); 570 } 571 572 /** 573 * {@inheritDoc} 574 */ 575 @Override 576 public Class<?> getPropertyTypeNullSafe(Class<?> objectType, String propertyName) { 577 DataObjectMetadata objectMetadata = dataObjectService.getMetadataRepository().getMetadata(objectType); 578 return getPropertyTypeChild(objectMetadata,propertyName); 579 } 580 581 /** 582 * Gets the property type for a property name. 583 * 584 * @param objectMetadata the metadata object. 585 * @param propertyName the name of the property. 586 * @return the property type for a property name. 587 */ 588 private Class<?> getPropertyTypeChild(DataObjectMetadata objectMetadata, String propertyName){ 589 if(PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)){ 590 String attributePrefix = StringUtils.substringBefore(propertyName,"."); 591 String attributeName = StringUtils.substringAfter(propertyName,"."); 592 593 if(StringUtils.isNotBlank(attributePrefix) && StringUtils.isNotBlank(attributeName) && 594 objectMetadata!= null){ 595 Class<?> propertyType = traverseRelationship(objectMetadata,attributePrefix,attributeName); 596 if(propertyType != null){ 597 return propertyType; 598 } 599 } 600 } 601 return getPropertyType(propertyName); 602 } 603 604 /** 605 * Gets the property type for a property name in a relationship. 606 * 607 * @param objectMetadata the metadata object. 608 * @param attributePrefix the prefix of the property that indicated it was in a relationship. 609 * @param attributeName the name of the property. 610 * @return the property type for a property name. 611 */ 612 private Class<?> traverseRelationship(DataObjectMetadata objectMetadata,String attributePrefix, 613 String attributeName){ 614 DataObjectRelationship rd = objectMetadata.getRelationship(attributePrefix); 615 if(rd != null){ 616 DataObjectMetadata relatedObjectMetadata = 617 dataObjectService.getMetadataRepository().getMetadata(rd.getRelatedType()); 618 if(relatedObjectMetadata != null){ 619 if(PropertyAccessorUtils.isNestedOrIndexedProperty(attributeName)){ 620 return getPropertyTypeChild(relatedObjectMetadata,attributeName); 621 } else{ 622 if(relatedObjectMetadata.getAttribute(attributeName) == null && 623 relatedObjectMetadata.getRelationship(attributeName)!=null){ 624 DataObjectRelationship relationship = relatedObjectMetadata.getRelationship(attributeName); 625 return relationship.getRelatedType(); 626 } 627 return relatedObjectMetadata.getAttribute(attributeName).getDataType().getType(); 628 } 629 } 630 } 631 return null; 632 } 633 634 /** 635 * {@inheritDoc} 636 */ 637 @Override 638 public void linkChanges(Set<String> changedPropertyPaths) { 639 referenceLinker.linkChanges(getWrappedInstance(), changedPropertyPaths); 640 } 641 642 /** 643 * {@inheritDoc} 644 */ 645 @Override 646 public void linkForeignKeys(boolean onlyLinkReadOnly) { 647 linkForeignKeysInternalWrapped(this, onlyLinkReadOnly, Sets.newHashSet()); 648 } 649 650 /** 651 * Links all foreign keys on the data object. 652 * 653 * @param object the object to link. 654 * @param onlyLinkReadOnly whether to only link read-only objects. 655 * @param linked the set of currently linked objects, used as a base case to exit out of recursion. 656 */ 657 protected void linkForeignKeysInternal(Object object, boolean onlyLinkReadOnly, Set<Object> linked) { 658 if (object == null || linked.contains(object) || !dataObjectService.supports(object.getClass())) { 659 return; 660 } 661 linked.add(object); 662 DataObjectWrapper<?> wrapped = dataObjectService.wrap(object); 663 linkForeignKeysInternalWrapped(wrapped, onlyLinkReadOnly, linked); 664 } 665 666 /** 667 * Links all foreign keys on the wrapped data object. 668 * 669 * @param wrapped the wrapped object to link. 670 * @param onlyLinkReadOnly whether to only link read-only objects. 671 * @param linked the set of currently linked objects, used as a base case to exit out of recursion. 672 */ 673 protected void linkForeignKeysInternalWrapped(DataObjectWrapper<?> wrapped, boolean onlyLinkReadOnly, Set<Object> linked) { 674 List<DataObjectRelationship> relationships = wrapped.getMetadata().getRelationships(); 675 for (DataObjectRelationship relationship : relationships) { 676 String relationshipName = relationship.getName(); 677 Object relationshipValue = wrapped.getPropertyValue(relationshipName); 678 679 // let's get the current value and recurse down if it's a relationship that is cascaded on save 680 if (relationship.isSavedWithParent()) { 681 682 linkForeignKeysInternal(relationshipValue, onlyLinkReadOnly, linked); 683 } 684 685 // next, if we have related attributes, we need to link our keys 686 linkForeignKeysInternal(wrapped, relationship, relationshipValue, onlyLinkReadOnly); 687 } 688 List<DataObjectCollection> collections = wrapped.getMetadata().getCollections(); 689 for (DataObjectCollection collection : collections) { 690 String relationshipName = collection.getName(); 691 692 // let's get the current value and recurse down for each element if it's a collection that is cascaded on save 693 if (collection.isSavedWithParent()) { 694 Collection<?> collectionValue = (Collection<?>)wrapped.getPropertyValue(relationshipName); 695 if (collectionValue != null) { 696 for (Object object : collectionValue) { 697 linkForeignKeysInternal(object, onlyLinkReadOnly, linked); 698 } 699 } 700 } 701 } 702 703 } 704 705 /** 706 * {@inheritDoc} 707 */ 708 @Override 709 public void fetchRelationship(String relationshipName) { 710 fetchRelationship(relationshipName, true, true); 711 } 712 713 /** 714 * {@inheritDoc} 715 */ 716 @Override 717 public void fetchRelationship(String relationshipName, boolean useForeignKeyAttribute, boolean nullifyDanglingRelationship) { 718 fetchRelationship(findAndValidateRelationship(relationshipName), useForeignKeyAttribute, 719 nullifyDanglingRelationship); 720 } 721 /** 722 * Fetches and populates the value for the relationship with the given name on the wrapped object. 723 * 724 * @param relationship the relationship on the wrapped data object to refresh 725 * @param useForeignKeyAttribute whether to use the foreign key attribute to fetch the relationship 726 * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found 727 */ 728 protected void fetchRelationship(MetadataChild relationship, boolean useForeignKeyAttribute, boolean nullifyDanglingRelationship) { 729 Class<?> relatedType = relationship.getRelatedType(); 730 if (!dataObjectService.supports(relatedType)) { 731 LOG.warn("Encountered a related type that is not supported by DataObjectService, fetch " 732 + "relationship will do nothing: " + relatedType); 733 return; 734 } 735 // if we have at least one attribute relationships here, then we are set to proceed 736 if (useForeignKeyAttribute) { 737 fetchRelationshipUsingAttributes(relationship, nullifyDanglingRelationship); 738 } else { 739 fetchRelationshipUsingIdentity(relationship, nullifyDanglingRelationship); 740 } 741 } 742 743 /** 744 * Fetches the relationship using the foreign key attributes. 745 * 746 * @param relationship the relationship on the wrapped data object to refresh 747 * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found 748 */ 749 protected void fetchRelationshipUsingAttributes(MetadataChild relationship, boolean nullifyDanglingRelationship) { 750 Class<?> relatedType = relationship.getRelatedType(); 751 if (relationship.getAttributeRelationships().isEmpty()) { 752 LOG.warn("Attempted to fetch a relationship using a foreign key attribute " 753 + "when one does not exist: " 754 + relationship.getName()); 755 } else { 756 Object fetchedValue = null; 757 if (relationship instanceof DataObjectRelationship) { 758 Object foreignKey = getForeignKeyAttributeValue(relationship.getName()); 759 if (foreignKey != null) { 760 fetchedValue = dataObjectService.find(relatedType, foreignKey); 761 } 762 } else if (relationship instanceof DataObjectCollection) { 763 Map<String, Object> foreignKeyAttributeMap = getForeignKeyAttributeMap(relationship.getName()); 764 fetchedValue = dataObjectService.findMatching(relatedType, 765 QueryByCriteria.Builder.andAttributes(foreignKeyAttributeMap).build()).getResults(); 766 } 767 if (fetchedValue != null || nullifyDanglingRelationship) { 768 setPropertyValue(relationship.getName(), fetchedValue); 769 } 770 } 771 } 772 773 /** 774 * Fetches the relationship using the primary key attributes. 775 * 776 * @param relationship the relationship on the wrapped data object to refresh 777 * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found 778 */ 779 protected void fetchRelationshipUsingIdentity(MetadataChild relationship, boolean nullifyDanglingRelationship) { 780 Object propertyValue = getPropertyValue(relationship.getName()); 781 if (propertyValue != null) { 782 if (!dataObjectService.supports(propertyValue.getClass())) { 783 throw new IllegalArgumentException("Attempting to fetch an invalid relationship, must be a" 784 + "DataObjectRelationship when fetching without a foreign key"); 785 } 786 DataObjectWrapper<?> wrappedRelationship = dataObjectService.wrap(propertyValue); 787 Map<String, Object> primaryKeyValues = wrappedRelationship.getPrimaryKeyValues(); 788 Object newPropertyValue = dataObjectService.find(wrappedRelationship.getWrappedClass(), 789 new CompoundKey(primaryKeyValues)); 790 if (newPropertyValue != null || nullifyDanglingRelationship) { 791 propertyValue = newPropertyValue; 792 setPropertyValue(relationship.getName(), propertyValue); 793 } 794 } 795 // now copy pk values back to the foreign key, because we are being explicity asked to fetch the relationship 796 // using the identity and not the FK, we don't care about whether the FK field is read only or not so pass 797 // "false" for onlyLinkReadOnly argument to linkForeignKeys 798 linkForeignKeysInternal(this, relationship, propertyValue, false); 799 populateInverseRelationship(relationship, propertyValue); 800 } 801 802 /** 803 * {@inheritDoc} 804 */ 805 @Override 806 public void linkForeignKeys(String relationshipName, boolean onlyLinkReadOnly) { 807 MetadataChild relationship = findAndValidateRelationship(relationshipName); 808 Object propertyValue = getPropertyValue(relationshipName); 809 linkForeignKeysInternal(this, relationship, propertyValue, onlyLinkReadOnly); 810 } 811 812 /** 813 * {@inheritDoc} 814 */ 815 @Override 816 public void materializeReferencedObjects(MaterializeOption... options) { 817 materializeReferencedObjectsToDepth(1, options); 818 } 819 820 /** 821 * {@inheritDoc} 822 */ 823 @Override 824 public void materializeReferencedObjectsToDepth(int maxDepth, MaterializeOption... options) { 825 boolean setInvalidRefsToNull = ArrayUtils.contains(options, MaterializeOption.NULL_INVALID_REFS); 826 Collection<MetadataChild> matchingChildRelationships = getChildrenMatchingOptions(options); 827 828 for (MetadataChild child : matchingChildRelationships) { 829 fetchRelationship(child, true, setInvalidRefsToNull); 830 // No need to look at children if we will not be recursing 831 if (maxDepth > 1) { 832 Object childValue = getPropertyValue(child.getName()); 833 if (childValue != null) { 834 if (!(childValue instanceof Collection)) { 835 DataObjectWrapper<Object> childWrapper = dataObjectService.wrap(childValue); 836 // we can not proceed if the child object has no metadata 837 if (childWrapper.getMetadata() != null) { 838 childWrapper.materializeReferencedObjectsToDepth(maxDepth - 1, options); 839 } 840 } else { // Collection objects 841 // we must retrieve the list and materialize each one of them 842 for (Object collectionElement : (Collection<?>) childValue) { 843 DataObjectWrapper<Object> childWrapper = dataObjectService.wrap(collectionElement); 844 // we can not proceed if the child object has no metadata 845 if (childWrapper.getMetadata() != null) { 846 childWrapper.materializeReferencedObjectsToDepth(maxDepth - 1, options); 847 } 848 } 849 } 850 } 851 } 852 } 853 } 854 855 /** 856 * This method retrieves the {@link MetadataChild} objects ({@link DataObjectRelationship} or 857 * {@link DataObjectCollection}) which match the given {@link MaterializeOption} options. 858 * 859 * It utilizes the known information in the {@link DataObjectMetadata} and compares the flags there with the options 860 * given. 861 * 862 * If no options are given, this method will return all {@link DataObjectRelationship} and 863 * {@link DataObjectCollection} objects which are not updatable (not {@link MetadataChild#isSavedWithParent()}) and 864 * are lazily loaded ({@link MetadataChild#isLoadedDynamicallyUponUse()}). 865 * 866 * @param options 867 * An optional list of {@link MaterializeOption} objects to alter the default behavior. 868 * @return A non-null collection of {@link MetadataChild} objects matching the given parameters. 869 */ 870 public Collection<MetadataChild> getChildrenMatchingOptions(MaterializeOption... options) { 871 Collection<MetadataChild> matchingChildren = new ArrayList<>(); 872 if (metadata == null) { 873 return matchingChildren; 874 } 875 boolean materializeUpdatable = ArrayUtils.contains(options, MaterializeOption.UPDATE_UPDATABLE_REFS); 876 boolean rematerializeEagerRefs = ArrayUtils.contains(options, MaterializeOption.INCLUDE_EAGER_REFS); 877 // we include relationships IF it's explicitly specified *OR* neither was specified 878 boolean includeRelationships = ArrayUtils.contains(options, MaterializeOption.REFERENCES) 879 || !ArrayUtils.contains(options, MaterializeOption.COLLECTIONS); 880 boolean includeCollections = ArrayUtils.contains(options, MaterializeOption.COLLECTIONS) 881 || !ArrayUtils.contains(options, MaterializeOption.REFERENCES); 882 883 if (includeRelationships) { 884 for (DataObjectRelationship rel : metadata.getRelationships()) { 885 // avoiding lots of nesting and combined logic by filtering out each invalid combination 886 887 // updatable reference 888 if (rel.isSavedWithParent() && !materializeUpdatable) { 889 continue; 890 } 891 892 // eagerly loaded reference 893 if (rel.isLoadedAtParentLoadTime() && !rematerializeEagerRefs) { 894 continue; 895 } 896 897 matchingChildren.add(rel); 898 } 899 } 900 901 if (includeCollections) { 902 for (DataObjectCollection rel : metadata.getCollections()) { 903 // avoiding lots of nesting and combined logic by filtering out each invalid combination 904 905 // updatable reference 906 if (rel.isSavedWithParent() && !materializeUpdatable) { 907 continue; 908 } 909 910 // eagerly loaded reference 911 if (rel.isLoadedAtParentLoadTime() && !rematerializeEagerRefs) { 912 continue; 913 } 914 915 matchingChildren.add(rel); 916 } 917 } 918 919 return matchingChildren; 920 } 921 922 /** 923 * Links foreign keys non-recursively using the relationship with the given name on the wrapped data object. 924 * 925 * @param wrapped 926 * the wrapped object to link. 927 * @param relationship 928 * the relationship on the wrapped data object for which to link foreign keys. 929 * @param relationshipValue 930 * the value of the relationship. 931 * @param onlyLinkReadOnly 932 * indicates whether or not only read-only foreign keys should be linked. 933 */ 934 protected void linkForeignKeysInternal(DataObjectWrapper<?> wrapped, MetadataChild relationship, 935 Object relationshipValue, boolean onlyLinkReadOnly) { 936 if (!relationship.getAttributeRelationships().isEmpty()) { 937 // this means there's a foreign key so we need to copy values back 938 DataObjectWrapper<?> wrappedRelationship = null; 939 if (relationshipValue != null) { 940 wrappedRelationship = dataObjectService.wrap(relationshipValue); 941 } 942 for (DataObjectAttributeRelationship attributeRelationship : relationship.getAttributeRelationships()) { 943 String parentAttributeName = attributeRelationship.getParentAttributeName(); 944 // if the property value is null, we need to copy null back to all parent foreign keys, 945 // otherwise we copy back the actual value 946 Object childAttributeValue = null; 947 if (wrappedRelationship != null) { 948 childAttributeValue = 949 wrappedRelationship.getPropertyValue(attributeRelationship.getChildAttributeName()); 950 } 951 if (onlyLinkReadOnly) { 952 DataObjectAttribute attribute = wrapped.getMetadata().getAttribute(parentAttributeName); 953 if (attribute.isReadOnly()) { 954 wrapped.setPropertyValue(parentAttributeName, childAttributeValue); 955 } 956 } else { 957 wrapped.setPropertyValue(parentAttributeName, childAttributeValue); 958 } 959 } 960 } 961 } 962 963 /** 964 * Populates the property on the other side of the relationship. 965 * 966 * @param relationship the relationship on the wrapped data object for which to populate the inverse relationship. 967 * @param propertyValue the value of the property. 968 */ 969 protected void populateInverseRelationship(MetadataChild relationship, Object propertyValue) { 970 if (propertyValue != null) { 971 MetadataChild inverseRelationship = relationship.getInverseRelationship(); 972 if (inverseRelationship != null) { 973 DataObjectWrapper<?> wrappedRelationship = dataObjectService.wrap(propertyValue); 974 if (inverseRelationship instanceof DataObjectCollection) { 975 DataObjectCollection collectionRelationship = (DataObjectCollection)inverseRelationship; 976 String colRelName = inverseRelationship.getName(); 977 Collection<Object> collection = 978 (Collection<Object>)wrappedRelationship.getPropertyValue(colRelName); 979 if (collection == null) { 980 // if the collection is null, let's instantiate an empty one 981 collection = 982 CollectionFactory.createCollection(wrappedRelationship.getPropertyType(colRelName), 1); 983 wrappedRelationship.setPropertyValue(colRelName, collection); 984 } 985 collection.add(getWrappedInstance()); 986 } 987 } 988 } 989 } 990 991 /** 992 * Finds and validates the relationship specified by the given name. 993 * 994 * @param relationshipName the name of the relationship to find. 995 * @return the found relationship. 996 */ 997 private MetadataChild findAndValidateRelationship(String relationshipName) { 998 if (StringUtils.isBlank(relationshipName)) { 999 throw new IllegalArgumentException("The relationshipName must not be null or blank"); 1000 } 1001 // validate the relationship exists 1002 MetadataChild relationship = getMetadata().getRelationship(relationshipName); 1003 if (relationship == null) { 1004 relationship = getMetadata().getCollection(relationshipName); 1005 if (relationship == null) { 1006 throw new IllegalArgumentException("Failed to locate a valid relationship from " + getWrappedClass() 1007 + " with the given relationship name '" + relationshipName + "'"); 1008 } 1009 } 1010 return relationship; 1011 } 1012 1013}