View Javadoc
1   /**
2    * Copyright 2005-2015 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 java.beans.PropertyDescriptor;
19  import java.beans.PropertyEditor;
20  import java.lang.reflect.Field;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import org.apache.commons.lang.ArrayUtils;
30  import org.apache.commons.lang.StringUtils;
31  import org.kuali.rice.core.api.criteria.QueryByCriteria;
32  import org.kuali.rice.krad.data.CompoundKey;
33  import org.kuali.rice.krad.data.DataObjectService;
34  import org.kuali.rice.krad.data.DataObjectWrapper;
35  import org.kuali.rice.krad.data.MaterializeOption;
36  import org.kuali.rice.krad.data.metadata.DataObjectAttribute;
37  import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
38  import org.kuali.rice.krad.data.metadata.DataObjectCollection;
39  import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
40  import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
41  import org.kuali.rice.krad.data.metadata.MetadataChild;
42  import org.kuali.rice.krad.data.util.ReferenceLinker;
43  import org.springframework.beans.BeanWrapper;
44  import org.springframework.beans.BeansException;
45  import org.springframework.beans.InvalidPropertyException;
46  import org.springframework.beans.NullValueInNestedPathException;
47  import org.springframework.beans.PropertyAccessorFactory;
48  import org.springframework.beans.PropertyAccessorUtils;
49  import org.springframework.beans.PropertyValue;
50  import org.springframework.beans.PropertyValues;
51  import org.springframework.beans.TypeMismatchException;
52  import org.springframework.core.CollectionFactory;
53  import org.springframework.core.MethodParameter;
54  import org.springframework.core.convert.ConversionService;
55  import org.springframework.core.convert.TypeDescriptor;
56  
57  import com.google.common.collect.Sets;
58  
59  /**
60   * The base implementation of {@link DataObjectWrapper}.
61   *
62   * @param <T> the type of the data object to wrap.
63   *
64   * @author Kuali Rice Team (rice.collab@kuali.org)
65   */
66  public abstract class DataObjectWrapperBase<T> implements DataObjectWrapper<T> {
67  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DataObjectWrapperBase.class);
68  
69      private final T dataObject;
70      private final DataObjectMetadata metadata;
71      private final BeanWrapper wrapper;
72      private final DataObjectService dataObjectService;
73      private final ReferenceLinker referenceLinker;
74  
75      /**
76       * Creates a data object wrapper.
77       *
78       * @param dataObject the data object to wrap.
79       * @param metadata the metadata of the data object.
80       * @param dataObjectService the data object service to use.
81       * @param referenceLinker the reference linker implementation.
82       */
83      protected DataObjectWrapperBase(T dataObject, DataObjectMetadata metadata, DataObjectService dataObjectService,
84              ReferenceLinker referenceLinker) {
85          this.dataObject = dataObject;
86          this.metadata = metadata;
87          this.dataObjectService = dataObjectService;
88          this.referenceLinker = referenceLinker;
89          this.wrapper = PropertyAccessorFactory.forBeanPropertyAccess(dataObject);
90          // note that we do *not* want to set auto grow to be true here since we are using this primarily for
91          // access to the data, we will expose getPropertyValueNullSafe instead because it prevents a a call to
92          // getPropertyValue from modifying the internal state of the object by growing intermediate nested paths
93      }
94  
95      /**
96       * {@inheritDoc}
97       */
98      @Override
99      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 }