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.google.common.collect.Sets; 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.document.support.LazyProxy; import org.fastmongo.odm.mapping.support.converter.ConversionService; import org.fastmongo.odm.mapping.support.linkwriter.LinkWriter; import org.fastmongo.odm.util.MongoNamingService; import org.fastmongo.odm.util.ReflectionUtils; import java.lang.reflect.Field; import java.util.*; import static org.fastmongo.odm.dbobject.mapping.core.ConverterHelper.*; /** * Converts domain models to {@link com.mongodb.DBObject}s. * <p/> * This implementation is thread safe. * * @author Alexander Gulko */ public class DomainToDbObjectConverter { /** * Classes that are supported by MongoDB Java Driver as values for document simple fields. * We don't need to perform any conversion on the them. */ private static final Set<Class<?>> VALUE_CLASSES = Sets.<Class<?>>newHashSet(Integer.class, int.class, Long.class, long.class, Boolean.class, boolean.class, Character.class, char.class, Double.class, double.class, Float.class, float.class, String.class, Date.class); /** * 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; /** * Returns names of mongo collections and other mapper internal names for the specified classes. */ private MongoNamingService namingService; /** * A prefix for full class names to reduce the length of strings in mongo. */ private String classPrefix = ""; /** * Classes that will be converted to DBObjects "in place", * i.e. without creating a link to actual object (either in external collection or in data section). */ private Set<Class<?>> flatModels = new HashSet<>(); /** * Perform additional actions on links to the bounded class. */ private Map<Class<?>, LinkWriter<?>> linkWriters = new HashMap<>(); /** * The set of Classes stored in separate MongoDB collections. */ private Set<Class<?>> collectionClasses = new HashSet<>(); /** * Constructor with the required dependencies. */ public DomainToDbObjectConverter(ConversionService conversionService, ClassNameResolver classNameResolver, MongoNamingService namingService) { this.conversionService = conversionService; this.classNameResolver = classNameResolver; this.namingService = namingService; } /** * Converts the given domain object into the MongoDB document. * * @param object the object to write into MongoDB. * @return the MongoDB document. */ @SuppressWarnings("UnusedDeclaration") public <T> BasicDBObject toDbObject(T object) { return toTopDbObject(object); } /** * Converts the given root object into the {@link com.mongodb.DBObject}. * * @param obj the root domain object (e.g. vehicle Make). * @return the {@link com.mongodb.DBObject} for the root domain object. */ private BasicDBObject toTopDbObject(Object obj) { return toFullDbObject(obj); } /** * Creates a link to the given domain object with id field. * <p/> * Calls object class {@link org.fastmongo.odm.mapping.support.linkwriter.LinkWriter} if present. * * @param obj the domain object. * @param id the Id value of the domain object. * @return the link to the given domain object with id field. */ @SuppressWarnings("unchecked") private DBObject createLink(Object obj, Object id) { Class<?> type = obj.getClass(); // the order matters: 1) link 2) class 3) id and other fields BasicDBObject link = new BasicDBObject(LINK_KEY, namingService.getLinkToClass(type)); writeClass(link, type, true); link.append(ID_KEY, id); LinkWriter linkWriter = linkWriters.get(type); return linkWriter != null ? linkWriter.write(link, obj) : link; } /** * Converts all fields of the given object into {@link com.mongodb.DBObject}. * * @param obj the domain object. * @return the {@link com.mongodb.DBObject} for the given object. */ private BasicDBObject toFullDbObject(final Object obj) { final BasicDBObject db = new BasicDBObject(); Class<?> type = obj.getClass(); // write class first (it is important!) writeClass(db, type, false); // then write Id field value Field idField = ReflectionUtils.findField(type, ID_KEY); if (idField != null) { idField.setAccessible(true); processField(idField, db, obj); } // and other fields for (Field field : ConverterHelper.getClassFields(type)) { if (field != idField) { processField(field, db, obj); } } return db; } /** * Converts the value of the given object field to its mongo representation. * * @param field the field of the domain object. * @param db the partially filled document. * @param obj the domain object. */ private void processField(Field field, BasicDBObject db, Object obj) { db.put(field.getName(), toDbValue(getFieldValue(field, obj))); } /** * Converts the given value to its mongo representation. * * @param value the value to be converted. * @return mongo representation of the given value. */ private Object toDbValue(Object value) { if (value == null) { return null; } if (value instanceof LazyProxy) { // we can be given value from DbObjectToDomainConverter class // but proxy doesn't have any field so we need to unwrap it before further processing value = ((LazyProxy) value).unwrap(); } Class<?> type = value.getClass(); Object dbValue; if (VALUE_CLASSES.contains(type)) { dbValue = value; } else if (Enum.class.isAssignableFrom(type)) { dbValue = conversionService.convertEnumToString((Enum<?>) value); } else if (conversionService.canConvert(type)) { dbValue = conversionService.convert(value, type, null); } else if (Collection.class.isAssignableFrom(type)) { dbValue = toCollection((Collection<?>) value); } else if (Map.class.isAssignableFrom(type)) { dbValue = toMap((Map<?, ?>) value); } else { // in this branch we are converting some domain object Field idField = ReflectionUtils.findField(type, ID_KEY); if (!isCollectionClass(type)) { dbValue = toFullDbObject(value); } else { Object id = getFieldValue(idField, value); dbValue = createLink(value, id); } } return dbValue; } /** * Returns <tt>true</tt> if the given class is a class stored in MongoDB Collection. * * @param clazz the class to check. * @return <tt>true</tt> if the given class is a class stored in MongoDB Collection. */ private boolean isCollectionClass(Class<?> clazz) { return collectionClasses.contains(clazz); } /** * Converts the given map into {@link com.mongodb.BasicDBObject}. * * @param map the map to be converted. * @return the converted map. */ private BasicDBObject toMap(Map<?, ?> map) { BasicDBObject db = new BasicDBObject(); for (Map.Entry<?, ?> entry : map.entrySet()) { String key = entry.getKey().toString(); db.put(escape(key), toDbValue(entry.getValue())); } return db; } /** * Converts the given collection into {@link com.mongodb.BasicDBList}. * * @param col the collection to be converted. * @return the converted collection. */ private BasicDBList toCollection(Collection<?> col) { final BasicDBList db = new BasicDBList(); for (Object obj : col) { db.add(toDbValue(obj)); } return db; } /** * Writes class name for the document. To restore the appropriate implementation in opposite mapping. * <p/> * <strong>Should be called before any other fields be added to the document.</strong> * * @param db the document. * @param type the class of the document. * @param isLink if the document represents a link to actual full document. */ <T extends DBObject> void writeClass(T db, Class<?> type, boolean isLink) { if (classNameResolver.isWriteClassName(type) || (isLink && classNameResolver.isWriteClassNameToLinks(type))) { db.put(CLASS_KEY, cleanClassName(type, classPrefix)); } } /* * Spring setters. */ @SuppressWarnings("UnusedDeclaration") public void setLinkWriters(List<LinkWriter<?>> linkWriterList) { for (LinkWriter<?> writer : linkWriterList) { for (Class<?> clazz : writer.supportedClasses()) { linkWriters.put(clazz, writer); } } } @SuppressWarnings("UnusedDeclaration") public void setFlatModels(List<Class<?>> classes) { flatModels.addAll(classes); } @SuppressWarnings("UnusedDeclaration") public void setClassPrefix(String classPrefix) { if (classPrefix != null) { this.classPrefix = classPrefix; } } @SuppressWarnings("unused") public void setCollectionClasses(Collection<Class<?>> collectionClasses) { if (collectionClasses != null) { this.collectionClasses.addAll(collectionClasses); } } }