com.github.dozermapper.core.MappingProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.github.dozermapper.core.MappingProcessor.java

Source

/*
 * Copyright 2005-2018 Dozer Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.dozermapper.core;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.github.dozermapper.core.builder.BuilderUtil;
import com.github.dozermapper.core.builder.DestBeanBuilderCreator;
import com.github.dozermapper.core.cache.Cache;
import com.github.dozermapper.core.cache.CacheKeyFactory;
import com.github.dozermapper.core.cache.CacheManager;
import com.github.dozermapper.core.cache.DozerCacheType;
import com.github.dozermapper.core.classmap.ClassMap;
import com.github.dozermapper.core.classmap.ClassMapBuilder;
import com.github.dozermapper.core.classmap.ClassMappings;
import com.github.dozermapper.core.classmap.Configuration;
import com.github.dozermapper.core.classmap.CopyByReferenceContainer;
import com.github.dozermapper.core.classmap.MappingDirection;
import com.github.dozermapper.core.classmap.RelationshipType;
import com.github.dozermapper.core.classmap.generator.BeanMappingGenerator;
import com.github.dozermapper.core.config.BeanContainer;
import com.github.dozermapper.core.converters.DateFormatContainer;
import com.github.dozermapper.core.converters.PrimitiveOrWrapperConverter;
import com.github.dozermapper.core.events.DefaultEvent;
import com.github.dozermapper.core.events.EventManager;
import com.github.dozermapper.core.events.EventTypes;
import com.github.dozermapper.core.factory.BeanCreationDirective;
import com.github.dozermapper.core.factory.DestBeanCreator;
import com.github.dozermapper.core.fieldmap.CustomGetSetMethodFieldMap;
import com.github.dozermapper.core.fieldmap.ExcludeFieldMap;
import com.github.dozermapper.core.fieldmap.FieldMap;
import com.github.dozermapper.core.fieldmap.HintContainer;
import com.github.dozermapper.core.fieldmap.MapFieldMap;
import com.github.dozermapper.core.propertydescriptor.PropertyDescriptorFactory;
import com.github.dozermapper.core.util.CollectionUtils;
import com.github.dozermapper.core.util.DozerConstants;
import com.github.dozermapper.core.util.IteratorUtils;
import com.github.dozermapper.core.util.LogMsgFactory;
import com.github.dozermapper.core.util.MappingUtils;
import com.github.dozermapper.core.util.MappingValidator;
import com.github.dozermapper.core.util.ReflectionUtils;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.github.dozermapper.core.util.DozerConstants.BASE_CLASS;
import static com.github.dozermapper.core.util.DozerConstants.ITERATE;

/**
 * Internal Mapping Engine. Not intended for direct use by Application code.
 * This class does most of the heavy lifting and is very recursive in nature.
 * <p>
 * This class is not threadsafe and is instantiated for each new mapping request.
 */
public class MappingProcessor implements Mapper {

    private final Logger log = LoggerFactory.getLogger(MappingProcessor.class);

    private final ClassMappings classMappings;
    private final Configuration globalConfiguration;
    private final List<CustomConverter> customConverterObjects;
    private final Map<String, CustomConverter> customConverterObjectsWithId;
    private final EventManager eventManager;
    private final CustomFieldMapper customFieldMapper;

    private final MappedFieldsTracker mappedFields = new MappedFieldsTracker();

    private final Cache converterByDestTypeCache;
    private final Cache superTypeCache;
    private final PrimitiveOrWrapperConverter primitiveConverter;
    private final LogMsgFactory logMsgFactory = new LogMsgFactory();
    private final BeanContainer beanContainer;
    private final ClassMapBuilder classMapBuilder;
    private final DestBeanCreator destBeanCreator;
    private final DestBeanBuilderCreator destBeanBuilderCreator;

    protected MappingProcessor(ClassMappings classMappings, Configuration globalConfiguration,
            CacheManager cacheMgr, List<CustomConverter> customConverterObjects, EventManager eventManager,
            CustomFieldMapper customFieldMapper, Map<String, CustomConverter> customConverterObjectsWithId,
            BeanContainer beanContainer, DestBeanCreator destBeanCreator,
            DestBeanBuilderCreator destBeanBuilderCreator, BeanMappingGenerator beanMappingGenerator,
            PropertyDescriptorFactory propertyDescriptorFactory) {
        this.classMappings = classMappings;
        this.globalConfiguration = globalConfiguration;
        this.customConverterObjects = customConverterObjects;
        this.eventManager = eventManager;
        this.customFieldMapper = customFieldMapper;
        this.converterByDestTypeCache = cacheMgr.getCache(DozerCacheType.CONVERTER_BY_DEST_TYPE.name());
        this.superTypeCache = cacheMgr.getCache(DozerCacheType.SUPER_TYPE_CHECK.name());
        this.customConverterObjectsWithId = customConverterObjectsWithId;
        this.beanContainer = beanContainer;
        this.destBeanBuilderCreator = destBeanBuilderCreator;
        this.classMapBuilder = new ClassMapBuilder(beanContainer, destBeanCreator, beanMappingGenerator,
                propertyDescriptorFactory);
        this.primitiveConverter = new PrimitiveOrWrapperConverter(beanContainer);
        this.destBeanCreator = destBeanCreator;
    }

    /* Mapper Interface Implementation */

    public <T> T map(final Object srcObj, final Class<T> destClass) {
        return map(srcObj, destClass, null);
    }

    public <T> T map(final Object srcObj, final Class<T> destClass, final String mapId) {
        MappingValidator.validateMappingRequest(srcObj, destClass);
        return mapGeneral(srcObj, destClass, null, mapId);
    }

    public void map(final Object srcObj, final Object destObj) {
        map(srcObj, destObj, null);
    }

    public void map(final Object srcObj, final Object destObj, final String mapId) {
        MappingValidator.validateMappingRequest(srcObj, destObj);
        mapGeneral(srcObj, null, destObj, mapId);
    }

    /**
     * Only required due to type coupling of MappingProcessor and Mapper - needs re-think
     * @return nothing
     */
    @Override
    public MapperModelContext getMapperModelContext() {
        throw new IllegalArgumentException("Can only be called on instance returned by DozerBeanMapperBuilder");
    }

    /* End of Mapper Interface Implementation */

