All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.landawn.abacus.util.MongoDB Maven / Gradle / Ivy

/*
 * Copyright (C) 2015 HaiYang Li
 *
 * 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.landawn.abacus.util;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.bson.BSONObject;
import org.bson.BasicBSONObject;
import org.bson.BsonReader;
import org.bson.BsonWriter;
import org.bson.Document;
import org.bson.codecs.BsonTypeClassMap;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.DocumentCodec;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;

import com.landawn.abacus.DataSet;
import com.landawn.abacus.DirtyMarker;
import com.landawn.abacus.parser.JSONParser;
import com.landawn.abacus.parser.ParserFactory;
import com.landawn.abacus.type.Type;
import com.landawn.abacus.util.u.Optional;
import com.mongodb.BasicDBObject;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;

/**
 * It's a simple wrapper of MongoDB Java client.
 * 
 * 
We recommend to define "id" property in java entity/bean as the object "_id" in MongoDB to keep things as simple as possible.
* * * @since 0.8 * * @author Haiyang Li * * @see com.mongodb.client.model.Filters * @see com.mongodb.client.model.Aggregates * @see com.mongodb.client.model.Accumulators * @see com.mongodb.client.model.Projections * @see com.mongodb.client.model.Sorts */ public final class MongoDB { /** * It's name of object id set in Map/Object array. */ public static final String _ID = "_id"; /** * Property name of id. */ public static final String ID = "id"; private static final JSONParser jsonParser = ParserFactory.createJSONParser(); // private static CodecRegistry codecRegistry = CodecRegistries.fromCodecs(new CalendarCodec(), new TimeCodec(), new TimestampCodec()); private static final CodecRegistry codecRegistry = CodecRegistries.fromRegistries(MongoClient.getDefaultCodecRegistry(), new GeneralCodecRegistry()); private static final Map, Method> classIdSetMethodPool = new ConcurrentHashMap<>(); private final Map collExecutorPool = new ConcurrentHashMap<>(); private final Map, MongoCollectionMapper> collMapperPool = new ConcurrentHashMap<>(); private final MongoDatabase mongoDB; private final AsyncExecutor asyncExecutor; public MongoDB(final MongoDatabase mongoDB) { this(mongoDB, new AsyncExecutor(64, 300, TimeUnit.SECONDS)); } public MongoDB(final MongoDatabase mongoDB, final AsyncExecutor asyncExecutor) { this.mongoDB = mongoDB.withCodecRegistry(codecRegistry); this.asyncExecutor = asyncExecutor; } public MongoDatabase db() { return mongoDB; } public MongoCollection collection(final String collectionName) { return mongoDB.getCollection(collectionName); } public MongoCollection collection(final Class targetClass, final String collectionName) { return mongoDB.getCollection(collectionName, targetClass); } public MongoCollectionExecutor collExecutor(final String collectionName) { MongoCollectionExecutor collExecutor = collExecutorPool.get(collectionName); if (collExecutor == null) { synchronized (collExecutorPool) { collExecutor = collExecutorPool.get(collectionName); if (collExecutor == null) { collExecutor = new MongoCollectionExecutor(this, mongoDB.getCollection(collectionName), asyncExecutor); collExecutorPool.put(collectionName, collExecutor); } } } return collExecutor; } public MongoCollectionMapper collMapper(final Class targetClass) { return collMapper(targetClass, ClassUtil.getSimpleClassName(targetClass)); } @SuppressWarnings("rawtypes") public MongoCollectionMapper collMapper(final Class targetClass, String collectionName) { N.checkArgNotNull(targetClass, "targetClass"); N.checkArgNotNull(collectionName, "collectionName"); MongoCollectionMapper collMapper = collMapperPool.get(targetClass); if (collMapper == null) { synchronized (collMapperPool) { collMapper = collMapperPool.get(targetClass); if (collMapper == null) { collMapper = new MongoCollectionMapper(collExecutor(collectionName), targetClass); collMapperPool.put(targetClass, collMapper); } } } return collMapper; } /** * The object id ("_id") property will be read from/write to the specified property * @param cls * @param idPropertyName */ public static void registerIdProeprty(final Class cls, final String idPropertyName) { if (ClassUtil.getPropGetMethod(cls, idPropertyName) == null || ClassUtil.getPropSetMethod(cls, idPropertyName) == null) { throw new IllegalArgumentException("The specified class: " + ClassUtil.getCanonicalClassName(cls) + " doesn't have getter or setter method for the specified id propery: " + idPropertyName); } final Method setMethod = ClassUtil.getPropSetMethod(cls, idPropertyName); final Class parameterType = setMethod.getParameterTypes()[0]; if (!(String.class.isAssignableFrom(parameterType) || ObjectId.class.isAssignableFrom(parameterType))) { throw new IllegalArgumentException( "The parameter type of the specified id setter method must be 'String' or 'ObjectId': " + setMethod.toGenericString()); } classIdSetMethodPool.put(cls, setMethod); } public static DataSet extractData(final MongoIterable findIterable) { return extractData(Map.class, findIterable); } /** * * @param targetClass an entity class with getter/setter method or Map.class/Document.class * @param findIterable * @return */ public static DataSet extractData(final Class targetClass, final MongoIterable findIterable) { return extractData(targetClass, null, findIterable); } /** * * @param targetClass an entity class with getter/setter method or Map.class/Document.class * @param selectPropNames * @param findIterable * @return */ static DataSet extractData(final Class targetClass, final Collection selectPropNames, final MongoIterable findIterable) { checkTargetClass(targetClass); final List rowList = findIterable.into(new ArrayList<>()); final Optional first = N.firstNonNull(rowList); if (first.isPresent()) { /* if (Map.class.isAssignableFrom(first.get().getClass())) { if (N.isNullOrEmpty(selectPropNames)) { final Set columnNames = new LinkedHashSet<>(); @SuppressWarnings("rawtypes") final List> tmp = (List) rowList; for (Map row : tmp) { columnNames.addAll(row.keySet()); } return N.newDataSet(columnNames, rowList); } else { return N.newDataSet(selectPropNames, rowList); } } else { return N.newDataSet(rowList); } */ if (Map.class.isAssignableFrom(targetClass) && Map.class.isAssignableFrom(first.get().getClass())) { if (N.isNullOrEmpty(selectPropNames)) { final Set columnNames = new LinkedHashSet<>(); @SuppressWarnings("rawtypes") final List> tmp = (List) rowList; for (Map row : tmp) { columnNames.addAll(row.keySet()); } return N.newDataSet(columnNames, rowList); } else { return N.newDataSet(selectPropNames, rowList); } } else if (Document.class.isAssignableFrom(first.get().getClass())) { final List newRowList = new ArrayList<>(rowList.size()); for (Object row : rowList) { newRowList.add(toEntity(targetClass, (Document) row)); } return N.newDataSet(selectPropNames, newRowList); } else { return N.newDataSet(selectPropNames, rowList); } } else { return N.newEmptyDataSet(); } } /** * * @param targetClass an entity class with getter/setter method, Map.class or basic single value type(Primitive/String/Date...) * @param findIterable * @return */ @SuppressWarnings("rawtypes") public static List toList(final Class targetClass, final MongoIterable findIterable) { final Type type = N.typeOf(targetClass); final List rowList = findIterable.into(new ArrayList<>()); final Optional first = N.firstNonNull(rowList); if (first.isPresent()) { if (targetClass.isAssignableFrom(first.getClass())) { return (List) rowList; } else { final List resultList = new ArrayList<>(rowList.size()); if (type.isEntity() || type.isMap()) { if (first.get() instanceof Document) { for (Object row : rowList) { resultList.add(toEntity(targetClass, (Document) row)); } } else if (type.isMap()) { for (Object row : rowList) { resultList.add(Maps.entity2Map((Map) N.newInstance(targetClass), row)); } } else { for (Object row : rowList) { resultList.add(N.copy(targetClass, row)); } } } else if (first.get() instanceof Map && ((Map) first.get()).size() == 1) { final Map m = (Map) first.get(); final String propName = m.keySet().iterator().next(); if (m.get(propName) != null && targetClass.isAssignableFrom(m.get(propName).getClass())) { for (Object row : rowList) { resultList.add(((Map) row).get(propName)); } } else { for (Object row : rowList) { resultList.add(N.convert(((Map) row).get(propName), targetClass)); } } } else { throw new IllegalArgumentException( "Can't covert document: " + first.toString() + " to class: " + ClassUtil.getCanonicalClassName(targetClass)); } return (List) resultList; } } else { return new ArrayList<>(); } } /** * The id in the specified doc will be set to the returned object if and only if the id is not null or empty and it's acceptable to the targetClass. * * @param targetClass an entity class with getter/setter method, Map.class or basic single value type(Primitive/String/Date...) * @param doc * @return */ @SuppressWarnings("deprecation") public static T toEntity(final Class targetClass, final Document doc) { checkTargetClass(targetClass); if (Map.class.isAssignableFrom(targetClass)) { // return (T) new LinkedHashMap<>(doc); if (targetClass.isAssignableFrom(doc.getClass())) { return (T) doc; } else { final Map map = (Map) N.newInstance(targetClass); map.putAll(doc); return (T) map; } } final Method idSetMethod = getObjectIdSetMethod(targetClass); final Class parameterType = idSetMethod == null ? null : idSetMethod.getParameterTypes()[0]; final Object objectId = doc.getObjectId(_ID); T entity = null; doc.remove(_ID); try { entity = Maps.map2Entity(targetClass, doc); if (objectId != null && parameterType != null) { if (parameterType.isAssignableFrom(objectId.getClass())) { ClassUtil.setPropValue(entity, idSetMethod, objectId); } else if (parameterType.isAssignableFrom(String.class)) { ClassUtil.setPropValue(entity, idSetMethod, objectId.toString()); } else { ClassUtil.setPropValue(entity, idSetMethod, objectId); } } } finally { doc.put(_ID, objectId); } if (N.isDirtyMarker(entity.getClass())) { ((DirtyMarker) entity).markDirty(false); } return entity; } private static Method getObjectIdSetMethod(final Class targetClass) { Method idSetMethod = classIdSetMethodPool.get(targetClass); if (idSetMethod == null) { Method idPropSetMethod = ClassUtil.getPropSetMethod(targetClass, ID); Class parameterType = idPropSetMethod == null ? null : idPropSetMethod.getParameterTypes()[0]; // if (parameterType != null && (ObjectId.class.isAssignableFrom(parameterType) || String.class.isAssignableFrom(parameterType))) { // idSetMethod = idPropSetMethod; // } if (parameterType != null && ObjectId.class.isAssignableFrom(parameterType)) { idSetMethod = idPropSetMethod; } if (idSetMethod == null) { idSetMethod = ClassUtil.METHOD_MASK; } classIdSetMethodPool.put(targetClass, idSetMethod); } return idSetMethod == ClassUtil.METHOD_MASK ? null : idSetMethod; } public static String toJSON(final Bson bson) { return bson instanceof Map ? N.toJSON(bson) : N.toJSON(bson.toBsonDocument(Document.class, codecRegistry)); } public static String toJSON(final BSONObject bsonObject) { return bsonObject instanceof Map ? N.toJSON(bsonObject) : N.toJSON(bsonObject.toMap()); } public static String toJSON(final BasicDBObject bsonObject) { return bsonObject instanceof Map ? N.toJSON(bsonObject) : N.toJSON(bsonObject.toMap()); } /** * Returns an instance of the specified target class with the property values from the specified JSON String. * * @param targetClass Bson.class, Document.class, BasicBSONObject.class, BasicDBObject.class * @param json * @return */ public static T fromJSON(final Class targetClass, final String json) { if (targetClass.equals(Bson.class) || targetClass.equals(Document.class)) { final Document doc = new Document(); jsonParser.readString(doc, json); return (T) doc; } else if (targetClass.equals(BasicBSONObject.class)) { final BasicBSONObject result = new BasicBSONObject(); jsonParser.readString(result, json); return (T) result; } else if (targetClass.equals(BasicDBObject.class)) { final BasicDBObject result = new BasicDBObject(); jsonParser.readString(result, json); return (T) result; } else { throw new IllegalArgumentException("Unsupported type: " + ClassUtil.getCanonicalClassName(targetClass)); } } /** * * @param obj an array of pairs of property name and value/Map or an entity with getter/setter methods. * @return */ public static Bson toBson(final Object obj) { return toDocument(obj); } /** * Create a new document with specified parameter(s). It could an array of property name and value, or a map, or an entity. * * @param a * @return */ @SafeVarargs public static Bson toBson(final Object... a) { return toDocument(a); } /** * * @param obj an array of pairs of property name and value/Map or an entity with getter/setter methods. * @return */ public static Document toDocument(final Object obj) { return toDocument(obj, false); } /** * Create a new document with specified parameter(s). It could an array of property name and value, or a map, or an entity. * * @param a * @return */ @SafeVarargs public static Document toDocument(final Object... a) { if (N.isNullOrEmpty(a)) { return new Document(); } return a.length == 1 ? toDocument(a[0]) : toDocument((Object) a); } @SuppressWarnings("deprecation") static Document toDocument(final Object obj, final boolean isForUpdate) { final Document result = new Document(); if (obj instanceof Map) { result.putAll((Map) obj); } else if (N.isEntity(obj.getClass())) { if (obj instanceof DirtyMarker) { final Class srCls = obj.getClass(); final Set updatePropNames = isForUpdate ? ((DirtyMarker) obj).dirtyPropNames() : ((DirtyMarker) obj).signedPropNames(); if (updatePropNames.size() == 0) { // logger.warn("No property is signed/updated in the specified source entity: " + N.toString(obj)); } else { Method propGetMethod = null; Object propValue = null; for (String propName : updatePropNames) { propGetMethod = ClassUtil.getPropGetMethod(srCls, propName); propName = ClassUtil.getPropNameByMethod(propGetMethod); propValue = ClassUtil.getPropValue(obj, propGetMethod); result.put(propName, propValue); } } } else { final Map getterMethodList = ClassUtil.getPropGetMethodList(obj.getClass()); if (getterMethodList.size() == 0) { throw new IllegalArgumentException("No property getter/setter method found in the specified entity: " + obj.getClass().getCanonicalName()); } String propName = null; Object propValue = null; for (Map.Entry entry : getterMethodList.entrySet()) { propName = entry.getKey(); propValue = ClassUtil.getPropValue(obj, entry.getValue()); if (propValue == null) { continue; } result.put(propName, propValue); } } } else if (obj instanceof Object[]) { final Object[] a = (Object[]) obj; if (0 != (a.length % 2)) { throw new IllegalArgumentException( "The parameters must be the pairs of property name and value, or Map, or an entity class with getter/setter methods."); } for (int i = 0; i < a.length; i++) { result.put((String) a[i], a[++i]); } } else { throw new IllegalArgumentException("The parameters must be a Map, or an entity class with getter/setter methods"); } resetObjectId(obj, result); return result; } /** * * @param obj an array of pairs of property name and value/Map or an entity with getter/setter methods. * @return */ public static BasicBSONObject toBSONObject(final Object obj) { final BasicBSONObject result = new BasicBSONObject(); if (obj instanceof Map) { result.putAll((Map) obj); } else if (N.isEntity(obj.getClass())) { Maps.deepEntity2Map(result, obj); } else if (obj instanceof Object[]) { final Object[] a = (Object[]) obj; if (0 != (a.length % 2)) { throw new IllegalArgumentException( "The parameters must be the pairs of property name and value, or Map, or an entity class with getter/setter methods."); } for (int i = 0; i < a.length; i++) { result.put((String) a[i], a[++i]); } } else { throw new IllegalArgumentException("The parameters must be a Map, or an entity class with getter/setter methods"); } resetObjectId(obj, result); return result; } @SafeVarargs public static BasicBSONObject toBSONObject(final Object... a) { if (N.isNullOrEmpty(a)) { return new BasicBSONObject(); } return a.length == 1 ? toBSONObject(a[0]) : toBSONObject((Object) a); } /** * * @param obj an array of pairs of property name and value/Map or an entity with getter/setter methods. * @return */ public static BasicDBObject toDBObject(final Object obj) { final BasicDBObject result = new BasicDBObject(); if (obj instanceof Map) { result.putAll((Map) obj); } else if (N.isEntity(obj.getClass())) { Maps.deepEntity2Map(result, obj); } else if (obj instanceof Object[]) { final Object[] a = (Object[]) obj; if (0 != (a.length % 2)) { throw new IllegalArgumentException( "The parameters must be the pairs of property name and value, or Map, or an entity class with getter/setter methods."); } for (int i = 0; i < a.length; i++) { result.put((String) a[i], a[++i]); } } else { throw new IllegalArgumentException("The parameters must be a Map, or an entity class with getter/setter methods"); } resetObjectId(obj, result); return result; } @SafeVarargs public static BasicDBObject toDBObject(final Object... a) { if (N.isNullOrEmpty(a)) { return new BasicDBObject(); } return a.length == 1 ? toDBObject(a[0]) : toDBObject((Object) a); } private static void resetObjectId(final Object obj, final Map doc) { final Class cls = obj.getClass(); final Method idSetMethod = getObjectIdSetMethod(obj.getClass()); final String idPropertyName = N.isEntity(cls) ? (idSetMethod == null ? null : ClassUtil.getPropNameByMethod(idSetMethod)) : _ID; if (idPropertyName != null && doc.containsKey(idPropertyName)) { Object id = doc.remove(idPropertyName); try { if (id instanceof String) { id = new ObjectId((String) id); } else if (id instanceof Date) { id = new ObjectId((Date) id); } else if (id instanceof byte[]) { id = new ObjectId((byte[]) id); } } finally { doc.put(_ID, id); } } } private static void checkTargetClass(final Class targetClass) { if (!(N.isEntity(targetClass) || Map.class.isAssignableFrom(targetClass))) { throw new IllegalArgumentException("The target class must be an entity class with getter/setter methods or Map.class/Document.class. But it is: " + ClassUtil.getCanonicalClassName(targetClass)); } } private static class GeneralCodecRegistry implements CodecRegistry { private static final Map, Codec> pool = new ObjectPool, Codec>(128); @Override public Codec get(final Class clazz) { Codec codec = pool.get(clazz); if (codec == null) { codec = new GeneralCodec(clazz); pool.put(clazz, codec); } return (Codec) codec; } } private static class GeneralCodec implements Codec { private static final DocumentCodec documentCodec = new DocumentCodec(codecRegistry, new BsonTypeClassMap()); private final Class cls; private final boolean isEntityClass; public GeneralCodec(final Class cls) { this.cls = cls; isEntityClass = N.isEntity(cls); } @Override public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) { if (isEntityClass) { documentCodec.encode(writer, toDocument(value), encoderContext); } else { writer.writeString(N.stringOf(value)); } } @Override public T decode(final BsonReader reader, final DecoderContext decoderContext) { if (isEntityClass) { return toEntity(cls, documentCodec.decode(reader, decoderContext)); } else { return N.valueOf(cls, reader.readString()); } } @Override public Class getEncoderClass() { return cls; } } }