/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* 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.iternine.jeppetto.dao.mongodb.enhance;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.DefaultDBCallback;
import org.bson.BSON;
import org.bson.BSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MongoDBCallback extends DefaultDBCallback {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private static final Map<String, Class> classCache = new HashMap<String, Class>();
private Class rootClass;
private static Logger logger = LoggerFactory.getLogger(MongoDBCallback.class);
// List of nested documents that come back from an "explain()" call. This callback is used
// to construct the result object, but these fields do not have a corresponding sub-object.
private static final List<String> EXPLAIN_PATHS_TO_IGNORE = Arrays.asList("allPlans",
"indexBounds",
"shards",
"oldPlan");
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
public MongoDBCallback(DBCollection dbCollection, Class rootClass) {
super(dbCollection);
if (dbCollection != null && !dbCollection.getName().equals("$cmd")) {
this.rootClass = rootClass;
}
}
//-------------------------------------------------------------
// Methods - Overrides - DBCallback
//-------------------------------------------------------------
@Override
public BSONObject create(boolean array, List<String> pathParts) {
if (rootClass == null) {
return array ? new BasicDBList() : new BasicDBObject();
}
if (pathParts == null) {
try {
return (DBObject) rootClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
String path = buildPath(pathParts);
Class returnClass;
if ((returnClass = getClassFromCache(path)) == null) {
returnClass = deriveClass(path, pathParts.get(pathParts.size() - 1), array);
}
// At this point, we know what class to construct and the class cache is properly set
if (DBObject.class.isAssignableFrom(returnClass)) {
try {
return (DBObject) returnClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (Map.class.isAssignableFrom(returnClass)) {
if (Modifier.isAbstract(returnClass.getModifiers()) || Modifier.isInterface(returnClass.getModifiers())) {
return new DirtyableDBObjectMap();
} else {
try {
return new DirtyableDBObjectMap((Map) returnClass.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} else if (List.class.isAssignableFrom(returnClass)) {
if (Modifier.isAbstract(returnClass.getModifiers()) || Modifier.isInterface(returnClass.getModifiers())) {
return new DirtyableDBObjectList();
} else {
try {
return new DirtyableDBObjectList((List) returnClass.newInstance(), false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} else if (Set.class.isAssignableFrom(returnClass)) {
DirtyableDBObjectSet dirtyableDBObjectSet;
if (Modifier.isAbstract(returnClass.getModifiers()) || Modifier.isInterface(returnClass.getModifiers())) {
dirtyableDBObjectSet = new DirtyableDBObjectSet();
} else {
try {
dirtyableDBObjectSet = new DirtyableDBObjectSet((Set) returnClass.newInstance(), false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// The MongoDB Java Driver adds objects to the container before populating them. To maintain
// set semantics (which may require using the objects' values), we run a decoding hook to
// properly configure the set.
BSON.addDecodingHook(DirtyableDBObjectSet.class, dirtyableDBObjectSet.getDecodingTransformer());
return dirtyableDBObjectSet;
} else {
return new BasicDBObject();
}
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private String buildPath(List<String> path) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.size(); i++) {
if (i > 0) {
sb.append('.');
}
sb.append(path.get(i));
}
return sb.toString();
}
private Class getClassFromCache(String path) {
Class clazz = classCache.get(rootClass.getSimpleName() + "." + path);
if (clazz != null) {
return clazz;
}
int lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex > 0) {
return classCache.get(rootClass.getSimpleName() + "." + path.substring(0, lastDotIndex + 1));
}
return null;
}
private Class deriveClass(String path, String lastPathPart, boolean array) {
synchronized (classCache) {
// Re-check the cache in case it was set in another thread.
Class cachedClass = getClassFromCache(path);
if (cachedClass != null) {
return cachedClass;
}
return deriveClass1(path, lastPathPart, array);
}
}
/* tests:
* names at different depths
* maps w/ other objects as keys
*
* ""
* relatedMongoObjectMap relatedMongoObjectMap Map<>
* relatedMongoObjectMap.foo relatedMongoObjectMap. RelatedObject
* nestedSimpleMongoObject nestedSimpleMongoObject SimpleObject
* nestedSimpleMongoObject.relatedMongoObjectMap nestedSimpleMongoObject.relatedObjectMap Map<>
* nestedSimpleMongoObject.relatedMongoObjectMap.bar nestedSimpleMongoObject.relatedObjectMap. RelatedObject
*/
private Class deriveClass1(String path, String lastPathPart, boolean array) {
Class containerClass;
if (path.equals(lastPathPart)) {
containerClass = rootClass;
} else {
containerClass = classCache.get(rootClass.getSimpleName() + "." + path.substring(0, path.lastIndexOf('.')));
}
if (containerClass != null && DBObject.class.isAssignableFrom(containerClass)) {
try {
Method m = containerClass.getMethod("__getPreEnhancedClass");
containerClass = (Class) m.invoke(null);
} catch (Exception e) {
logger.warn("DBObject without __getPreEnhancedClass() method. Was the container class enhanced?");
}
}
// If we don't have a container class at this point, we are in a part of the result document that
// does not correspond to the object model. Return a basic MongoDB object.
if (containerClass == null) {
return array ? BasicDBList.class : BasicDBObject.class;
}
Method getter;
try {
// noinspection ConstantConditions
getter = containerClass.getMethod("get" + Character.toUpperCase(lastPathPart.charAt(0)) + lastPathPart.substring(1));
} catch (NoSuchMethodException e) {
if (!EXPLAIN_PATHS_TO_IGNORE.contains(lastPathPart) && !lastPathPart.startsWith("__")) {
logger.warn("No getter for: {} ({})", lastPathPart, e.getMessage());
}
return array ? BasicDBList.class : BasicDBObject.class;
}
Type returnType = getter.getGenericReturnType();
String qualifiedPath = rootClass.getSimpleName() + "." + path;
if (Class.class.isAssignableFrom(returnType.getClass())) {
// noinspection unchecked
Class enhancedClass = EnhancerHelper.getDirtyableDBObjectEnhancer((Class) returnType).getEnhancedClass();
classCache.put(qualifiedPath, enhancedClass);
return enhancedClass;
} else if (ParameterizedType.class.isAssignableFrom(returnType.getClass())) {
ParameterizedType parameterizedType = (ParameterizedType) returnType;
Class rawType = (Class) parameterizedType.getRawType();
Class rawClass;
Class enhancedClass;
if (Map.class.isAssignableFrom(rawType)) {
rawClass = (Class) parameterizedType.getActualTypeArguments()[1];
} else if (Iterable.class.isAssignableFrom(rawType)) {
rawClass = (Class) parameterizedType.getActualTypeArguments()[0];
} else {
throw new RuntimeException("unknown type: " + rawType);
}
classCache.put(qualifiedPath, rawType);
if (!DBObjectUtil.needsNoConversion(rawClass)) {
//noinspection unchecked
enhancedClass = EnhancerHelper.getDirtyableDBObjectEnhancer(rawClass).getEnhancedClass();
classCache.put(qualifiedPath + ".", enhancedClass);
}
return rawType;
} else {
throw new RuntimeException("Don't know how to handle: " + qualifiedPath);
}
}
}