    /**
     * Single point of entry for atomic mapping operations
     *
     * @param srcObj    source object
     * @param destClass destination class
     * @param destObj   destination object
     * @param mapId     mapping identifier
     * @param <T>       destination object type
     * @return new or updated destination object
     */
    private <T> T mapGeneral(Object srcObj, final Class<T> destClass, final T destObj, final String mapId) {
        srcObj = MappingUtils.deProxy(srcObj, beanContainer);

        Class<T> destType;
        T result;
        if (destClass == null) {
            destType = (Class<T>) destObj.getClass();
            result = destObj;
        } else {
            destType = destClass;
            result = null;
        }

        ClassMap classMap = null;
        try {
            classMap = getClassMap(srcObj.getClass(), destType, mapId);

            eventManager.on(new DefaultEvent(EventTypes.MAPPING_STARTED, classMap, null, srcObj, result, null));

            // TODO Check if any proxy issues are here
            // Check to see if custom converter has been specified for this mapping
            // combination. If so, just use it.
            Class<?> converterClass = MappingUtils.findCustomConverter(converterByDestTypeCache,
                    classMap.getCustomConverters(), srcObj.getClass(), destType);

            if (destObj == null) {
                // If this is a nested MapperAware conversion this mapping can be already processed
                // but we can do this optimization only in case of no destObject, instead we must copy to the dest object
                Object alreadyMappedValue = mappedFields.getMappedValue(srcObj, destType, mapId);
                if (alreadyMappedValue != null) {
                    return (T) alreadyMappedValue;
                }
            }

            if (converterClass != null) {
                return (T) mapUsingCustomConverter(converterClass, srcObj.getClass(), srcObj, destType, result,
                        null, true);
            }

            BeanCreationDirective creationDirective = new BeanCreationDirective(srcObj, classMap.getSrcClassToMap(),
                    classMap.getDestClassToMap(), destType, classMap.getDestClassBeanFactory(),
                    classMap.getDestClassBeanFactoryId(), classMap.getDestClassCreateMethod(),
                    classMap.getDestClass().isSkipConstructor());

            result = createByCreationDirectiveAndMap(creationDirective, classMap, srcObj, result, false, null);
        } catch (Throwable e) {
            MappingUtils.throwMappingException(e);
        }
        eventManager.on(new DefaultEvent(EventTypes.MAPPING_FINISHED, classMap, null, srcObj, result, null));

        return result;
    }

    /**
     * Create builder or target object if needed and call
     * {@link MappingProcessor#mapToDestObject(com.github.dozermapper.core.classmap.ClassMap, Object, Object, boolean, String)} function with
     * arguments {@code classMap}, {@code srcObj}, {@code result}, {@code bypassSuperMappings}, {@code mapId}
     *
     * @param creationDirective   directive for concrete mapping (based mostly on {@code classMap})
     * @param classMap            class map information for concrete class
     * @param srcObj              source object
     * @param result              target entity for mapping
     * @param bypassSuperMappings //TODO
     * @param mapId               mapping identifier
     * @return result or created target entity for mapping
     */
    private <T> T createByCreationDirectiveAndMap(BeanCreationDirective creationDirective, ClassMap classMap,
            Object srcObj, T result, boolean bypassSuperMappings, String mapId) {
        if (result == null) {
            BeanBuilder beanBuilder = destBeanBuilderCreator.create(creationDirective);
            if (beanBuilder == null) {
                result = (T) destBeanCreator.create(creationDirective);
                mapToDestObject(classMap, srcObj, result, bypassSuperMappings, mapId);
            } else {
                mapToDestObject(classMap, srcObj, beanBuilder, bypassSuperMappings, mapId);
                result = (T) beanBuilder.build();
            }
        } else {
            mapToDestObject(classMap, srcObj, result, bypassSuperMappings, mapId);
        }
        return result;
    }

    /**
     * This function used to map into created instance of destination class
     *
     * @param classMap            object with mapping configuration
     * @param srcObj              source object
     * @param destObj             destination object
     * @param bypassSuperMappings //TODO
     * @param mapId               mapping identifier
     */
    private void mapToDestObject(ClassMap classMap, Object srcObj, Object destObj, boolean bypassSuperMappings,
            String mapId) {
        Object result = destObj;
        if (javax.xml.bind.JAXBElement.class.isAssignableFrom(destObj.getClass())) {
            classMap = getClassMap(srcObj.getClass(),
                    javax.xml.bind.JAXBElement.class.cast(destObj).getDeclaredType(), mapId);
            result = javax.xml.bind.JAXBElement.class.cast(destObj).getValue();
        }

        map(classMap, srcObj, result, bypassSuperMappings, new ArrayList<>(), mapId);
    }

    private void map(ClassMap classMap, Object srcObj, Object destObj, boolean bypassSuperMappings,
            List<String> mappedParentFields, String mapId) {
        srcObj = MappingUtils.deProxy(srcObj, beanContainer);

        // 1596766 - Recursive object mapping issue. Prevent recursive mapping
        // infinite loop. Keep a record of mapped fields
        // by storing the id of the sourceObj and the destObj to be mapped. This can
        // be referred to later to avoid recursive mapping loops
        mappedFields.put(srcObj, destObj, mapId);

        // If class map hasn't already been determined, find the appropriate one for
        // the src/dest object combination
        if (classMap == null) {
            classMap = getClassMap(srcObj.getClass(), destObj.getClass(), mapId);
        }

        Class<?> srcClass = srcObj.getClass();
        Class<?> destClass = destObj.getClass();

        // Check to see if custom converter has been specified for this mapping
        // combination. If so, just use it.
        Class<?> converterClass = MappingUtils.findCustomConverter(converterByDestTypeCache,
                classMap.getCustomConverters(), srcClass, destClass);
        if (converterClass != null) {
            destObj = mapUsingCustomConverter(converterClass, srcClass, srcObj, destClass, destObj, null, true);
            return;
        }

        // Now check for super class mappings.  Process super class mappings first.
        if (!bypassSuperMappings) {
            Collection<ClassMap> superMappings = new ArrayList<>();

            Collection<ClassMap> superClasses = checkForSuperTypeMapping(srcClass, destClass);
            //List<ClassMap> interfaceMappings = classMappings.findInterfaceMappings(srcClass, destClass);

            superMappings.addAll(superClasses);
            //superMappings.addAll(interfaceMappings);
            if (!superMappings.isEmpty()) {
                processSuperTypeMapping(superMappings, srcObj, destObj, mappedParentFields, mapId);
            }
        }

        // Perform mappings for each field. Iterate through Fields Maps for this class mapping
        for (FieldMap fieldMapping : classMap.getFieldMaps()) {
            //Bypass field if it has already been mapped as part of super class mappings.
            String key = MappingUtils.getMappedParentFieldKey(destObj, fieldMapping);
            if (mappedParentFields != null && mappedParentFields.contains(key)) {
                continue;
            }
            mapField(fieldMapping, srcObj, destObj);
        }
    }

