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