/*
* Copyright 2014 Thiago da Silva Gonzaga <thiagosg@sjrp.unesp.br>..
*
* 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.arquivolivre.mongocom.management;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.arquivolivre.mongocom.annotations.Document;
import com.arquivolivre.mongocom.annotations.GeneratedValue;
import com.arquivolivre.mongocom.annotations.Id;
import com.arquivolivre.mongocom.annotations.Internal;
import com.arquivolivre.mongocom.annotations.ObjectId;
import com.arquivolivre.mongocom.annotations.Reference;
import com.arquivolivre.mongocom.annotations.Index;
import com.arquivolivre.mongocom.utils.Generator;
import com.mongodb.BasicDBList;
import com.mongodb.DBCollection;
import com.mongodb.Mongo;
import com.mongodb.WriteConcern;
import java.io.Closeable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.bson.types.BasicBSONList;
/**
*
* @author Thiago da Silva Gonzaga <thiagosg@sjrp.unesp.br>.
*/
public final class CollectionManager implements Closeable {
private final Mongo client;
private DB db;
private static final Logger LOG = Logger.getLogger(CollectionManager.class.getName());
//TODO: a better way to manage db connection
protected CollectionManager(Mongo client, String dataBase) {
this.client = client;
if (dataBase != null && !dataBase.equals("")) {
this.db = client.getDB(dataBase);
} else {
this.db = client.getDB(client.getDatabaseNames().get(0));
}
}
protected CollectionManager(Mongo client, String dbName, String user, String password) {
this(client, dbName);
db.authenticate(user, password.toCharArray());
}
protected CollectionManager(Mongo client) {
this.client = client;
}
/**
* Uses the specified Database, creates one if it doesn't exist.
*
* @param dbName Database name
*/
public void use(String dbName) {
db = client.getDB(dbName);
}
/**
* The number of documents in the specified collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @return the total of documents.
*/
public <A extends Object> long count(Class<A> collectionClass) {
return count(collectionClass, new MongoQuery());
}
/**
* The number of documents that match the specified query.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @param query
* @return the total of documents.
*/
public <A extends Object> long count(Class<A> collectionClass, MongoQuery query) {
long ret = 0l;
try {
A result = collectionClass.newInstance();
String collectionName = reflectCollectionName(result);
ret = db.getCollection(collectionName).count(query.getQuery());
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) {
LOG.log(Level.SEVERE, null, ex);
}
return ret;
}
/**
* Find all documents in the specified collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @return a list of documents.
*/
public <A extends Object> List<A> find(Class<A> collectionClass) {
return find(collectionClass, new MongoQuery());
}
/**
* Find all documents that match the specified query in the given
* collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @param query
* @return a list of documents.
*/
public <A extends Object> List<A> find(Class<A> collectionClass, MongoQuery query) {
List<A> resultSet = new ArrayList<>();
DBCursor cursor = null;
try {
A obj = collectionClass.newInstance();
String collectionName = reflectCollectionName(obj);
cursor = db.getCollection(collectionName).find(query.getQuery(), query.getConstraits());
if (query.getSkip() > 0) {
cursor = cursor.skip(query.getSkip());
}
if (query.getLimit() > 0) {
cursor = cursor.limit(query.getLimit());
}
while (cursor.hasNext()) {
DBObject objDB = cursor.next();
loadObject(obj, objDB);
resultSet.add(obj);
obj = collectionClass.newInstance();
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) {
LOG.log(Level.SEVERE, null, ex);
} finally {
if (cursor != null) {
cursor.close();
}
}
return resultSet;
}
/**
* Find a single document of the specified collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @return a document.
*/
public <A extends Object> A findOne(Class<A> collectionClass) {
A result = null;
try {
result = collectionClass.newInstance();
String collectionName = reflectCollectionName(result);
DBObject obj = db.getCollection(collectionName).findOne();
if (obj == null) {
return null;
}
loadObject(result, obj);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException ex) {
LOG.log(Level.SEVERE, null, ex);
}
return result;
}
/**
* Find a single document that matches the specified query in the given
* collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @param query
* @return a document.
*/
public <A extends Object> A findOne(Class<A> collectionClass, MongoQuery query) {
A result = null;
try {
result = collectionClass.newInstance();
String collectionName = reflectCollectionName(result);
DBObject obj = db.getCollection(collectionName).findOne(query.getQuery(), query.getConstraits());
if (obj == null) {
return null;
}
loadObject(result, obj);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException ex) {
LOG.log(Level.SEVERE, null, ex);
}
return result;
}
/**
* Find a single document that matches the specified id in the given
* collection.
*
* @param <A> generic type of the collection.
* @param collectionClass
* @param id
* @return a document.
*/
public <A extends Object> A findById(Class<A> collectionClass, String id) {
return findOne(collectionClass, new MongoQuery("_id", id));
}
/**
* Remove the specified document from the collection.
*
* @param document to be removed.
*/
public void remove(Object document) {
try {
BasicDBObject obj = loadDocument(document);
String collectionName = reflectCollectionName(document);
db.getCollection(collectionName).remove(obj);
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | SecurityException | IllegalArgumentException ex) {
LOG.log(Level.SEVERE, "An error occured while removing this document: {0}", ex.getMessage());
}
}
/**
* Insert the document in a collection
*
* @param document
* @return the <code>_id</code> of the inserted document, <code>null</code>
* if fails.
*/
public String insert(Object document) {
String _id = null;
if (document == null) {
return _id;
}
try {
BasicDBObject obj = loadDocument(document);
String collectionName = reflectCollectionName(document);
db.getCollection(collectionName).insert(obj);
_id = obj.getString("_id");
Field field = getFieldByAnnotation(document, ObjectId.class, false);
if (field != null) {
field.setAccessible(true);
field.set(document, _id);
}
indexFields(document);
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | SecurityException | IllegalArgumentException | NoSuchFieldException ex) {
LOG.log(Level.SEVERE, "An error occured while inserting this document: {0}", ex.getMessage());
}
if (_id != null) {
LOG.log(Level.INFO, "Object \"{0}\" inserted successfully.", _id);
}
return _id;
}
public void update(MongoQuery query, Object document) {
update(query, document, false, false);
}
public void update(MongoQuery query, Object document, boolean upsert, boolean multi) {
update(query, document, upsert, multi, WriteConcern.ACKNOWLEDGED);
}
public void update(MongoQuery query, Object document, boolean upsert, boolean multi, WriteConcern concern) {
try {
BasicDBObject obj = loadDocument(document);
String collectionName = reflectCollectionName(document);
db.getCollection(collectionName).update(query.getQuery(), obj, upsert, multi, concern);
} catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | SecurityException | IllegalArgumentException ex) {
LOG.log(Level.SEVERE, null, ex);
}
}
public void updateMulti(MongoQuery query, Object document) {
update(query, document, false, true);
}
public String save(Object document) {
//TODO: a better way to throw/treat exceptions
/*if (!document.getClass().isAnnotationPresent(Document.class)) {
throw new NoSuchMongoCollectionException(document.getClass() + " is not a valid Document.");
}*/
String _id = null;
if (document == null) {
return _id;
}
try {
BasicDBObject obj = loadDocument(document);
String collectionName = reflectCollectionName(document);
db.getCollection(collectionName).save(obj);
_id = obj.getString("_id");
indexFields(document);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) {
LOG.log(Level.SEVERE, "An error occured while saving this document: {0}", ex.getMessage());
}
if (_id != null) {
LOG.log(Level.INFO, "Object \"{0}\" saved successfully.", _id);
}
return _id;
}
private void indexFields(Object document) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
String collectionName = reflectCollectionName(document);
Field[] fields = getFieldsByAnnotation(document, Index.class);
Map<String, List<String>> compoundIndexes = new TreeMap<>();
BasicDBObject compoundIndexesOpt = new BasicDBObject("background", true);
DBCollection collection = db.getCollection(collectionName);
for (Field field : fields) {
Annotation annotation = field.getAnnotation(Index.class);
BasicDBObject options = new BasicDBObject();
BasicDBObject indexKeys = new BasicDBObject();
String indexName = (String) annotation.annotationType().getMethod("value").invoke(annotation);
String type = (String) annotation.annotationType().getMethod("type").invoke(annotation);
boolean unique = (boolean) annotation.annotationType().getMethod("unique").invoke(annotation);
boolean sparse = (boolean) annotation.annotationType().getMethod("sparse").invoke(annotation);
boolean dropDups = (boolean) annotation.annotationType().getMethod("dropDups").invoke(annotation);
boolean background = (boolean) annotation.annotationType().getMethod("background").invoke(annotation);
int order = (int) annotation.annotationType().getMethod("order").invoke(annotation);
if (!indexName.equals("")) {
options.append("name", indexName);
}
options.append("background", background);
options.append("unique", unique);
options.append("sparse", sparse);
options.append("dropDups", dropDups);
String fieldName = field.getName();
if (indexName.equals("") && type.equals("")) {
indexKeys.append(fieldName, order);
collection.ensureIndex(indexKeys, options);
} else if (!indexName.equals("") && type.equals("")) {
List<String> result = compoundIndexes.get(indexName);
if (result == null) {
result = new ArrayList<>();
compoundIndexes.put(indexName, result);
}
result.add(fieldName + "_" + order);
} else if (!type.equals("")) {
indexKeys.append(fieldName, type);
collection.ensureIndex(indexKeys, compoundIndexesOpt);
}
}
Set<String> keys = compoundIndexes.keySet();
for (String key : keys) {
BasicDBObject keysObj = new BasicDBObject();
compoundIndexesOpt.append("name", key);
for (String value : compoundIndexes.get(key)) {
boolean with_ = false;
if (value.startsWith("_")) {
value = value.replaceFirst("_", "");
with_ = true;
}
String[] opt = value.split("_");
if (with_) {
opt[0] = "_" + opt[0];
}
keysObj.append(opt[0], Integer.parseInt(opt[1]));
}
collection.ensureIndex(keysObj, compoundIndexesOpt);
}
}
private BasicDBObject loadDocument(Object document) throws SecurityException, InstantiationException, InvocationTargetException, NoSuchMethodException {
Field[] fields = document.getClass().getDeclaredFields();
BasicDBObject obj = new BasicDBObject();
for (Field field : fields) {
try {
field.setAccessible(true);
String fieldName = field.getName();
Object fieldContent = field.get(document);
if (fieldContent == null && !field.isAnnotationPresent(GeneratedValue.class)) {
continue;
}
if (fieldContent instanceof List) {
BasicDBList list = new BasicDBList();
boolean isInternal = field.isAnnotationPresent(Internal.class);
for (Object item : (List) fieldContent) {
if (isInternal) {
list.add(loadDocument(item));
} else {
list.add(item);
}
}
obj.append(fieldName, list);
} else if (field.getType().isEnum()) {
obj.append(fieldName, fieldContent.toString());
} else if (field.isAnnotationPresent(Reference.class)) {
obj.append(fieldName, new org.bson.types.ObjectId(save(fieldContent)));
} else if (field.isAnnotationPresent(Internal.class)) {
obj.append(fieldName, loadDocument(fieldContent));
} else if (field.isAnnotationPresent(Id.class) && !fieldContent.equals("")) {
obj.append(fieldName, reflectId(field));
} else if (field.isAnnotationPresent(GeneratedValue.class)) {
Object value = reflectGeneratedValue(field, fieldContent);
if (value != null) {
obj.append(fieldName, value);
}
} else if (!field.isAnnotationPresent(ObjectId.class)) {
obj.append(fieldName, fieldContent);
} else if (!fieldContent.equals("")) {
obj.append("_id", new org.bson.types.ObjectId((String) fieldContent));
}
} catch (IllegalArgumentException | IllegalAccessException ex) {
LOG.log(Level.SEVERE, null, ex);
}
}
return obj;
}
private <A extends Object> void loadObject(A object, DBObject document) throws IllegalAccessException, IllegalArgumentException, SecurityException, InstantiationException {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
Object fieldContent = document.get(fieldName);
if (fieldContent instanceof BasicBSONList) {
Class<?> fieldArgClass = null;
ParameterizedType genericFieldType = (ParameterizedType) field.getGenericType();
Type[] fieldArgTypes = genericFieldType.getActualTypeArguments();
for (Type fieldArgType : fieldArgTypes) {
fieldArgClass = (Class<?>) fieldArgType;
}
List<Object> list = new ArrayList<>();
boolean isInternal = field.isAnnotationPresent(Internal.class);
for (Object item : (BasicBSONList) fieldContent) {
if (isInternal) {
Object o = fieldArgClass.newInstance();
loadObject(o, (DBObject) item);
list.add(o);
} else {
list.add(item);
}
}
field.set(object, list);
} else if ((fieldContent != null) && field.getType().isEnum()) {
field.set(object, Enum.valueOf((Class) field.getType(), (String) fieldContent));
} else if ((fieldContent != null) && field.isAnnotationPresent(Reference.class)) {
field.set(object, findById(field.getType(), ((org.bson.types.ObjectId) fieldContent).toString()));
} else if (field.isAnnotationPresent(ObjectId.class)) {
field.set(object, ((org.bson.types.ObjectId) document.get("_id")).toString());
} else if (field.getType().isPrimitive() && (fieldContent == null)) {
} else if (fieldContent != null) {
field.set(object, fieldContent);
}
}
}
private Field getFieldByAnnotation(Object obj, Class<? extends Annotation> annotationClass, boolean annotationRequired) throws NoSuchFieldException {
Field[] fields = getFieldsByAnnotation(obj, annotationClass);
if ((fields.length == 0) && annotationRequired) {
throw new NoSuchFieldException("@" + annotationClass.getSimpleName() + " field not found.");
} else if (fields.length > 0) {
if (fields.length > 1) {
LOG.log(Level.WARNING, "There are more than one @{0} field. Assuming the first one.", annotationClass.getSimpleName());
}
return fields[0];
}
return null;
}
private Field[] getFieldsByAnnotation(Object obj, Class<? extends Annotation> annotationClass) {
Field[] fields = obj.getClass().getDeclaredFields();
List<Field> fieldsAnnotated = new ArrayList<>();
for (Field field : fields) {
if (field.isAnnotationPresent(annotationClass)) {
fieldsAnnotated.add(field);
}
}
fields = new Field[fieldsAnnotated.size()];
return fieldsAnnotated.toArray(fields);
}
private void invokeAnnotatedMethods(Object obj, Class<? extends Annotation> annotationClass) {
Method[] methods = getMethodsByAnnotation(obj, annotationClass);
for (Method method : methods) {
try {
method.invoke(obj);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
LOG.log(Level.SEVERE, null, ex);
}
}
}
private Method[] getMethodsByAnnotation(Object obj, Class<? extends Annotation> annotationClass) {
Method[] methods = obj.getClass().getDeclaredMethods();
List<Method> methodsAnnotated = new ArrayList<>();
for (Method method : methods) {
if (method.isAnnotationPresent(annotationClass)) {
methodsAnnotated.add(method);
}
}
return (Method[]) methodsAnnotated.toArray();
}
private String reflectCollectionName(Object document) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, SecurityException, IllegalArgumentException {
Annotation annotation = document.getClass().getAnnotation(Document.class);
String coll = (String) annotation.annotationType().getMethod("collection").invoke(annotation);
if (coll.equals("")) {
coll = document.getClass().getSimpleName();
}
return coll;
}
private <A extends Object> A reflectId(Field field) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
Annotation annotation = field.getAnnotation(Id.class);
Boolean autoIncrement = (Boolean) annotation.annotationType().getMethod("autoIncrement").invoke(annotation);
Class generator = (Class) annotation.annotationType().getMethod("generator").invoke(annotation);
if (autoIncrement) {
Generator g = (Generator) generator.newInstance();
return g.generateValue(field.getDeclaringClass(), db);
}
return null;
}
private <A extends Object> A reflectGeneratedValue(Field field, Object oldValue) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
Annotation annotation = field.getAnnotation(GeneratedValue.class);
Class<? extends Annotation> annotationType = annotation.annotationType();
Boolean update = (Boolean) annotationType.getMethod("update").invoke(annotation);
Class generator = (Class) annotationType.getMethod("generator").invoke(annotation);
Generator g = (Generator) generator.newInstance();
if ((update && (oldValue != null)) || (oldValue == null)) {
return g.generateValue(field.getDeclaringClass(), db);
} else if (oldValue instanceof Number) {
boolean test = oldValue.equals(oldValue.getClass().cast(0));
if (test) {
return g.generateValue(field.getDeclaringClass(), db);
} else if (update) {
return g.generateValue(field.getDeclaringClass(), db);
}
}
return null;
}
public String getStatus() {
return client.getAddress() + " " + client.getMongoOptions();
}
@Override
public void close() {
client.close();
}
}