org.springframework.data.document.mongodb.convert.SimpleMongoConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.document.mongodb.convert.SimpleMongoConverter.java

Source

/*
 * Copyright 2010-2011 the original author or authors.
 *
 * 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 org.springframework.data.document.mongodb.convert;

import static org.springframework.data.document.mongodb.convert.ObjectIdConverters.*;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.types.CodeWScope;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.document.mongodb.MongoPropertyDescriptors.MongoPropertyDescriptor;
import org.springframework.data.document.mongodb.mapping.MongoPersistentEntity;
import org.springframework.data.document.mongodb.mapping.MongoPersistentProperty;
import org.springframework.data.document.mongodb.mapping.SimpleMongoMappingContext;
import org.springframework.data.mapping.model.MappingContext;
import org.springframework.util.Assert;
import org.springframework.util.comparator.CompoundComparator;

/**
 * Basic {@link MongoConverter} implementation to convert between domain classes and {@link DBObject}s.
 *
 * @author Mark Pollack
 * @author Thomas Risberg
 * @author Oliver Gierke
 */
public class SimpleMongoConverter extends AbstractMongoConverter implements InitializingBean {

    private static final Log LOG = LogFactory.getLog(SimpleMongoConverter.class);
    @SuppressWarnings("unchecked")
    private static final List<Class<?>> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class,
            DBObject.class);
    private static final Set<String> SIMPLE_TYPES;

    static {
        Set<String> basics = new HashSet<String>();
        basics.add(boolean.class.getName());
        basics.add(long.class.getName());
        basics.add(short.class.getName());
        basics.add(int.class.getName());
        basics.add(byte.class.getName());
        basics.add(float.class.getName());
        basics.add(double.class.getName());
        basics.add(char.class.getName());
        basics.add(Boolean.class.getName());
        basics.add(Long.class.getName());
        basics.add(Short.class.getName());
        basics.add(Integer.class.getName());
        basics.add(Byte.class.getName());
        basics.add(Float.class.getName());
        basics.add(Double.class.getName());
        basics.add(Character.class.getName());
        basics.add(String.class.getName());
        basics.add(java.util.Date.class.getName());
        // basics.add(Time.class.getName());
        // basics.add(Timestamp.class.getName());
        // basics.add(java.sql.Date.class.getName());
        // basics.add(BigDecimal.class.getName());
        // basics.add(BigInteger.class.getName());
        basics.add(Locale.class.getName());
        // basics.add(Calendar.class.getName());
        // basics.add(GregorianCalendar.class.getName());
        // basics.add(java.util.Currency.class.getName());
        // basics.add(TimeZone.class.getName());
        // basics.add(Object.class.getName());
        basics.add(Class.class.getName());
        // basics.add(byte[].class.getName());
        // basics.add(Byte[].class.getName());
        // basics.add(char[].class.getName());
        // basics.add(Character[].class.getName());
        // basics.add(Blob.class.getName());
        // basics.add(Clob.class.getName());
        // basics.add(Serializable.class.getName());
        // basics.add(URI.class.getName());
        // basics.add(URL.class.getName());
        basics.add(DBRef.class.getName());
        basics.add(Pattern.class.getName());
        basics.add(CodeWScope.class.getName());
        basics.add(ObjectId.class.getName());
        basics.add(Enum.class.getName());
        SIMPLE_TYPES = Collections.unmodifiableSet(basics);
    }

    private final GenericConversionService conversionService;
    private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;

    /**
     * Creates a {@link SimpleMongoConverter}.
     */
    public SimpleMongoConverter() {
        this.conversionService = ConversionServiceFactory.createDefaultConversionService();
        this.conversionService.removeConvertible(Object.class, String.class);
        this.mappingContext = new SimpleMongoMappingContext();
    }

    /* (non-Javadoc)
        * @see org.springframework.data.document.mongodb.convert.MongoConverter#getMappingContext()
        */
    public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
        return mappingContext;
    }

    /**
     * Initializes additional converters that handle {@link ObjectId} conversion. Will register converters for supported
     * id types if none are registered for those conversion already. {@link GenericConversionService} is configured.
     */
    private void initializeConverters() {

        if (!conversionService.canConvert(ObjectId.class, String.class)) {
            conversionService.addConverter(ObjectIdToStringConverter.INSTANCE);
        }
        if (!conversionService.canConvert(String.class, ObjectId.class)) {
            conversionService.addConverter(StringToObjectIdConverter.INSTANCE);
        }
        if (!conversionService.canConvert(ObjectId.class, BigInteger.class)) {
            conversionService.addConverter(ObjectIdToBigIntegerConverter.INSTANCE);
        }
        if (!conversionService.canConvert(BigInteger.class, ObjectId.class)) {
            conversionService.addConverter(BigIntegerToObjectIdConverter.INSTANCE);
        }
    }

    /**
     * Add custom {@link Converter} or {@link ConverterFactory} instances to be used that will take presidence over
     * using object traversal to convert and object to/from DBObject
     *
     * @param converters
     */
    public void setConverters(Set<?> converters) {
        for (Object converter : converters) {
            boolean added = false;
            if (converter instanceof Converter) {
                this.conversionService.addConverter((Converter<?, ?>) converter);
                added = true;
            }
            if (converter instanceof ConverterFactory) {
                this.conversionService.addConverterFactory((ConverterFactory<?, ?>) converter);
                added = true;
            }
            if (!added) {
                throw new IllegalArgumentException(
                        "Given set contains element that is neither Converter nor ConverterFactory!");
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.data.document.mongodb.MongoWriter#write(java.lang.Object, com.mongodb.DBObject)
     */
    @SuppressWarnings("rawtypes")
    public void write(Object obj, DBObject dbo) {

        MongoBeanWrapper beanWrapper = createWrapper(obj, false);
        for (MongoPropertyDescriptor descriptor : beanWrapper.getDescriptors()) {
            if (descriptor.isMappable()) {
                Object value = beanWrapper.getValue(descriptor);

                if (value == null) {
                    continue;
                }

                String keyToUse = descriptor.getKeyToMap();
                if (descriptor.isEnum()) {
                    writeValue(dbo, keyToUse, ((Enum) value).name());
                } else if (descriptor.isIdProperty() && descriptor.isOfIdType()) {
                    if (value instanceof String && ObjectId.isValid((String) value)) {
                        try {
                            writeValue(dbo, keyToUse, conversionService.convert(value, ObjectId.class));
                        } catch (ConversionFailedException iae) {
                            LOG.warn("Unable to convert the String " + value + " to an ObjectId");
                            writeValue(dbo, keyToUse, value);
                        }
                    } else {
                        // we can't convert this id - use as is
                        writeValue(dbo, keyToUse, value);
                    }
                } else {
                    writeValue(dbo, keyToUse, value);
                }
            } else {
                if (!"class".equals(descriptor.getName())) {
                    LOG.debug("Skipping property " + descriptor.getName() + " as it's not a mappable one.");
                }
            }
        }
    }

    /**
     * Writes the given value to the given {@link DBObject}. Will skip {@literal null} values.
     *
     * @param dbo
     * @param keyToUse
     * @param value
     */
    private void writeValue(DBObject dbo, String keyToUse, Object value) {

        if (!isSimpleType(value.getClass())) {
            writeCompoundValue(dbo, keyToUse, value);
        } else {
            dbo.put(keyToUse, value);
        }
    }

    /**
     * Writes the given {@link CompoundComparator} value to the given {@link DBObject}.
     *
     * @param dbo
     * @param keyToUse
     * @param value
     */
    @SuppressWarnings("unchecked")
    private void writeCompoundValue(DBObject dbo, String keyToUse, Object value) {
        if (value instanceof Map) {
            writeMap(dbo, keyToUse, (Map<String, Object>) value);
            return;
        }
        if (value instanceof Collection) {
            // Should write a collection!
            writeArray(dbo, keyToUse, ((Collection<Object>) value).toArray());
            return;
        }
        if (value instanceof Object[]) {
            // Should write an array!
            writeArray(dbo, keyToUse, (Object[]) value);
            return;
        }

        Class<?> customTargetType = getCustomTargetType(value);
        if (customTargetType != null) {
            dbo.put(keyToUse, conversionService.convert(value, customTargetType));
            return;
        }

        DBObject nestedDbo = new BasicDBObject();
        write(value, nestedDbo);
        dbo.put(keyToUse, nestedDbo);
    }

    /**
     * Returns whether the {@link ConversionService} has a custom {@link Converter} registered that can convert the given
     * object into one of the types supported by MongoDB.
     *
     * @param obj
     * @return
     */
    private Class<?> getCustomTargetType(Object obj) {

        for (Class<?> mongoType : MONGO_TYPES) {
            if (conversionService.canConvert(obj.getClass(), mongoType)) {
                return mongoType;
            }
        }
        return null;
    }

    /**
     * Writes the given {@link Map} to the given {@link DBObject}.
     *
     * @param dbo
     * @param mapKey
     * @param map
     */
    protected void writeMap(DBObject dbo, String mapKey, Map<String, Object> map) {
        // TODO support non-string based keys as long as there is a Spring Converter obj->string and (optionally)
        // string->obj
        DBObject dboToPopulate = null;

        // TODO - Does that make sense? If we create a new object here it's content will never make it out of this
        // method
        if (mapKey != null) {
            dboToPopulate = new BasicDBObject();
        } else {
            dboToPopulate = dbo;
        }
        if (map != null) {
            for (Entry<String, Object> entry : map.entrySet()) {

                Object entryValue = entry.getValue();
                String entryKey = entry.getKey();

                if (!isSimpleType(entryValue.getClass())) {
                    writeCompoundValue(dboToPopulate, entryKey, entryValue);
                } else {
                    dboToPopulate.put(entryKey, entryValue);
                }
            }
            dbo.put(mapKey, dboToPopulate);
        }
    }

    /**
     * Writes the given array to the given {@link DBObject}.
     *
     * @param dbo
     * @param keyToUse
     * @param array
     */
    protected void writeArray(DBObject dbo, String keyToUse, Object[] array) {
        Object[] dboValues;
        if (array != null) {
            dboValues = new Object[array.length];
            int i = 0;
            for (Object o : array) {
                if (!isSimpleType(o.getClass())) {
                    DBObject dboValue = new BasicDBObject();
                    write(o, dboValue);
                    dboValues[i] = dboValue;
                } else {
                    dboValues[i] = o;
                }
                i++;
            }
            dbo.put(keyToUse, dboValues);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.data.document.mongodb.MongoReader#read(java.lang.Class, com.mongodb.DBObject)
     */
    public <S> S read(Class<S> clazz, DBObject source) {

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

        Assert.notNull(clazz, "Mapped class was not specified");
        S target = BeanUtils.instantiateClass(clazz);
        MongoBeanWrapper bw = new MongoBeanWrapper(target, conversionService, true);

        for (MongoPropertyDescriptor descriptor : bw.getDescriptors()) {
            String keyToUse = descriptor.getKeyToMap();
            if (source.containsField(keyToUse)) {
                if (descriptor.isMappable()) {
                    Object value = source.get(keyToUse);
                    if (!isSimpleType(value.getClass())) {
                        if (value instanceof Object[]) {
                            bw.setValue(descriptor,
                                    readCollection(descriptor, Arrays.asList((Object[]) value)).toArray());
                        } else if (value instanceof BasicDBList) {
                            bw.setValue(descriptor, readCollection(descriptor, (BasicDBList) value));
                        } else if (value instanceof DBObject) {
                            bw.setValue(descriptor, readCompoundValue(descriptor, (DBObject) value));
                        } else {
                            LOG.warn("Unable to map compound DBObject field " + keyToUse + " to property "
                                    + descriptor.getName()
                                    + ".  The field value should have been a 'DBObject.class' but was "
                                    + value.getClass().getName());
                        }
                    } else {
                        bw.setValue(descriptor, value);
                    }
                } else {
                    LOG.warn("Unable to map DBObject field " + keyToUse + " to property " + descriptor.getName()
                            + ".  Skipping.");
                }
            }
        }

        return target;
    }

    /**
     * Reads the given collection values (that are {@link DBObject}s potentially) into a {@link Collection} of domain
     * objects.
     *
     * @param descriptor
     * @param values
     * @return
     */
    private Collection<Object> readCollection(MongoPropertyDescriptor descriptor, Collection<?> values) {

        Class<?> targetCollectionType = descriptor.getPropertyType();
        boolean targetIsArray = targetCollectionType.isArray();

        @SuppressWarnings("unchecked")
        Collection<Object> result = targetIsArray ? new ArrayList<Object>(values.size())
                : CollectionFactory.createCollection(targetCollectionType, values.size());

        for (Object o : values) {
            if (o instanceof DBObject) {
                Class<?> type;
                if (targetIsArray) {
                    type = targetCollectionType.getComponentType();
                } else {
                    type = getGenericParameters(descriptor.getTypeToSet()).get(0);
                }
                result.add(read(type, (DBObject) o));
            } else {
                result.add(o);
            }
        }

        return result;
    }

    /**
     * Reads a compound value from the given {@link DBObject} for the given property.
     *
     * @param pd
     * @param dbo
     * @return
     */
    private Object readCompoundValue(MongoPropertyDescriptor pd, DBObject dbo) {

        Assert.isTrue(!pd.isCollection(), "Collections not supported!");

        if (pd.isMap()) {
            return readMap(pd, dbo, getGenericParameters(pd.getTypeToSet()).get(1));
        } else {
            return read(pd.getPropertyType(), dbo);
        }
    }

    /**
     * Create a {@link Map} instance. Will return a {@link HashMap} by default. Subclasses might want to override this
     * method to use a custom {@link Map} implementation.
     *
     * @return
     */
    protected Map<String, Object> createMap() {
        return new HashMap<String, Object>();
    }

    /**
     * Reads every key/value pair from the {@link DBObject} into a {@link Map} instance.
     *
     * @param pd
     * @param dbo
     * @param targetType
     * @return
     */
    protected Map<?, ?> readMap(MongoPropertyDescriptor pd, DBObject dbo, Class<?> targetType) {
        Map<String, Object> map = createMap();
        for (String key : dbo.keySet()) {
            Object value = dbo.get(key);
            if (!isSimpleType(value.getClass())) {
                map.put(key, read(targetType, (DBObject) value));
                // Can do some reflection tricks here -
                // throw new RuntimeException("User types not supported yet as values for Maps");
            } else {
                map.put(key, conversionService.convert(value, targetType));
            }
        }
        return map;
    }

    protected static boolean isSimpleType(Class<?> propertyType) {
        if (propertyType == null) {
            return false;
        }
        if (propertyType.isArray()) {
            return isSimpleType(propertyType.getComponentType());
        }
        return SIMPLE_TYPES.contains(propertyType.getName());
    }

    /**
     * Callback to allow customizing creation of a {@link MongoBeanWrapper}.
     *
     * @param target         the target object to wrap
     * @param fieldAccess whether to use field access or property access
     * @return
     */
    protected MongoBeanWrapper createWrapper(Object target, boolean fieldAccess) {

        return new MongoBeanWrapper(target, conversionService, fieldAccess);
    }

    public List<Class<?>> getGenericParameters(Type genericParameterType) {

        List<Class<?>> actualGenericParameterTypes = new ArrayList<Class<?>>();

        if (genericParameterType instanceof ParameterizedType) {
            ParameterizedType aType = (ParameterizedType) genericParameterType;
            Type[] parameterArgTypes = aType.getActualTypeArguments();
            for (Type parameterArgType : parameterArgTypes) {
                if (parameterArgType instanceof GenericArrayType) {
                    Class<?> arrayType = (Class<?>) ((GenericArrayType) parameterArgType).getGenericComponentType();
                    actualGenericParameterTypes.add(Array.newInstance(arrayType, 0).getClass());
                } else {
                    if (parameterArgType instanceof ParameterizedType) {
                        ParameterizedType paramTypeArgs = (ParameterizedType) parameterArgType;
                        actualGenericParameterTypes.add((Class<?>) paramTypeArgs.getRawType());
                    } else {
                        if (parameterArgType instanceof TypeVariable) {
                            throw new RuntimeException(
                                    "Can not map " + ((TypeVariable<?>) parameterArgType).getName());
                        } else {
                            if (parameterArgType instanceof Class) {
                                actualGenericParameterTypes.add((Class<?>) parameterArgType);
                            } else {
                                throw new RuntimeException("Can not map " + parameterArgType);
                            }
                        }
                    }
                }
            }
        }

        return actualGenericParameterTypes;
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.convert.MongoConverter#convertObjectId(org.bson.types.ObjectId, java.lang.Class)
     */
    public <T> T convertObjectId(ObjectId id, Class<T> targetType) {
        return conversionService.convert(id, targetType);
    }

    /* (non-Javadoc)
     * @see org.springframework.data.document.mongodb.convert.MongoConverter#convertObjectId(java.lang.Object)
     */
    public ObjectId convertObjectId(Object id) {
        return conversionService.convert(id, ObjectId.class);
    }

    /* (non-Javadoc)
        * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
        */
    public void afterPropertiesSet() {
        initializeConverters();
    }
}