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