    /**
     * Perform mapping of a field.
     * Uses {@link #mapFromFieldMap(Object, Object, Object, FieldMap)} to do the real work, unless
     * if iterate, where {@link #mapFromIterateMethodFieldMap(Object, Object, Object, FieldMap)} is used.
     *
     * @param fieldMapping Field mapping.
     * @param srcObj       Source object.
     * @param destObj      Destination object.
     */
    private void mapField(FieldMap fieldMapping, Object srcObj, Object destObj) {

        // The field has been explicitly excluded from mapping. So just return, as
        // no further processing is needed for this field
        if (fieldMapping instanceof ExcludeFieldMap) {
            return;
        }

        Object srcFieldValue = null;
        try {
            // If a custom field mapper was specified, then invoke it. If not, or the
            // custom field mapper returns false(indicating the
            // field was not actually mapped by the custom field mapper), proceed as
            // normal(use Dozer to map the field)
            srcFieldValue = fieldMapping.getSrcFieldValue(srcObj);
            boolean fieldMapped = false;
            if (customFieldMapper != null) {
                fieldMapped = customFieldMapper.mapField(srcObj, destObj, srcFieldValue, fieldMapping.getClassMap(),
                        fieldMapping);
            }

            if (!fieldMapped) {
                if (fieldMapping.getDestFieldType() != null && ITERATE.equals(fieldMapping.getDestFieldType())) {
                    // special logic for iterate feature
                    mapFromIterateMethodFieldMap(srcObj, destObj, srcFieldValue, fieldMapping);
                } else {
                    // either deep field map or generic map. The is the most likely
                    // scenario
                    mapFromFieldMap(srcObj, destObj, srcFieldValue, fieldMapping);
                }
            }

        } catch (Throwable e) {
            log.error(logMsgFactory.createFieldMappingErrorMsg(srcObj, fieldMapping, srcFieldValue, destObj), e);

            // check error handling policy.
            if (fieldMapping.isStopOnErrors()) {
                MappingUtils.throwMappingException(e);
            } else {
                // check if any Exceptions should be allowed to be thrown
                if (!fieldMapping.getClassMap().getAllowedExceptions().isEmpty()
                        && e.getCause() instanceof InvocationTargetException) {
                    Throwable thrownType = ((InvocationTargetException) e.getCause()).getTargetException();
                    Class<? extends Throwable> exceptionClass = thrownType.getClass();
                    if (fieldMapping.getClassMap().getAllowedExceptions().contains(exceptionClass)) {
                        throw (RuntimeException) thrownType;
                    }
                }
            }
        }
    }

    private void mapFromFieldMap(Object srcObj, Object destObj, Object srcFieldValue, FieldMap fieldMapping) {
        Class<?> destFieldType;
        if (fieldMapping instanceof CustomGetSetMethodFieldMap) {
            try {
                destFieldType = fieldMapping.getDestFieldWriteMethodParameter(destObj.getClass());
            } catch (Throwable e) {
                // try traditional way
                destFieldType = fieldMapping.getDestFieldType(BuilderUtil.unwrapDestClassFromBuilder(destObj));
            }
        } else {
            destFieldType = fieldMapping.getDestFieldType(BuilderUtil.unwrapDestClassFromBuilder(destObj));
        }

        // 1476780 - 12/2006 mht - Add support for field level custom converters
        // Use field level custom converter if one was specified. Otherwise, map or
        // recurse the object as normal
        // 1770440 - fdg - Using multiple instances of CustomConverter
        Object destFieldValue;
        if (!MappingUtils.isBlankOrNull(fieldMapping.getCustomConverterId())) {
            if (customConverterObjectsWithId != null
                    && customConverterObjectsWithId.containsKey(fieldMapping.getCustomConverterId())) {
                Class<?> srcFieldClass = srcFieldValue != null ? srcFieldValue.getClass()
                        : fieldMapping.getSrcFieldType(srcObj.getClass());
                destFieldValue = mapUsingCustomConverterInstance(
                        customConverterObjectsWithId.get(fieldMapping.getCustomConverterId()), srcFieldClass,
                        srcFieldValue, destFieldType, destObj, fieldMapping, false);
            } else {
                throw new MappingException(
                        "CustomConverter instance not found with id:" + fieldMapping.getCustomConverterId());
            }
        } else if (MappingUtils.isBlankOrNull(fieldMapping.getCustomConverter())) {
            destFieldValue = mapOrRecurseObject(srcObj, srcFieldValue, destFieldType, fieldMapping, destObj);
        } else {
            Class<?> srcFieldClass = srcFieldValue != null ? srcFieldValue.getClass()
                    : fieldMapping.getSrcFieldType(srcObj.getClass());
            destFieldValue = mapUsingCustomConverter(
                    MappingUtils.loadClass(fieldMapping.getCustomConverter(), beanContainer), srcFieldClass,
                    srcFieldValue, destFieldType, destObj, fieldMapping, false);
        }

        writeDestinationValue(destObj, destFieldValue, fieldMapping, srcObj);

        if (log.isDebugEnabled()) {
            log.debug(logMsgFactory.createFieldMappingSuccessMsg(srcObj.getClass(), destObj.getClass(),
                    fieldMapping.getSrcFieldName(), fieldMapping.getDestFieldName(), srcFieldValue, destFieldValue,
                    fieldMapping.getClassMap().getMapId()));
        }
    }

