View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.data.provider.impl;
17  
18  import com.google.common.collect.Sets;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.criteria.QueryByCriteria;
21  import org.kuali.rice.krad.data.CompoundKey;
22  import org.kuali.rice.krad.data.DataObjectService;
23  import org.kuali.rice.krad.data.DataObjectWrapper;
24  import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
25  import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
26  import org.kuali.rice.krad.data.metadata.DataObjectCollection;
27  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
28  import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
29  import org.kuali.rice.krad.data.metadata.MetadataChild;
30  import org.kuali.rice.krad.data.util.ReferenceLinker;
31  import org.springframework.beans.BeanWrapper;
32  import org.springframework.beans.BeansException;
33  import org.springframework.beans.InvalidPropertyException;
34  import org.springframework.beans.NullValueInNestedPathException;
35  import org.springframework.beans.PropertyAccessorFactory;
36  import org.springframework.beans.PropertyAccessorUtils;
37  import org.springframework.beans.PropertyValue;
38  import org.springframework.beans.PropertyValues;
39  import org.springframework.beans.TypeMismatchException;
40  import org.springframework.core.CollectionFactory;
41  import org.springframework.core.MethodParameter;
42  import org.springframework.core.convert.ConversionService;
43  import org.springframework.core.convert.TypeDescriptor;
44  
45  import java.beans.PropertyDescriptor;
46  import java.beans.PropertyEditor;
47  import java.lang.reflect.Field;
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.HashMap;
51  import java.util.LinkedHashMap;
52  import java.util.List;
53  import java.util.Map;
54  import java.util.Set;
55  
56  /**
57   * The base implementation of {@link DataObjectWrapper}.
58   *
59   * @param <T> the type of the data object to wrap.
60   *
61   * @author Kuali Rice Team (rice.collab@kuali.org)
62   */
63  public abstract class DataObjectWrapperBase<T> implements DataObjectWrapper<T> {
64  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DataObjectWrapperBase.class);
65  
66      private final T dataObject;
67      private final DataObjectMetadata metadata;
68      private final BeanWrapper wrapper;
69      private final DataObjectService dataObjectService;
70      private final ReferenceLinker referenceLinker;
71  
72      /**
73       * Creates a data object wrapper.
74       *
75       * @param dataObject the data object to wrap.
76       * @param metadata the metadata of the data object.
77       * @param dataObjectService the data object service to use.
78       * @param referenceLinker the reference linker implementation.
79       */
80      protected DataObjectWrapperBase(T dataObject, DataObjectMetadata metadata, DataObjectService dataObjectService,
81              ReferenceLinker referenceLinker) {
82          this.dataObject = dataObject;
83          this.metadata = metadata;
84          this.dataObjectService = dataObjectService;
85          this.referenceLinker = referenceLinker;
86          this.wrapper = PropertyAccessorFactory.forBeanPropertyAccess(dataObject);
87          // note that we do *not* want to set auto grow to be true here since we are using this primarily for
88          // access to the data, we will expose getPropertyValueNullSafe instead because it prevents a a call to
89          // getPropertyValue from modifying the internal state of the object by growing intermediate nested paths
90      }
91  
92      /**
93       * {@inheritDoc}
94       */
95      @Override
96      public DataObjectMetadata getMetadata() {
97          return metadata;
98      }
99  
100     /**
101      * {@inheritDoc}
102      */
103     @Override
104     public T getWrappedInstance() {
105         return dataObject;
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
112     public Object getPropertyValueNullSafe(String propertyName) throws BeansException {
113         try {
114             return getPropertyValue(propertyName);
115         } catch (NullValueInNestedPathException e) {
116             return null;
117         }
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123 	@SuppressWarnings("unchecked")
124 	@Override
125     public Class<T> getWrappedClass() {
126         return (Class<T>) wrapper.getWrappedClass();
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     @Override
133     public PropertyDescriptor[] getPropertyDescriptors() {
134         return wrapper.getPropertyDescriptors();
135     }
136 
137     /**
138      * {@inheritDoc}
139      */
140     @Override
141     public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException {
142         return wrapper.getPropertyDescriptor(propertyName);
143     }
144 
145     /**
146      * {@inheritDoc}
147      */
148     @Override
149     public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) {
150         wrapper.setAutoGrowNestedPaths(autoGrowNestedPaths);
151     }
152 
153     /**
154      * {@inheritDoc}
155      */
156     @Override
157     public boolean isAutoGrowNestedPaths() {
158         return wrapper.isAutoGrowNestedPaths();
159     }
160 
161     /**
162      * {@inheritDoc}
163      */
164     @Override
165     public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
166         wrapper.setAutoGrowCollectionLimit(autoGrowCollectionLimit);
167     }
168 
169     /**
170      * {@inheritDoc}
171      */
172     @Override
173     public int getAutoGrowCollectionLimit() {
174         return wrapper.getAutoGrowCollectionLimit();
175     }
176 
177     /**
178      * {@inheritDoc}
179      */
180     @Override
181     public void setConversionService(ConversionService conversionService) {
182         wrapper.setConversionService(conversionService);
183     }
184 
185     /**
186      * {@inheritDoc}
187      */
188     @Override
189     public ConversionService getConversionService() {
190         return wrapper.getConversionService();
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
197     public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
198         wrapper.setExtractOldValueForEditor(extractOldValueForEditor);
199     }
200 
201     /**
202      * {@inheritDoc}
203      */
204     @Override
205     public boolean isExtractOldValueForEditor() {
206         return wrapper.isExtractOldValueForEditor();
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     @Override
213     public boolean isReadableProperty(String propertyName) {
214         return wrapper.isReadableProperty(propertyName);
215     }
216 
217     /**
218      * {@inheritDoc}
219      */
220     @Override
221     public boolean isWritableProperty(String propertyName) {
222         return wrapper.isWritableProperty(propertyName);
223     }
224 
225     /**
226      * {@inheritDoc}
227      */
228     @Override
229     public Class<?> getPropertyType(String propertyName) throws BeansException {
230         return wrapper.getPropertyType(propertyName);
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
237     public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
238         return wrapper.getPropertyTypeDescriptor(propertyName);
239     }
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
245     public Object getPropertyValue(String propertyName) throws BeansException {
246         return wrapper.getPropertyValue(propertyName);
247     }
248 
249     /**
250      * {@inheritDoc}
251      */
252     @Override
253     public void setPropertyValue(String propertyName, Object value) throws BeansException {
254         wrapper.setPropertyValue(propertyName, value);
255     }
256 
257     /**
258      * {@inheritDoc}
259      */
260     @Override
261     public void setPropertyValue(PropertyValue pv) throws BeansException {
262         wrapper.setPropertyValue(pv);
263     }
264 
265     /**
266      * {@inheritDoc}
267      */
268     @Override
269     public void setPropertyValues(Map<?, ?> map) throws BeansException {
270         wrapper.setPropertyValues(map);
271     }
272 
273     /**
274      * {@inheritDoc}
275      */
276     @Override
277     public void setPropertyValues(PropertyValues pvs) throws BeansException {
278         wrapper.setPropertyValues(pvs);
279     }
280 
281     /**
282      * {@inheritDoc}
283      */
284     @Override
285     public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) throws BeansException {
286         wrapper.setPropertyValues(pvs, ignoreUnknown);
287     }
288 
289     /**
290      * {@inheritDoc}
291      */
292     @Override
293     public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown,
294             boolean ignoreInvalid) throws BeansException {
295         wrapper.setPropertyValues(pvs, ignoreUnknown, ignoreInvalid);
296     }
297 
298     /**
299      * {@inheritDoc}
300      */
301     @Override
302     public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
303         wrapper.registerCustomEditor(requiredType, propertyEditor);
304     }
305 
306     /**
307      * {@inheritDoc}
308      */
309     @Override
310     public void registerCustomEditor(Class<?> requiredType, String propertyPath, PropertyEditor propertyEditor) {
311         wrapper.registerCustomEditor(requiredType, propertyPath, propertyEditor);
312     }
313 
314     /**
315      * {@inheritDoc}
316      */
317     @Override
318     public PropertyEditor findCustomEditor(Class<?> requiredType, String propertyPath) {
319         return wrapper.findCustomEditor(requiredType, propertyPath);
320     }
321 
322     /**
323      * {@inheritDoc}
324      */
325     @Override
326 	public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType) throws TypeMismatchException {
327         return wrapper.convertIfNecessary(value, requiredType);
328     }
329 
330     /**
331      * {@inheritDoc}
332      */
333     @Override
334 	public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType,
335             MethodParameter methodParam) throws TypeMismatchException {
336         return wrapper.convertIfNecessary(value, requiredType, methodParam);
337     }
338 
339     /**
340      * {@inheritDoc}
341      */
342     @Override
343 	public <Y> Y convertIfNecessary(Object value, Class<Y> requiredType, Field field) throws TypeMismatchException {
344         return wrapper.convertIfNecessary(value, requiredType, field);
345     }
346 
347     /**
348      * {@inheritDoc}
349      */
350     @Override
351     public Map<String, Object> getPrimaryKeyValues() {
352         Map<String, Object> primaryKeyValues = new HashMap<String, Object>();
353 		if (metadata != null) {
354 			List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames();
355 			if (primaryKeyAttributeNames != null) {
356 				for (String primaryKeyAttributeName : primaryKeyAttributeNames) {
357 					primaryKeyValues.put(primaryKeyAttributeName, getPropertyValue(primaryKeyAttributeName));
358 				}
359 			}
360 		} else {
361 			LOG.warn("Attempt to retrieve PK fields on object with no metadata: " + dataObject.getClass().getName());
362         }
363         return primaryKeyValues;
364     }
365 
366     /**
367      * {@inheritDoc}
368      */
369     @Override
370     public Object getPrimaryKeyValue() {
371         if (!areAllPrimaryKeyAttributesPopulated()) {
372             return null;
373         }
374         Map<String, Object> primaryKeyValues = getPrimaryKeyValues();
375         if (getPrimaryKeyValues().size() == 1) {
376             return primaryKeyValues.values().iterator().next();
377         } else {
378             return new CompoundKey(primaryKeyValues);
379         }
380     }
381 
382     /**
383      * {@inheritDoc}
384      */
385 	@Override
386 	public boolean areAllPrimaryKeyAttributesPopulated() {
387 		if (metadata != null) {
388 			List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames();
389 			if (primaryKeyAttributeNames != null) {
390 				for (String primaryKeyAttributeName : primaryKeyAttributeNames) {
391 					Object propValue = getPropertyValue(primaryKeyAttributeName);
392 					if (propValue == null || (propValue instanceof String && StringUtils.isBlank((String) propValue))) {
393 						return false;
394 					}
395 				}
396 			}
397 			return true;
398 		} else {
399 			LOG.warn("Attempt to check areAllPrimaryKeyAttributesPopulated on object with no metadata: "
400 					+ dataObject.getClass().getName());
401 			return true;
402 		}
403 	}
404 
405     /**
406      * {@inheritDoc}
407      */
408 	@Override
409 	public boolean areAnyPrimaryKeyAttributesPopulated() {
410 		if (metadata != null) {
411 			List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames();
412 			if (primaryKeyAttributeNames != null) {
413 				for (String primaryKeyAttributeName : primaryKeyAttributeNames) {
414 					Object propValue = getPropertyValue(primaryKeyAttributeName);
415 					if (propValue instanceof String && StringUtils.isNotBlank((String) propValue)) {
416 						return true;
417 					} else if (propValue != null) {
418 						return true;
419 					}
420 				}
421 			}
422 			return false;
423 		} else {
424 			LOG.warn("Attempt to check areAnyPrimaryKeyAttributesPopulated on object with no metadata: "
425 					+ dataObject.getClass().getName());
426 			return true;
427 		}
428 	}
429 
430     /**
431      * {@inheritDoc}
432      */
433 	@Override
434 	public List<String> getUnpopulatedPrimaryKeyAttributeNames() {
435 		List<String> emptyKeys = new ArrayList<String>();
436 		if (metadata != null) {
437 			List<String> primaryKeyAttributeNames = metadata.getPrimaryKeyAttributeNames();
438 			if (primaryKeyAttributeNames != null) {
439 				for (String primaryKeyAttributeName : primaryKeyAttributeNames) {
440 					Object propValue = getPropertyValue(primaryKeyAttributeName);
441 					if (propValue == null || (propValue instanceof String && StringUtils.isBlank((String) propValue))) {
442 						emptyKeys.add(primaryKeyAttributeName);
443 					}
444 				}
445 			}
446 		} else {
447 			LOG.warn("Attempt to check getUnpopulatedPrimaryKeyAttributeNames on object with no metadata: "
448 					+ dataObject.getClass().getName());
449 		}
450 		return emptyKeys;
451 	}
452 
453     /**
454      * {@inheritDoc}
455      */
456     @Override
457     public boolean equalsByPrimaryKey(T object) {
458         if (object == null) {
459             return false;
460         }
461         DataObjectWrapper<T> wrap = dataObjectService.wrap(object);
462         if (!getWrappedClass().isAssignableFrom(wrap.getWrappedClass())) {
463             throw new IllegalArgumentException("The type of the given data object does not match the type of this " +
464                     "data object. Given: " + wrap.getWrappedClass() + ", but expected: " + getWrappedClass());
465         }
466         // since they are the same type, we know they must have the same number of primary keys,
467         Map<String, Object> localPks = getPrimaryKeyValues();
468         Map<String, Object> givenPks = wrap.getPrimaryKeyValues();
469         for (String localPk : localPks.keySet()) {
470             Object localPkValue = localPks.get(localPk);
471             if (localPkValue == null || !localPkValue.equals(givenPks.get(localPk))) {
472                 return false;
473             }
474         }
475         return true;
476     }
477 
478     /**
479      * {@inheritDoc}
480      */
481     @Override
482     public Object getForeignKeyValue(String relationshipName) {
483         Object foreignKeyAttributeValue = getForeignKeyAttributeValue(relationshipName);
484         if (foreignKeyAttributeValue != null) {
485             return foreignKeyAttributeValue;
486         }
487         // if there are no attribute relationships, or the attribute relationships are not fully populated, fall
488         // back to the actual relationship object
489         Object relationshipObject = getPropertyValue(relationshipName);
490         if (relationshipObject == null) {
491             return null;
492         }
493         return dataObjectService.wrap(relationshipObject).getPrimaryKeyValue();
494     }
495 
496     /**
497      * {@inheritDoc}
498      */
499     @Override
500     public Object getForeignKeyAttributeValue(String relationshipName) {
501 		Map<String, Object> attributeMap = getForeignKeyAttributeMap(relationshipName);
502 		if (attributeMap == null) {
503 			return null;
504 		}
505 		return asSingleKey(attributeMap);
506 	}
507 
508     /**
509      * Gets the map of child attribute names to the parent attribute values.
510      *
511      * @param relationshipName the name of the relationship for which to get the map.
512      * @return the map of child attribute names to the parent attribute values.
513      */
514 	public Map<String, Object> getForeignKeyAttributeMap(String relationshipName) {
515 		MetadataChild relationship = findAndValidateRelationship(relationshipName);
516         List<DataObjectAttributeRelationship> attributeRelationships = relationship.getAttributeRelationships();
517 
518         if (!attributeRelationships.isEmpty()) {
519             Map<String, Object> attributeMap = new LinkedHashMap<String, Object>();
520 
521             for (DataObjectAttributeRelationship attributeRelationship : attributeRelationships) {
522                 // obtain the property value on the current parent object
523                 String parentAttributeName = attributeRelationship.getParentAttributeName();
524                 Object parentAttributeValue = null;
525 
526                 try {
527                     parentAttributeValue = getPropertyValue(parentAttributeName);
528                 } catch (BeansException be) {
529                     // exception thrown may be a db property which may not be defined on class (JPA foreign keys)
530                     // use null value for parentAttributeValue
531                 }
532 
533                 // not all of our relationships are populated, so we cannot obtain a valid foreign key
534                 if (parentAttributeValue == null) {
535                     return null;
536                 }
537 
538                 // store the mapping with the child attribute name to fetch on the referenced child object
539                 String childAttributeName = attributeRelationship.getChildAttributeName();
540                 attributeMap.put(childAttributeName, parentAttributeValue);
541             }
542 
543             return attributeMap;
544         }
545 
546         return null;
547     }
548 
549     /**
550      * 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
551      * {@link CompoundKey}.
552      *
553      * @param keyValues the map of keys to process.
554      * @return a single key from a set map of keys.
555      */
556     private Object asSingleKey(Map<String, Object> keyValues) {
557         if (keyValues.size() == 1) {
558             return keyValues.values().iterator().next();
559         }
560         return new CompoundKey(keyValues);
561     }
562 
563     /**
564      * {@inheritDoc}
565      */
566     @Override
567     public Class<?> getPropertyTypeNullSafe(Class<?> objectType, String propertyName) {
568         DataObjectMetadata objectMetadata = dataObjectService.getMetadataRepository().getMetadata(objectType);
569         return getPropertyTypeChild(objectMetadata,propertyName);
570     }
571 
572     /**
573      * Gets the property type for a property name.
574      *
575      * @param objectMetadata the metadata object.
576      * @param propertyName the name of the property.
577      * @return the property type for a property name.
578      */
579     private Class<?> getPropertyTypeChild(DataObjectMetadata objectMetadata, String propertyName){
580         if(PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)){
581             String attributePrefix = StringUtils.substringBefore(propertyName,".");
582             String attributeName = StringUtils.substringAfter(propertyName,".");
583 
584             if(StringUtils.isNotBlank(attributePrefix) && StringUtils.isNotBlank(attributeName) &&
585                     objectMetadata!= null){
586                 Class<?> propertyType = traverseRelationship(objectMetadata,attributePrefix,attributeName);
587                 if(propertyType != null){
588                     return propertyType;
589                 }
590             }
591         }
592         return getPropertyType(propertyName);
593     }
594 
595     /**
596      * Gets the property type for a property name in a relationship.
597      *
598      * @param objectMetadata the metadata object.
599      * @param attributePrefix the prefix of the property that indicated it was in a relationship.
600      * @param attributeName the name of the property.
601      * @return the property type for a property name.
602      */
603     private Class<?> traverseRelationship(DataObjectMetadata objectMetadata,String attributePrefix,
604                                           String attributeName){
605         DataObjectRelationship rd = objectMetadata.getRelationship(attributePrefix);
606         if(rd != null){
607             DataObjectMetadata relatedObjectMetadata =
608                     dataObjectService.getMetadataRepository().getMetadata(rd.getRelatedType());
609             if(relatedObjectMetadata != null){
610                 if(PropertyAccessorUtils.isNestedOrIndexedProperty(attributeName)){
611                     return getPropertyTypeChild(relatedObjectMetadata,attributeName);
612                 } else{
613                     if(relatedObjectMetadata.getAttribute(attributeName) == null &&
614                             relatedObjectMetadata.getRelationship(attributeName)!=null){
615                         DataObjectRelationship relationship = relatedObjectMetadata.getRelationship(attributeName);
616                         return relationship.getRelatedType();
617                     }
618                     return relatedObjectMetadata.getAttribute(attributeName).getDataType().getType();
619                 }
620             }
621         }
622         return null;
623     }
624 
625     /**
626      * {@inheritDoc}
627      */
628     @Override
629     public void linkChanges(Set<String> changedPropertyPaths) {
630         referenceLinker.linkChanges(getWrappedInstance(), changedPropertyPaths);
631     }
632 
633     /**
634      * {@inheritDoc}
635      */
636     @Override
637     public void linkForeignKeys(boolean onlyLinkReadOnly) {
638         linkForeignKeysInternalWrapped(this, onlyLinkReadOnly, Sets.newHashSet());
639     }
640 
641     /**
642      * Links all foreign keys on the data object.
643      *
644      * @param object the object to link.
645      * @param onlyLinkReadOnly whether to only link read-only objects.
646      * @param linked the set of currently linked objects, used as a base case to exit out of recursion.
647      */
648     protected void linkForeignKeysInternal(Object object, boolean onlyLinkReadOnly, Set<Object> linked) {
649         if (object == null || linked.contains(object) || !dataObjectService.supports(object.getClass())) {
650             return;
651         }
652         linked.add(object);
653         DataObjectWrapper<?> wrapped = dataObjectService.wrap(object);
654         linkForeignKeysInternalWrapped(wrapped, onlyLinkReadOnly, linked);
655     }
656 
657     /**
658      * Links all foreign keys on the wrapped data object.
659      *
660      * @param wrapped the wrapped object to link.
661      * @param onlyLinkReadOnly whether to only link read-only objects.
662      * @param linked the set of currently linked objects, used as a base case to exit out of recursion.
663      */
664     protected void linkForeignKeysInternalWrapped(DataObjectWrapper<?> wrapped, boolean onlyLinkReadOnly, Set<Object> linked) {
665         List<DataObjectRelationship> relationships = wrapped.getMetadata().getRelationships();
666         for (DataObjectRelationship relationship : relationships) {
667             String relationshipName = relationship.getName();
668             Object relationshipValue = wrapped.getPropertyValue(relationshipName);
669 
670             // let's get the current value and recurse down if it's a relationship that is cascaded on save
671             if (relationship.isSavedWithParent()) {
672 
673                 linkForeignKeysInternal(relationshipValue, onlyLinkReadOnly, linked);
674             }
675 
676             // next, if we have related attributes, we need to link our keys
677             linkForeignKeysInternal(wrapped, relationship, relationshipValue, onlyLinkReadOnly);
678         }
679         List<DataObjectCollection> collections = wrapped.getMetadata().getCollections();
680         for (DataObjectCollection collection : collections) {
681             String relationshipName = collection.getName();
682 
683             // let's get the current value and recurse down for each element if it's a collection that is cascaded on save
684             if (collection.isSavedWithParent()) {
685                 Collection<?> collectionValue = (Collection<?>)wrapped.getPropertyValue(relationshipName);
686                 if (collectionValue != null) {
687                     for (Object object : collectionValue) {
688                         linkForeignKeysInternal(object, onlyLinkReadOnly, linked);
689                     }
690                 }
691             }
692         }
693 
694     }
695 
696     /**
697      * {@inheritDoc}
698      */
699     @Override
700     public void fetchRelationship(String relationshipName) {
701         fetchRelationship(relationshipName, true, true);
702     }
703 
704     /**
705      * {@inheritDoc}
706      */
707     @Override
708     public void fetchRelationship(String relationshipName, boolean useForeignKeyAttribute, boolean nullifyDanglingRelationship) {
709         fetchRelationship(findAndValidateRelationship(relationshipName), useForeignKeyAttribute,
710                 nullifyDanglingRelationship);
711     }
712     /**
713      * Fetches and populates the value for the relationship with the given name on the wrapped object.
714      *
715      * @param relationship the relationship on the wrapped data object to refresh
716      * @param useForeignKeyAttribute whether to use the foreign key attribute to fetch the relationship
717      * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found
718      */
719 	protected void fetchRelationship(MetadataChild relationship, boolean useForeignKeyAttribute, boolean nullifyDanglingRelationship) {
720         Class<?> relatedType = relationship.getRelatedType();
721         if (!dataObjectService.supports(relatedType)) {
722             LOG.warn("Encountered a related type that is not supported by DataObjectService, fetch "
723                     + "relationship will do nothing: " + relatedType);
724             return;
725         }
726         // if we have at least one attribute relationships here, then we are set to proceed
727         if (useForeignKeyAttribute) {
728             fetchRelationshipUsingAttributes(relationship, nullifyDanglingRelationship);
729         } else {
730             fetchRelationshipUsingIdentity(relationship, nullifyDanglingRelationship);
731         }
732     }
733 
734     /**
735      * Fetches the relationship using the foreign key attributes.
736      *
737      * @param relationship the relationship on the wrapped data object to refresh
738      * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found
739      */
740     protected void fetchRelationshipUsingAttributes(MetadataChild relationship, boolean nullifyDanglingRelationship) {
741         Class<?> relatedType = relationship.getRelatedType();
742         if (relationship.getAttributeRelationships().isEmpty()) {
743             LOG.warn("Attempted to fetch a relationship using a foreign key attribute "
744                     + "when one does not exist: "
745                     + relationship.getName());
746         } else {
747             Object fetchedValue = null;
748             if (relationship instanceof DataObjectRelationship) {
749                 Object foreignKey = getForeignKeyAttributeValue(relationship.getName());
750                 if (foreignKey != null) {
751                     fetchedValue = dataObjectService.find(relatedType, foreignKey);
752                 }
753             } else if (relationship instanceof DataObjectCollection) {
754                 Map<String, Object> foreignKeyAttributeMap = getForeignKeyAttributeMap(relationship.getName());
755                 fetchedValue = dataObjectService.findMatching(relatedType,
756                         QueryByCriteria.Builder.andAttributes(foreignKeyAttributeMap).build()).getResults();
757             }
758             if (fetchedValue != null || nullifyDanglingRelationship) {
759                 setPropertyValue(relationship.getName(), fetchedValue);
760             }
761         }
762     }
763 
764     /**
765      * Fetches the relationship using the primary key attributes.
766      *
767      * @param relationship the relationship on the wrapped data object to refresh
768      * @param nullifyDanglingRelationship whether to set the related object to null if no relationship value is found
769      */
770     protected void fetchRelationshipUsingIdentity(MetadataChild relationship, boolean nullifyDanglingRelationship) {
771         Object propertyValue = getPropertyValue(relationship.getName());
772         if (propertyValue != null) {
773             if (!dataObjectService.supports(propertyValue.getClass())) {
774                 throw new IllegalArgumentException("Attempting to fetch an invalid relationship, must be a"
775                         + "DataObjectRelationship when fetching without a foreign key");
776             }
777             DataObjectWrapper<?> wrappedRelationship = dataObjectService.wrap(propertyValue);
778             Map<String, Object> primaryKeyValues = wrappedRelationship.getPrimaryKeyValues();
779             Object newPropertyValue = dataObjectService.find(wrappedRelationship.getWrappedClass(),
780                     new CompoundKey(primaryKeyValues));
781             if (newPropertyValue != null || nullifyDanglingRelationship) {
782                 propertyValue = newPropertyValue;
783                 setPropertyValue(relationship.getName(), propertyValue);
784             }
785         }
786         // now copy pk values back to the foreign key, because we are being explicity asked to fetch the relationship
787         // using the identity and not the FK, we don't care about whether the FK field is read only or not so pass
788         // "false" for onlyLinkReadOnly argument to linkForeignKeys
789         linkForeignKeysInternal(this, relationship, propertyValue, false);
790         populateInverseRelationship(relationship, propertyValue);
791     }
792 
793     /**
794      * {@inheritDoc}
795      */
796     @Override
797     public void linkForeignKeys(String relationshipName, boolean onlyLinkReadOnly) {
798         MetadataChild relationship = findAndValidateRelationship(relationshipName);
799         Object propertyValue = getPropertyValue(relationshipName);
800         linkForeignKeysInternal(this, relationship, propertyValue, onlyLinkReadOnly);
801     }
802 
803     /**
804      * Links foreign keys non-recursively using the relationship with the given name on the wrapped data object.
805      *
806      * @param wrapped the wrapped object to link.
807      * @param relationship the relationship on the wrapped data object for which to link foreign keys.
808      * @param relationshipValue the value of the relationship.
809      * @param onlyLinkReadOnly indicates whether or not only read-only foreign keys should be linked.
810      */
811     protected void linkForeignKeysInternal(DataObjectWrapper<?> wrapped, MetadataChild relationship,
812             Object relationshipValue, boolean onlyLinkReadOnly) {
813         if (!relationship.getAttributeRelationships().isEmpty()) {
814             // this means there's a foreign key so we need to copy values back
815             DataObjectWrapper<?> wrappedRelationship = null;
816             if (relationshipValue != null) {
817                 wrappedRelationship = dataObjectService.wrap(relationshipValue);
818             }
819             for (DataObjectAttributeRelationship attributeRelationship : relationship.getAttributeRelationships()) {
820                 String parentAttributeName = attributeRelationship.getParentAttributeName();
821                 // if the property value is null, we need to copy null back to all parent foreign keys,
822                 // otherwise we copy back the actual value
823                 Object childAttributeValue = null;
824                 if (wrappedRelationship != null) {
825                     childAttributeValue =
826                             wrappedRelationship.getPropertyValue(attributeRelationship.getChildAttributeName());
827                 }
828                 if (onlyLinkReadOnly) {
829                     DataObjectAttribute attribute = wrapped.getMetadata().getAttribute(parentAttributeName);
830                     if (attribute.isReadOnly()) {
831                         wrapped.setPropertyValue(parentAttributeName, childAttributeValue);
832                     }
833                 } else {
834                     wrapped.setPropertyValue(parentAttributeName, childAttributeValue);
835                 }
836             }
837         }
838     }
839 
840     /**
841      * Populates the property on the other side of the relationship.
842      *
843      * @param relationship the relationship on the wrapped data object for which to populate the inverse relationship.
844      * @param propertyValue the value of the property.
845      */
846     protected void populateInverseRelationship(MetadataChild relationship, Object propertyValue) {
847         if (propertyValue != null) {
848             MetadataChild inverseRelationship = relationship.getInverseRelationship();
849             if (inverseRelationship != null) {
850                 DataObjectWrapper<?> wrappedRelationship = dataObjectService.wrap(propertyValue);
851                 if (inverseRelationship instanceof DataObjectCollection) {
852                     DataObjectCollection collectionRelationship = (DataObjectCollection)inverseRelationship;
853                     String colRelName = inverseRelationship.getName();
854                     Collection<Object> collection =
855                             (Collection<Object>)wrappedRelationship.getPropertyValue(colRelName);
856                     if (collection == null) {
857                         // if the collection is null, let's instantiate an empty one
858                         collection =
859                                 CollectionFactory.createCollection(wrappedRelationship.getPropertyType(colRelName), 1);
860                         wrappedRelationship.setPropertyValue(colRelName, collection);
861                     }
862                     collection.add(getWrappedInstance());
863                 }
864             }
865         }
866     }
867 
868     /**
869      * Finds and validates the relationship specified by the given name.
870      *
871      * @param relationshipName the name of the relationship to find.
872      * @return the found relationship.
873      */
874 	private MetadataChild findAndValidateRelationship(String relationshipName) {
875         if (StringUtils.isBlank(relationshipName)) {
876             throw new IllegalArgumentException("The relationshipName must not be null or blank");
877         }
878         // validate the relationship exists
879 		MetadataChild relationship = getMetadata().getRelationship(relationshipName);
880         if (relationship == null) {
881 			relationship = getMetadata().getCollection(relationshipName);
882 			if (relationship == null) {
883 				throw new IllegalArgumentException("Failed to locate a valid relationship from " + getWrappedClass()
884 						+ " with the given relationship name '" + relationshipName + "'");
885 			}
886         }
887         return relationship;
888     }
889 
890 }