Java tutorial
/* * Copyright (c) 2014 Alexander Gulko <kirhog at gmail dot com>. * * 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.fastmongo.odm.dbobject.mapping.core; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import org.fastmongo.odm.dbobject.mapping.support.classname.ClassNameResolver; import org.fastmongo.odm.mapping.core.*; import org.fastmongo.odm.mapping.support.converter.ConversionService; import org.fastmongo.odm.mapping.support.linkresolver.LinkResolver; import org.fastmongo.odm.mapping.support.linkresolver.LinkResolverFactory; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import static org.fastmongo.odm.dbobject.mapping.core.ConverterHelper.*; /** * Converts {@link com.mongodb.DBObject} to domain objects. * * @author Alexander Gulko */ public class DbObjectToDomainConverter { /** * Resolves links to internal/external documents. */ private LinkResolverFactory linkResolverFactory; /** * A service that converts simple values (e.g. BigDecimal to String). */ private ConversionService conversionService; /** * Used to map between DBObjects and corresponding domain class. */ private ClassNameResolver classNameResolver; /** * A prefix for full class names to reduce the length of strings in mongo. */ private String classPrefix; /** * Creates {@link java.util.Set}s with the predefined size. */ private SetFactory setFactory; /** * Creates {@link java.util.List}s with the predefined size. */ private ListFactory listFactory; /** * Constructor with the required dependencies. */ public DbObjectToDomainConverter(LinkResolverFactory linkResolverFactory, ConversionService conversionService, ClassNameResolver classNameResolver) { this.linkResolverFactory = linkResolverFactory; this.conversionService = conversionService; this.classNameResolver = classNameResolver; setFactory = new SetFactory(); listFactory = new ListFactory(); } /** * Converts the given document to object of the specified class. * * @param root the root document (nullable). * @param clazz the document class. * @return the converted object or <tt>null</tt> if root is <tt>null</tt>. */ @SuppressWarnings("unchecked") public <T> T fromDbObject(DBObject root, Class<?> clazz) { if (root == null) { return null; } String className = classNameResolver.getClassName(clazz, root, classPrefix); T obj = newInstance(className); Context context = createContext(root, clazz); fillObject(obj, root, context); return obj; } /** * Creates context for the given root documents and the corresponding collection class. * * @param root the root document. * @param collectionClass the document class. * @return the context for the root document. */ private Context createContext(DBObject root, Class<?> collectionClass) { LinkResolver linkResolver = linkResolverFactory.create(root, collectionClass); return new Context(linkResolver); } /** * Fills the given object with the {@link com.mongodb.DBObject} using the provided context. * * @param db the document which represents the domain object. * @param obj the domain object. * @param context the context for this operation, bounded to the root document. */ private void fillObject(final Object obj, final DBObject db, Context context) { for (Field field : getClassFields(obj.getClass())) { processField(field, obj, db, context); } } /** * Converts mongo value for the given object field into the corresponding field value. * * @param field the domain object field. * @param obj the domain object. * @param db the document which represents the domain object. * @param context the context for this operation, bounded to the root document. */ @SuppressWarnings("unchecked") private void processField(Field field, Object obj, DBObject db, Context context) { Object value = db.get(field.getName()); if (value == null) { return; } Class<?> dbValueType = value.getClass(); Class<?> fieldType = field.getType(); if (isCollection(fieldType)) { if (Set.class.isAssignableFrom(fieldType)) { processSet(field, obj, (List<Object>) value, context); } else { processList(field, obj, (List<Object>) value, context); } } else if (isMap(fieldType)) { Map<String, Object> map = new LinkedHashMap<>(); setFieldValue(field, obj, map); fillMap(map, (DBObject) value, ConverterHelper.getFieldGenericTypes(field)[1], context); } else if (!isMap(dbValueType)) { setFieldValue(field, obj, toSimpleType(fieldType, value)); } else { DBObject fieldDbValue = (DBObject) value; if (fieldDbValue.containsField(LINK_KEY)) { processLink(field, obj, fieldDbValue, context); } else { Object fieldValue = newInstance( classNameResolver.getClassName(field.getGenericType(), fieldDbValue, classPrefix)); setFieldValue(field, obj, fieldValue); fillObject(fieldValue, fieldDbValue, context); } } } /** * Checks if the class is-a {@link java.util.Map} class. * * @param type the class to check. * @return <tt>true</tt> if the class is-a Map. */ private static boolean isMap(Class<?> type) { return Map.class.isAssignableFrom(type); } /** * Checks if the class is-a {@link java.util.Collection} class. * * @param type the class to check. * @return <tt>true</tt> if the class is-a Collection. */ private static boolean isCollection(Class<?> type) { return Collection.class.isAssignableFrom(type); } /** * Converts the given mongo value to a value of the target type. * * @param targetType the target type of value. * @param dbValue the mongo value. * @return the value with the target type. */ private Object toSimpleType(Class targetType, Object dbValue) { if (Enum.class.isAssignableFrom(targetType)) { return conversionService.convertStringToEnum(targetType, (String) dbValue); } else if (conversionService.canConvert(targetType)) { return conversionService.convert(dbValue, dbValue.getClass(), targetType); } else { return dbValue; } } /** * Converts list of mongo values to the {@link java.util.List} for the given field. * * @param field the domain object field with {@link java.util.List} type. * @param obj the domain object. * @param dbList the list of mongo values. * @param context the context for this operation, bounded to the root document. */ private void processList(Field field, Object obj, List<Object> dbList, Context context) { processCollection(field, obj, dbList, listFactory, context); } /** * Converts list of mongo values to the {@link java.util.Set} for the given field. * * @param field the domain object field with {@link java.util.Set} type. * @param obj the domain object. * @param dbList the list of mongo values. * @param context the context for this operation, bounded to the root document. */ private void processSet(Field field, Object obj, List<Object> dbList, Context context) { processCollection(field, obj, dbList, setFactory, context); } /** * Creates collection proxy to convert list of mongo values * to one of {@link java.util.Collection} implementation for the given field later. * * @param field the domain object field with {@link java.util.Collection} type. * @param obj the domain object. * @param dbList the list of mongo values. * @param factory the factory to create appropriate {@link java.util.Collection} implementation. * @param context the context for this operation, bounded to the root document. */ private void processCollection(Field field, Object obj, List<Object> dbList, CollectionFactory factory, Context context) { final Class<?> elementType = (Class<?>) ConverterHelper.getFieldGenericTypes(field)[0]; Collection proxy = ConverterHelper.newProxy(factory.getCollectionClass(), new DefaultCollectionLoader(dbList, factory, elementType, context), new FieldReplacer(field, obj)); setFieldValue(field, obj, proxy); } /** * Fills collection of elements with the given type using provided mongo values. * * @param collection the collection of elements. * @param dbList the list of mongo values. * @param elementType the type of elements in the result collection. * @param context the context for this operation, bounded to the root document. */ private void fillCollection(Collection<Object> collection, List<Object> dbList, Class<?> elementType, Context context) { // try to bulk load the whole collection instead of loading inside a loop if (tryLoadAndFillEntireCollection(collection, dbList, elementType, context)) { // if we successfully loaded and converted collection then return return; } // otherwise convert each element in the loop for (Object dbObject : dbList) { if (dbObject instanceof DBObject) { DBObject db = (DBObject) dbObject; String className = classNameResolver.getClassName(elementType, db, classPrefix); if (className != null) { String link = (String) db.get(LINK_KEY); if (link != null) { final Object id = db.get(ID_KEY); final Class<?> clazz = loadClass(className); collection.add(loadObject(id, clazz, context)); } else { Object element = newInstance(className); fillObject(element, db, context); collection.add(element); } } else { collection.add(db.toMap()); } } else { collection.add(toSimpleType(elementType, dbObject)); } } } /** * Tries to load collection of external documents in one request to mongo. * * @param collection the collection of elements. * @param dbList the list of mongo values. * @param elementType the type of elements in the result collection. * @param context the context for this operation, bounded to the root document. * @return <tt>true</tt> if collection was loaded and converted, otherwise <tt>false</tt>. */ private boolean tryLoadAndFillEntireCollection(Collection<Object> collection, List<Object> dbList, Class<?> elementType, Context context) { if (!dbList.isEmpty()) { if (dbList.get(0) instanceof DBObject) { DBObject db = (DBObject) dbList.get(0); String link = (String) db.get(LINK_KEY); if (link != null) { String elementClassName = classNameResolver.getClassName(elementType, db, classPrefix); Class<?> elementClass = loadClass(elementClassName); // Mongo collection fillExternalDocumentCollection(collection, dbList, elementType, elementClass, context); return true; // load links from external collection and return } } } return false; } /** * Loads and fills top-level domain objects (that are root documents in a separate collection) * in the given domain objects collection. * * @param collection the domain objects collection. * @param dbList the collection of links to the corresponding root documents * @param collectionElementType the type of domain objects in the collection (usually an interface). * @param elementConcreteClass the concrete class of domain objects in the collection. * @param context the context for this operation, bounded to the root document. */ @SuppressWarnings("unchecked") private void fillExternalDocumentCollection(Collection<Object> collection, List<Object> dbList, Class<?> collectionElementType, Class<?> elementConcreteClass, Context context) { for (DBObject db : context.linkResolver.resolveExternalLinks(elementConcreteClass, getIds(dbList))) { String className = classNameResolver.getClassName(collectionElementType, db, classPrefix); final Class<?> clazz = loadClass(className); Object model = fromDbObject(db, clazz); collection.add(model); } } /** * Fills the result <tt>map</tt> with data from <tt>dbMap</tt> (JSON object from MongoDB). * * @param map the result map. * @param dbMap the DBObject. * @param mapValueType the class of values in the result map, determined from the instance variable * that holds this map. */ private void fillMap(final Map<String, Object> map, DBObject dbMap, Type mapValueType, Context context) { for (String key : dbMap.keySet()) { Object dbValue = dbMap.get(key); key = unescape(key); Object objValue; if (dbValue instanceof BasicDBObject) { objValue = fillMapObjectValue(map, mapValueType, key, (BasicDBObject) dbValue, context); } else if (dbValue instanceof BasicDBList) { objValue = fillMapCollectionValue(mapValueType, (BasicDBList) dbValue, context); } else if (mapValueType instanceof Class) { objValue = toSimpleType((Class<?>) mapValueType, dbValue); } else { objValue = dbValue; } map.put(key, objValue); } } /** * Fills collection of elements in a map. * This method is needed to determine actual classes of the collection and its elements. * * @param mapValueType the type of values in the map, assignable from {@link java.util.Collection}. * @param dbValue the corresponded mongo values for this collection. * @param context the context for this operation, bounded to the root document. * @return filled collection of elements. */ private Object fillMapCollectionValue(Type mapValueType, BasicDBList dbValue, Context context) { // class of value in the map, assignable from Collection Class<?> collectionClass; // class of elements in the Collection Class<?> collectionElementClass; if (mapValueType instanceof ParameterizedType) { // if type is a parameterized collection then get its type parameter ParameterizedType parameterizedType = (ParameterizedType) mapValueType; collectionClass = (Class<?>) parameterizedType.getRawType(); collectionElementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0]; } else { collectionClass = (Class<?>) mapValueType; collectionElementClass = Object.class; } Collection<Object> collection; if (Set.class.isAssignableFrom(collectionClass)) { collection = new LinkedHashSet<>(); } else { collection = new ArrayList<>(); } fillCollection(collection, dbValue, collectionElementClass, context); return collection; } /** * Fills and returns an object represented by the given dbObject and stored as a map value. * * @param map the map holding this object. * @param mapValueType the type of values in the map, not assignable from {@link java.util.Collection}. * @param key the corresponded key in the map for this object. * @param dbObject the mongo DBObject which represents result object. * @param context the context for this operation, bounded to the root document. * @return the filled map value. */ private Object fillMapObjectValue(Map<String, Object> map, Type mapValueType, String key, BasicDBObject dbObject, Context context) { String className = classNameResolver.getClassName(mapValueType, dbObject, classPrefix); if (className != null) { String link = (String) dbObject.get(LINK_KEY); if (link != null) { final Object id = dbObject.get(ID_KEY); final Class<?> clazz = loadClass(className); return newProxy(clazz, new LinkObjectLoader(clazz, id, context), new MapValueReplacer(map, key)); } else { Object obj = newInstance(className); fillObject(obj, dbObject, context); return obj; } } else { Map<String, Object> childMap = new LinkedHashMap<>(); fillMap(childMap, dbObject, mapValueType, context); return childMap; } } /** * Processes (i.e. creates proxy and stores this proxy in the field) the given link to an object. * * @param field a domain object field references to the object represented by the given link. * @param obj the domain object holding the field. * @param value the link to other object. * @param context the context for this operation, bounded to the root document. */ private void processLink(final Field field, final Object obj, DBObject value, Context context) { final String className = classNameResolver.getClassName(field.getGenericType(), value, classPrefix); final Object id = value.get(ID_KEY); final Class<?> clazz = loadClass(className); Object fieldValue = newProxy(clazz, new LinkObjectLoader(clazz, id, context), new FieldReplacer(field, obj)); setFieldValue(field, obj, fieldValue); } /** * Loads domain object from MongoDB or from data section for the given class and identifier and converts it. * * @param id the domain object`s Id. * @param clazz the <strong>concrete</strong> class of the domain object. * @param context the context for this operation, bounded to the root document. * @return the loaded and fully converted domain object. */ @SuppressWarnings("unchecked") private Object loadObject(Object id, Class<?> clazz, Context context) { DBObject linkRoot = context.linkResolver.resolveExternal(id, clazz); return fromDbObject(linkRoot, clazz); } /** * The callback to load link to other domain object. Called from a proxy. * * @see org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler */ class LinkObjectLoader implements ObjectLoader { private final Class<?> clazz; private final Object id; private final Context context; LinkObjectLoader(Class<?> clazz, Object id, Context context) { this.clazz = clazz; this.id = id; this.context = context; } @Override public Object load() { return loadObject(id, clazz, context); } @Override public Object getId() { return id; } } /** * The callback to load collection of links to other domain objects. Called from a proxy. * * @see org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler */ class DefaultCollectionLoader implements CollectionLoader { private final List<Object> dbList; private CollectionFactory factory; private final Class<?> elementType; private Context context; DefaultCollectionLoader(List<Object> dbList, CollectionFactory factory, Class<?> elementType, Context context) { this.dbList = dbList; this.factory = factory; this.elementType = elementType; this.context = context; } @Override public Object load() { Collection<Object> collection = factory.create(dbList.size()); fillCollection(collection, dbList, elementType, context); return collection; } @Override public <T> List<T> getIds() { return ConverterHelper.getIds(dbList); } @Override public boolean isEmpty() { return dbList == null || dbList.isEmpty(); } @Override public int size() { return dbList != null ? dbList.size() : 0; } } /** * Context is responsible for holding objects bound to the root document. * <p/> * Though for now it can be replaced to just using link resolver instead, it's left for future needs. */ private static class Context { private final LinkResolver linkResolver; private Context(LinkResolver linkResolver) { this.linkResolver = linkResolver; } } @SuppressWarnings("UnusedDeclaration") public void setClassPrefix(String classPrefix) { this.classPrefix = classPrefix; } }