    private Object mapOrRecurseObject(Object srcObj, Object srcFieldValue, Class<?> destFieldType,
            FieldMap fieldMap, Object destObj) {
        Class<?> srcFieldClass = srcFieldValue != null ? srcFieldValue.getClass()
                : fieldMap.getSrcFieldType(srcObj.getClass());
        Class<?> converterClass = MappingUtils.determineCustomConverter(fieldMap, converterByDestTypeCache,
                fieldMap.getClassMap().getCustomConverters(), srcFieldClass, destFieldType);

        // 1-2007 mht: Invoke custom converter even if the src value is null.
        // #1563795
        if (converterClass != null) {
            return mapUsingCustomConverter(converterClass, srcFieldClass, srcFieldValue, destFieldType, destObj,
                    fieldMap, false);
        }

        if (srcFieldValue == null) {
            return null;
        }

        String srcFieldName = fieldMap.getSrcFieldName();
        String destFieldName = fieldMap.getDestFieldName();

        // 1596766 - Recursive object mapping issue. Prevent recursive mapping
        // infinite loop
        // In case of "this->this" mapping this rule should be omitted as processing is done on objects, which has been
        // just marked as mapped.
        if (!(DozerConstants.SELF_KEYWORD.equals(srcFieldName)
                && DozerConstants.SELF_KEYWORD.equals(destFieldName))) {
            Object alreadyMappedValue = mappedFields.getMappedValue(srcFieldValue, destFieldType,
                    fieldMap.getMapId());
            if (alreadyMappedValue != null) {
                return alreadyMappedValue;
            }
        }

        if (fieldMap.isCopyByReference()) {
            // just get the src and return it, no transformation.
            return srcFieldValue;
        }

        boolean isSrcFieldClassSupportedMap = MappingUtils.isSupportedMap(srcFieldClass);
        boolean isDestFieldTypeSupportedMap = MappingUtils.isSupportedMap(destFieldType);
        if (isSrcFieldClassSupportedMap && isDestFieldTypeSupportedMap) {
            return mapMap(srcObj, (Map<?, ?>) srcFieldValue, fieldMap, destObj);
        }
        if (fieldMap instanceof MapFieldMap && destFieldType.equals(Object.class)) {
            // TODO: find better place for this logic. try to encapsulate in FieldMap?
            destFieldType = fieldMap.getDestHintContainer() != null ? fieldMap.getDestHintContainer().getHint()
                    : srcFieldClass;
        }

        if (primitiveConverter.accepts(srcFieldClass) || primitiveConverter.accepts(destFieldType)) {
            // Primitive or Wrapper conversion
            if (fieldMap.getDestHintContainer() != null) {
                Class<?> destHintType = fieldMap.getDestHintType(srcFieldValue.getClass());
                // if the destType is null this means that there was more than one hint.
                // we must have already set the destType then.
                if (destHintType != null) {
                    destFieldType = destHintType;
                }
            }

            //#1841448 - if trim-strings=true, then use a trimmed src string value when converting to dest value
            Object convertSrcFieldValue = srcFieldValue;
            if (fieldMap.isTrimStrings() && srcFieldValue.getClass().equals(String.class)) {
                convertSrcFieldValue = ((String) srcFieldValue).trim();
            }

            DateFormatContainer dfContainer = new DateFormatContainer(fieldMap.getDateFormat());

            if (fieldMap instanceof MapFieldMap && !primitiveConverter.accepts(destFieldType)) {
                // This handles a very special/rare use case(see indexMapping.xml + unit
                // test
                // testStringToIndexedSet_UsingMapSetMethod). If the destFieldType is a
                // custom object AND has a String param
                // constructor, we don't want to construct the custom object with the
                // src value because the map backed property
                // logic at lower layers handles setting the value on the custom object.
                // Without this special logic, the
                // destination map backed custom object would contain a value that is
                // the custom object dest type instead of the
                // desired src value.
                return primitiveConverter.convert(convertSrcFieldValue, convertSrcFieldValue.getClass(),
                        dfContainer);
            } else {
                return primitiveConverter.convert(convertSrcFieldValue, destFieldType, dfContainer, destFieldName,
                        destObj);
            }
        }
        if (MappingUtils.isSupportedCollection(srcFieldClass)
                && (MappingUtils.isSupportedCollection(destFieldType))) {
            return mapCollection(srcObj, srcFieldValue, fieldMap, destObj);
        }

        if (MappingUtils.isEnumType(srcFieldClass, destFieldType)) {
            return mapEnum((Enum) srcFieldValue, (Class<Enum>) destFieldType);
        }

        if (fieldMap.getDestDeepIndexHintContainer() != null) {
            destFieldType = fieldMap.getDestDeepIndexHintContainer().getHint();
        }

        // Default: Map from one custom data object to another custom data object
        return mapCustomObject(fieldMap, destObj, destFieldType, destFieldName, srcFieldValue);
    }

    private <T extends Enum<T>> T mapEnum(Enum<T> srcFieldValue, Class<T> destFieldType) {
        String name = srcFieldValue.name();
        return Enum.valueOf(getSerializableEnumClass(destFieldType), name);
    }

    private <T extends Enum<T>> Class<T> getSerializableEnumClass(Class<T> enumClass) {
        if (checkIfOverriddenEnum(enumClass)) {
            /* This cast is case because of the check performed by the checkIfOverridenEnum function. */
            @SuppressWarnings("unchecked")
            Class<T> castedSuperclass = (Class<T>) enumClass.getSuperclass();
            return castedSuperclass;
        }
        return enumClass;
    }

    private <T extends Enum<T>> boolean checkIfOverriddenEnum(Class<T> enumClass) {
        return !Enum.class.equals(enumClass.getSuperclass());
    }

    private Object mapCustomObject(FieldMap fieldMap, Object destObj, Class<?> destFieldType, String destFieldName,
            Object srcFieldValue) {
        srcFieldValue = MappingUtils.deProxy(srcFieldValue, beanContainer);

        // Custom java bean. Need to make sure that the destination object is not
        // already instantiated.
        Object result = null;
        // in case of iterate feature new objects are created in any case
        if (!DozerConstants.ITERATE.equals(fieldMap.getDestFieldType())) {
            result = getExistingValue(fieldMap, destObj, destFieldType);
        }

        // if the field is not null than we don't want a new instance
        if (result == null) {
            // first check to see if this plain old field map has hints to the actual
            // type.
            if (fieldMap.getDestHintContainer() != null) {
                Class<?> destHintType = fieldMap.getDestHintType(srcFieldValue.getClass());
                // if the destType is null this means that there was more than one hint.
                // we must have already set the destType then.
                if (destHintType != null) {
                    destFieldType = destHintType;
                }
            }
            // Check to see if explicit map-id has been specified for the field
            // mapping
            String mapId = fieldMap.getMapId();

            Class<?> targetClass;
            if (fieldMap.getDestHintContainer() != null && fieldMap.getDestHintContainer().getHint() != null) {
                targetClass = fieldMap.getDestHintContainer().getHint();
            } else {
                targetClass = destFieldType;
            }
            ClassMap classMap = getClassMap(srcFieldValue.getClass(), targetClass, mapId);

            BeanCreationDirective creationDirective = new BeanCreationDirective(srcFieldValue,
                    classMap.getSrcClassToMap(), classMap.getDestClassToMap(), destFieldType,
                    classMap.getDestClassBeanFactory(), classMap.getDestClassBeanFactoryId(),
                    fieldMap.getDestFieldCreateMethod() != null ? fieldMap.getDestFieldCreateMethod()
                            : classMap.getDestClassCreateMethod(),
                    classMap.getDestClass().isSkipConstructor(), destObj, destFieldName);

            result = createByCreationDirectiveAndMap(creationDirective, classMap, srcFieldValue, null, false,
                    fieldMap.getMapId());
        } else {
            mapToDestObject(null, srcFieldValue, result, false, fieldMap.getMapId());
        }

        return result;
    }

    private Object mapCollection(Object srcObj, Object srcCollectionValue, FieldMap fieldMap, Object destObj) {
        // since we are mapping some sort of collection now is a good time to decide
        // if they provided hints
        // if no hint is provided then we will use generics to determine the mapping type
        if (fieldMap.getDestHintContainer() == null) {
            Class<?> genericType = fieldMap.getGenericType(BuilderUtil.unwrapDestClassFromBuilder(destObj));
            if (genericType != null) {
                HintContainer destHintContainer = new HintContainer(beanContainer);
                destHintContainer.setHintName(genericType.getName());
                FieldMap cloneFieldMap = (FieldMap) fieldMap.clone();
                cloneFieldMap.setDestHintContainer(destHintContainer); // should affect only this time as fieldMap is cloned
                fieldMap = cloneFieldMap;
            }
        }

        // if it is an iterator object turn it into a List
        if (srcCollectionValue instanceof Iterator) {
            srcCollectionValue = IteratorUtils.toList((Iterator<?>) srcCollectionValue);
        }

        Class<?> destCollectionType = fieldMap.getDestFieldType(BuilderUtil.unwrapDestClassFromBuilder(destObj));
        Class<?> srcFieldType = srcCollectionValue.getClass();
        Object result = null;

        // if they use a standard Collection we have to assume it is a List...better
        // way to handle this?
        if (destCollectionType.getName().equals(Collection.class.getName())) {
            // dont!!
            destCollectionType = List.class;
            if (Set.class.isAssignableFrom(srcFieldType)) {
                destCollectionType = Set.class;
            }
        }
        // Array to Array
        if (CollectionUtils.isArray(srcFieldType) && (CollectionUtils.isArray(destCollectionType))) {
            result = mapArrayToArray(srcObj, srcCollectionValue, fieldMap, destObj);
        } else if (CollectionUtils.isArray(srcFieldType) && (CollectionUtils.isList(destCollectionType))) {
            // Array to List
            result = mapArrayToList(srcObj, srcCollectionValue, fieldMap, destObj);
        } else if (CollectionUtils.isList(srcFieldType) && (CollectionUtils.isArray(destCollectionType))) {
            // List to Array
            result = mapListToArray(srcObj, (List<?>) srcCollectionValue, fieldMap, destObj);
        } else if (CollectionUtils.isSet(srcFieldType) && CollectionUtils.isSet(destCollectionType)) {
            // Set to Set
            result = addToSet(srcObj, fieldMap, (Collection<?>) srcCollectionValue, destObj);
        } else if (CollectionUtils.isSet(srcFieldType) && CollectionUtils.isArray(destCollectionType)) {
            // Set to Array
            result = mapSetToArray(srcObj, (Set<?>) srcCollectionValue, fieldMap, destObj);
        } else if (CollectionUtils.isArray(srcFieldType) && CollectionUtils.isSet(destCollectionType)) {
            // Array to Set
            result = addToSet(srcObj, fieldMap, Arrays.asList((Object[]) srcCollectionValue), destObj);
        } else if (CollectionUtils.isCollection(srcFieldType) && CollectionUtils.isSet(destCollectionType)) {
            // Collection to Set
            result = addToSet(srcObj, fieldMap, (Collection<?>) srcCollectionValue, destObj);
        } else if (CollectionUtils.isCollection(srcFieldType) && MappingUtils.isSupportedMap(destCollectionType)) {
            // List to Map value
            result = mapListToList(srcObj, (List<?>) srcCollectionValue, fieldMap, destObj);
        } else if (CollectionUtils.isCollection(srcFieldType) && CollectionUtils.isList(destCollectionType)) {
            // List to List
            // Set to List
            // Collection to List. Fix for 3378952, http://sourceforge.net/tracker/index.php?func=detail&aid=3378952&group_id=133517&atid=727368
            result = mapListToList(srcObj, (Collection<?>) srcCollectionValue, fieldMap, destObj);
        }
        return result;
    }

    private Object mapMap(Object srcObj, Map srcMapValue, FieldMap fieldMap, Object destObj) {
        Map result;
        Map destinationMap = (Map) fieldMap.getDestValue(destObj);
        if (destinationMap == null) {
            result = destBeanCreator.create(srcMapValue.getClass());
        } else {
            result = destinationMap;
            if (fieldMap.isRemoveOrphans()) {
                result.clear();
            }
        }

        for (Entry<?, Object> srcEntry : ((Map<?, Object>) srcMapValue).entrySet()) {
            Object srcEntryValue = srcEntry.getValue();

            if (srcEntryValue == null) { // overwrites with null in any case
                result.put(srcEntry.getKey(), null);
                continue;
            }

            Object destEntryValue = mapOrRecurseObject(srcObj, srcEntryValue, srcEntryValue.getClass(), fieldMap,
                    destObj);
            Object obj = result.get(srcEntry.getKey());
            if (obj != null && obj.equals(destEntryValue) && fieldMap.isNonCumulativeRelationship()) {
                mapToDestObject(null, srcEntryValue, obj, false, null);
            } else {
                result.put(srcEntry.getKey(), destEntryValue);
            }
        }
        return result;
    }

    private Object mapArrayToArray(Object srcObj, Object srcCollectionValue, FieldMap fieldMap, Object destObj) {
        Class destEntryType = fieldMap.getDestFieldType(destObj.getClass()).getComponentType();
        Class srcEntryType = srcCollectionValue.getClass().getComponentType();
        int size = Array.getLength(srcCollectionValue);

        CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
        boolean isPrimitiveArray = CollectionUtils.isPrimitiveArray(srcCollectionValue.getClass());
        boolean isFinal = Modifier.isFinal(srcEntryType.getModifiers());
        boolean isCopyByReference = copyByReferences.contains(srcEntryType);

        if (destEntryType.isAssignableFrom(srcEntryType) && isFinal && (isPrimitiveArray || isCopyByReference)) {
            return addArrayContentCopy(fieldMap, size, srcCollectionValue, destObj, destEntryType);
        } else if (isPrimitiveArray) {
            return addToPrimitiveArray(srcObj, fieldMap, size, srcCollectionValue, destObj, destEntryType);
        } else {
            List<?> list = Arrays.asList((Object[]) srcCollectionValue);
            List<?> returnList;
            if (!destEntryType.getName().equals(BASE_CLASS)) {
                returnList = addOrUpdateToList(srcObj, fieldMap, list, destObj, destEntryType);
            } else {
                returnList = addOrUpdateToList(srcObj, fieldMap, list, destObj, null);
            }
            return CollectionUtils.convertListToArray(returnList, destEntryType);
        }
    }

    private void mapFromIterateMethodFieldMap(Object srcObj, Object destObj, Object srcFieldValue,
            FieldMap fieldMapping) {
        // Iterate over the destFieldValue - iterating is fine unless we are mapping
        // in the other direction.
        // Verify that it is truly a collection if it is an iterator object turn it
        // into a List
        if (srcFieldValue instanceof Iterator) {
            srcFieldValue = IteratorUtils.toList((Iterator<?>) srcFieldValue);
        }
        if (srcFieldValue != null) {
            for (int i = 0; i < CollectionUtils.getLengthOfCollection(srcFieldValue); i++) {
                final Object value = CollectionUtils.getValueFromCollection(srcFieldValue, i);

                // map this value
                if (fieldMapping.getDestHintContainer() == null) {
                    MappingUtils.throwMappingException(
                            "<field type=\"iterate\"> must have a source or destination type hint");
                }

                Class<?> destinationHint = fieldMapping.getDestHintType(value.getClass());

                Object result = mapOrRecurseObject(srcObj, value, destinationHint, fieldMapping, destObj);

                if (value != null) {
                    writeDestinationValue(destObj, result, fieldMapping, srcObj);
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug(logMsgFactory.createFieldMappingSuccessMsg(srcObj.getClass(), destObj.getClass(),
                    fieldMapping.getSrcFieldName(), fieldMapping.getDestFieldName(), srcFieldValue, null,
                    fieldMapping.getClassMap().getMapId()));
        }
    }

    private Object addArrayContentCopy(FieldMap fieldMap, int size, Object srcCollectionValue, Object destObj,
            Class destEntryType) {
        Object result;
        Object field = fieldMap.getDestValue(destObj);
        int arraySize = 0;
        if (field == null) {
            result = Array.newInstance(destEntryType, size);
        } else {
            result = Array.newInstance(destEntryType, size + Array.getLength(field));
            arraySize = Array.getLength(field);
            System.arraycopy(field, 0, result, 0, arraySize);
        }
        System.arraycopy(srcCollectionValue, 0, result, arraySize, size);
        return result;
    }

    private Object addToPrimitiveArray(Object srcObj, FieldMap fieldMap, int size, Object srcCollectionValue,
            Object destObj, Class<?> destEntryType) {

        Object result;
        Object field = fieldMap.getDestValue(destObj);
        int arraySize = 0;
        if (field == null) {
            result = Array.newInstance(destEntryType, size);
        } else {
            result = Array.newInstance(destEntryType, size + Array.getLength(field));
            arraySize = Array.getLength(field);
            System.arraycopy(field, 0, result, 0, arraySize);
        }
        // primitive arrays are ALWAYS cumulative
        for (int i = 0; i < size; i++) {
            CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
            Object toValue;
            if (srcCollectionValue != null && copyByReferences.contains(srcCollectionValue.getClass())) {
                toValue = srcCollectionValue;
            } else {
                toValue = mapOrRecurseObject(srcObj, Array.get(srcCollectionValue, i), destEntryType, fieldMap,
                        destObj);
            }
            Array.set(result, arraySize, toValue);
            arraySize++;
        }
        return result;
    }

    private Object mapListToArray(Object srcObj, Collection<?> srcCollectionValue, FieldMap fieldMap,
            Object destObj) {
        Class destEntryType = fieldMap.getDestFieldType(destObj.getClass()).getComponentType();
        List list;
        if (!destEntryType.getName().equals(BASE_CLASS)) {
            list = addOrUpdateToList(srcObj, fieldMap, srcCollectionValue, destObj, destEntryType);
        } else {
            list = addOrUpdateToList(srcObj, fieldMap, srcCollectionValue, destObj);
        }
        return CollectionUtils.convertListToArray(list, destEntryType);
    }

    private List<?> mapListToList(Object srcObj, Collection<?> srcCollectionValue, FieldMap fieldMap,
            Object destObj) {
        return addOrUpdateToList(srcObj, fieldMap, srcCollectionValue, destObj);
    }

    private Set<?> addToSet(Object srcObj, FieldMap fieldMap, Collection<?> srcCollectionValue, Object destObj) {
        // create a list here so we can keep track of which elements we have mapped, and remove all others if removeOrphans = true
        Set<Object> mappedElements = new HashSet<>();

        LinkedHashSet<Object> result = new LinkedHashSet<>();
        // don't want to create the set if it already exists.
        Object field = fieldMap.getDestValue(destObj);
        if (field != null) {
            result.addAll((Collection<?>) field);
        }
        Object destValue;

        Class<?> destEntryType = null;
        Class<?> prevDestEntryType = null;
        for (Object srcValue : srcCollectionValue) {
            if (destEntryType == null || (fieldMap.getDestHintContainer() != null
                    && fieldMap.getDestHintContainer().hasMoreThanOneHint())) {
                destEntryType = determineCollectionItemType(fieldMap, destObj, srcValue, prevDestEntryType);
            }

            CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
            if (srcValue != null && copyByReferences.contains(srcValue.getClass())) {
                destValue = srcValue;
            } else {
                destValue = mapOrRecurseObject(srcObj, srcValue, destEntryType, fieldMap, destObj);
            }
            prevDestEntryType = destEntryType;

            if (RelationshipType.NON_CUMULATIVE.equals(fieldMap.getRelationshipType())
                    && result.contains(destValue)) {
                List<Object> resultAsList = new ArrayList<>(result);
                int index = resultAsList.indexOf(destValue);
                // perform an update if complex type - can't map strings
                Object obj = resultAsList.get(index);
                // make sure it is not a String
                if (!obj.getClass().isAssignableFrom(String.class)) {
                    mapToDestObject(null, srcValue, obj, false, fieldMap.getMapId());
                    mappedElements.add(obj);
                }
            } else {
                if (destValue != null || fieldMap.isDestMapNull()) {
                    result.add(destValue);
                }
                mappedElements.add(destValue);
            }
        }

        // If remove orphans - we only want to keep the objects we've mapped from the src collection
        // so we'll clear result and replace all entries with the ones in mappedElements
        if (fieldMap.isRemoveOrphans()) {
            result.clear();
            result.addAll(mappedElements);
        }

        if (field == null) {
            Class<? extends Set<?>> destSetType = (Class<? extends Set<?>>) fieldMap
                    .getDestFieldType(destObj.getClass());
            return CollectionUtils.createNewSet(destSetType, result);
        } else {
            // Bug #1822421 - Clear first so we don't end up with the removed orphans again
            ((Set) field).clear();
            ((Set) field).addAll(result);
            return (Set<?>) field;
        }
    }

    private List<?> addOrUpdateToList(Object srcObj, FieldMap fieldMap, Collection<?> srcCollectionValue,
            Object destObj, Class<?> destEntryType) {
        // create a Set here so we can keep track of which elements we have mapped, and remove all others if removeOrphans = true
        List<Object> mappedElements = new ArrayList<>();
        List result;
        // don't want to create the list if it already exists.
        // these maps are special cases which do not fall under what we are looking for
        Object field = fieldMap.getDestValue(destObj);
        result = prepareDestinationList(srcCollectionValue, field);

        Object destValue;
        Class<?> prevDestEntryType = null;
        for (Object srcValue : srcCollectionValue) {
            if (destEntryType == null || (fieldMap.getDestHintContainer() != null
                    && fieldMap.getDestHintContainer().hasMoreThanOneHint())) {
                destEntryType = determineCollectionItemType(fieldMap, destObj, srcValue, prevDestEntryType);
            }

            CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
            if (srcValue != null && copyByReferences.contains(srcValue.getClass())) {
                destValue = srcValue;
            } else {
                destValue = mapOrRecurseObject(srcObj, srcValue, destEntryType, fieldMap, destObj);
            }
            prevDestEntryType = destEntryType;

            if (RelationshipType.NON_CUMULATIVE.equals(fieldMap.getRelationshipType())
                    && result.contains(destValue)) {
                int index = result.indexOf(destValue);
                // perform an update if complex type - can't map strings
                Object obj = result.get(index);
                // make sure it is not a String
                if (obj != null && !obj.getClass().isAssignableFrom(String.class)) {
                    mapToDestObject(null, srcValue, obj, false, fieldMap.getMapId());
                    mappedElements.add(obj);
                }
            } else {
                // respect null mappings
                if (destValue != null || fieldMap.isDestMapNull()) {
                    result.add(destValue);
                }
                mappedElements.add(destValue);
            }
        }

        // If remove orphans - we only want to keep the objects we've mapped from the src collection
        if (fieldMap.isRemoveOrphans()) {
            removeOrphans(mappedElements, result);
        }

        return result;
    }

    private Class<?> determineCollectionItemType(FieldMap fieldMap, Object destObj, Object srcValue,
            Class<?> prevDestEntryType) {
        if (srcValue == null && fieldMap.getDestHintType(destObj.getClass()) != null) {
            // try to get a possible configured dest hint for the dest obj
            return fieldMap.getDestHintType(destObj.getClass());
        } else if (srcValue == null && prevDestEntryType != null) {
            // if we already evaluated the dest type, use it
            return prevDestEntryType;
        } else if (srcValue != null) {
            // if there's no dest hint for the dest obj, take the src hint
            return fieldMap.getDestHintType(srcValue.getClass());
        }
        throw new MappingException(
                "Unable to determine type for value '" + srcValue + "'. Use hints or generic collections.");
    }

    static void removeOrphans(Collection<?> mappedElements, List<Object> result) {
        for (Iterator<?> iterator = result.iterator(); iterator.hasNext();) {
            Object object = iterator.next();
            if (!mappedElements.contains(object)) {
                iterator.remove();
            }
        }
        for (Object object : mappedElements) {
            if (!result.contains(object)) {
                result.add(object);
            }
        }
    }

    static List<?> prepareDestinationList(Collection<?> srcCollectionValue, Object field) {
        if (field == null) {
            return new ArrayList<>(srcCollectionValue.size());
        } else {
            if (CollectionUtils.isList(field.getClass())) {
                return (List<?>) field;
            } else if (CollectionUtils.isArray(field.getClass())) {
                return new ArrayList<>(Arrays.asList((Object[]) field));
            } else { // assume it is neither - safest way is to create new List
                return new ArrayList<>(srcCollectionValue.size());
            }
        }
    }

    private List<?> addOrUpdateToList(Object srcObj, FieldMap fieldMap, Collection<?> srcCollectionValue,
            Object destObj) {
        return addOrUpdateToList(srcObj, fieldMap, srcCollectionValue, destObj, null);
    }

    private Object mapSetToArray(Object srcObj, Collection<?> srcCollectionValue, FieldMap fieldMap,
            Object destObj) {
        return mapListToArray(srcObj, srcCollectionValue, fieldMap, destObj);
    }

    private List<?> mapArrayToList(Object srcObj, Object srcCollectionValue, FieldMap fieldMap, Object destObj) {
        Class<?> destEntryType;
        if (fieldMap.getDestHintContainer() != null) {
            destEntryType = fieldMap.getDestHintContainer().getHint();
        } else {
            destEntryType = srcCollectionValue.getClass().getComponentType();
        }
        List<?> srcValueList;
        if (CollectionUtils.isPrimitiveArray(srcCollectionValue.getClass())) {
            srcValueList = CollectionUtils.convertPrimitiveArrayToList(srcCollectionValue);
        } else {
            srcValueList = Arrays.asList((Object[]) srcCollectionValue);
        }
        return addOrUpdateToList(srcObj, fieldMap, srcValueList, destObj, destEntryType);
    }

    private void writeDestinationValue(Object destObj, Object destFieldValue, FieldMap fieldMap, Object srcObj) {
        boolean bypass = false;
        // don't map null to dest field if map-null="false"
        if (destFieldValue == null && !fieldMap.isDestMapNull()) {
            bypass = true;
        }

        // don't map "" to dest field if map-empty-string="false"
        if (destFieldValue != null && !fieldMap.isDestMapEmptyString()
                && destFieldValue.getClass().equals(String.class) && StringUtils.isEmpty((String) destFieldValue)) {
            bypass = true;
        }

        // trim string value if trim-strings="true"
        if (destFieldValue != null && fieldMap.isTrimStrings() && destFieldValue.getClass().equals(String.class)) {
            destFieldValue = ((String) destFieldValue).trim();
        }

        if (!bypass) {
            eventManager.on(new DefaultEvent(EventTypes.MAPPING_PRE_WRITING_DEST_VALUE, fieldMap.getClassMap(),
                    fieldMap, srcObj, destObj, destFieldValue));

            fieldMap.writeDestValue(destObj, destFieldValue);

            eventManager.on(new DefaultEvent(EventTypes.MAPPING_POST_WRITING_DEST_VALUE, fieldMap.getClassMap(),
                    fieldMap, srcObj, destObj, destFieldValue));
        }
    }

    private Object mapUsingCustomConverterInstance(CustomConverter converterInstance, Class<?> srcFieldClass,
            Object srcFieldValue, Class<?> destFieldClass, Object existingDestFieldValue, FieldMap fieldMap,
            boolean topLevel) {

        //1792048 - If map-null = "false" and src value is null, then don't even invoke custom converter
        if (srcFieldValue == null && !fieldMap.isDestMapNull()) {
            return null;
        }

        if (converterInstance instanceof MapperAware) {
            ((MapperAware) converterInstance).setMapper(this);
        }

        // TODO Remove code duplication
        Object result;
        if (converterInstance instanceof ConfigurableCustomConverter) {
            ConfigurableCustomConverter theConverter = (ConfigurableCustomConverter) converterInstance;

            // Converter could be not configured for this particular case
            if (fieldMap != null) {
                String param = fieldMap.getCustomConverterParam();
                theConverter.setParameter(param);
            }

            // if this is a top level mapping the destObj is the highest level
            // mapping...not a recursive mapping
            if (topLevel) {
                result = theConverter.convert(existingDestFieldValue, srcFieldValue, destFieldClass, srcFieldClass);
            } else {
                Object existingValue = getExistingValue(fieldMap, existingDestFieldValue, destFieldClass);
                result = theConverter.convert(existingValue, srcFieldValue, destFieldClass, srcFieldClass);
            }
        } else {
            // if this is a top level mapping the destObj is the highest level
            // mapping...not a recursive mapping
            if (topLevel) {
                result = converterInstance.convert(existingDestFieldValue, srcFieldValue, destFieldClass,
                        srcFieldClass);
            } else {
                Object existingValue = getExistingValue(fieldMap, existingDestFieldValue, destFieldClass);
                result = converterInstance.convert(existingValue, srcFieldValue, destFieldClass, srcFieldClass);
            }
        }

        return result;
    }

    // TODO: possibly extract this to a separate class

    private Object mapUsingCustomConverter(Class<?> customConverterClass, Class<?> srcFieldClass,
            Object srcFieldValue, Class<?> destFieldClass, Object existingDestFieldValue, FieldMap fieldMap,
            boolean topLevel) {
        CustomConverter converterInstance = null;
        // search our injected customconverters for a match
        if (customConverterObjects != null) {
            for (CustomConverter customConverterObject : customConverterObjects) {
                if (customConverterClass.isInstance(customConverterObject)) {
                    // we have a match
                    converterInstance = customConverterObject;
                }
            }
        }
        // if converter object instances were not injected, then create new instance
        // of the converter for each conversion
        // TODO : Should we really create it each time?
        if (converterInstance == null) {
            converterInstance = (CustomConverter) ReflectionUtils.newInstance(customConverterClass);
        }
        return mapUsingCustomConverterInstance(converterInstance, srcFieldClass, srcFieldValue, destFieldClass,
                existingDestFieldValue, fieldMap, topLevel);
    }

    private Collection<ClassMap> checkForSuperTypeMapping(Class<?> srcClass, Class<?> destClass) {
        // Check cache first
        Object cacheKey = CacheKeyFactory.createKey(destClass, srcClass);
        Collection<ClassMap> cachedResult = (Collection<ClassMap>) superTypeCache.get(cacheKey);
        if (cachedResult != null) {
            return cachedResult;
        }

        // If no existing cache entry is found, determine super type mappings.
        // Recursively walk the inheritance hierarchy.
        List<ClassMap> superClasses = new ArrayList<>();
        // Need to call getRealSuperclass because proxied data objects will not return correct
        // superclass when using basic reflection

        List<Class<?>> superSrcClasses = MappingUtils.getSuperClassesAndInterfaces(srcClass, beanContainer);
        List<Class<?>> superDestClasses = MappingUtils.getSuperClassesAndInterfaces(destClass, beanContainer);

        // add the actual classes to check for mappings between the original and the opposite
        // super classes
        superSrcClasses.add(0, srcClass);
        superDestClasses.add(0, destClass);

        for (Class<?> superSrcClass : superSrcClasses) {
            for (Class<?> superDestClass : superDestClasses) {
                if (!(superSrcClass.equals(srcClass) && superDestClass.equals(destClass))) {
                    checkForClassMapping(superSrcClass, superClasses, superDestClass);
                }
            }
        }

        Collections.reverse(superClasses); // Done so base classes are processed first

        superTypeCache.put(cacheKey, superClasses);

        return superClasses;
    }

    private void checkForClassMapping(Class<?> srcClass, List<ClassMap> superClasses, Class<?> superDestClass) {
        ClassMap srcClassMap = classMappings.find(srcClass, superDestClass);
        if (srcClassMap != null) {
            superClasses.add(srcClassMap);
        }
    }

    private void processSuperTypeMapping(Collection<ClassMap> superClasses, Object srcObj, Object destObj,
            List<String> mappedParentFields, String mapId) {
        for (ClassMap map : superClasses) {
            map(map, srcObj, destObj, true, mappedParentFields, mapId);
            for (FieldMap fieldMapping : map.getFieldMaps()) {
                String key = MappingUtils.getMappedParentFieldKey(destObj, fieldMapping);
                mappedParentFields.add(key);
            }
        }
    }

    private static Object getExistingValue(FieldMap fieldMap, Object destObj, Class<?> destFieldType) {
        // verify that the dest obj is not null
        if (destObj == null) {
            return null;
        }
        // call the getXX method to see if the field is already instantiated
        Object result = fieldMap.getDestValue(destObj);

        // When we are recursing through a list we need to make sure that we are not
        // in the list
        // by checking the destFieldType
        if (result != null) {
            if (CollectionUtils.isList(result.getClass()) || CollectionUtils.isArray(result.getClass())
                    || CollectionUtils.isSet(result.getClass()) || MappingUtils.isSupportedMap(result.getClass())) {
                if (!CollectionUtils.isList(destFieldType) && !CollectionUtils.isArray(destFieldType)
                        && !CollectionUtils.isSet(destFieldType) && !MappingUtils.isSupportedMap(destFieldType)) {
                    // this means the getXX field is a List but we are actually trying to
                    // map one of its elements
                    result = null;
                }
            }
        }
        return result;
    }

    private ClassMap getClassMap(Class<?> srcClass, Class<?> destClass, String mapId) {
        ClassMap mapping = classMappings.find(srcClass, destClass, mapId);

        if (mapping == null) {
            mapping = classMappings.find(destClass, srcClass, null);
            if (mapping != null && MappingDirection.ONE_WAY == mapping.getType()) {
                //Does the opposite mapping exist, but its only a ONE_WAY?
                //Then create an empty mapping, i.e.: an object instance will be returned by the mapper, but no fields will be set
                mapping = classMapBuilder.createDefaultClassMap(globalConfiguration, srcClass, destClass, false);
                classMappings.addDefault(srcClass, destClass, mapping);
            } else {
                // If mapping not found in existing custom mapping collection,
                // create default as an explicit mapping must not exist.
                // The create default class map method will also add all default
                // mappings that it can determine.
                mapping = classMapBuilder.createDefaultClassMap(globalConfiguration, srcClass, destClass);
                classMappings.addDefault(srcClass, destClass, mapping);
            }
        }

        return mapping;
    }

}