diff --git a/source/pom.xml b/source/pom.xml index 5ab0940..a2f2a97 100644 --- a/source/pom.xml +++ b/source/pom.xml @@ -353,6 +353,16 @@ httpcore 4.1.1 + + org.mongodb + bson + 2.5.3 + + + org.mongodb + mongo-java-driver + 2.3 + diff --git a/source/src/main/java/siena/mongodb/MongoDBIndexGenerator.java b/source/src/main/java/siena/mongodb/MongoDBIndexGenerator.java new file mode 100644 index 0000000..fc9bcb6 --- /dev/null +++ b/source/src/main/java/siena/mongodb/MongoDBIndexGenerator.java @@ -0,0 +1,21 @@ +package siena.mongodb; + +import java.util.List; + +/** + * A generator to process Siena annotated indexes in Mongo. + * + * @author pjarrell + */ +public interface MongoDBIndexGenerator { + + /** + * Generate indexes from the supplied model classes. + * + * @param clazzes + * is a list of model classes to process + */ + @SuppressWarnings("rawtypes") + void generate(final List clazzes); + +} diff --git a/source/src/main/java/siena/mongodb/MongoDBIndexGeneratorImpl.java b/source/src/main/java/siena/mongodb/MongoDBIndexGeneratorImpl.java new file mode 100644 index 0000000..8ebeb3d --- /dev/null +++ b/source/src/main/java/siena/mongodb/MongoDBIndexGeneratorImpl.java @@ -0,0 +1,91 @@ +package siena.mongodb; + +import java.lang.reflect.Field; +import java.util.List; + +import siena.ClassInfo; +import siena.Index; +import siena.Model; +import siena.PersistenceManager; +import siena.Unique; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; + +/** + * A simple generator to help process Siena annotated indexes with Mongo. This generator can be called on each start up since the indexes will only be created if necessary. + * + * @author pjarrell + */ +public class MongoDBIndexGeneratorImpl implements MongoDBIndexGenerator { + + private final MongoDBPersistenceManager persistenceManager; + + /** + * Create the generator using the Mongo {@link PersistenceManager}. + * + * @param persistenceManager + * is the persistence manager to use to generate indexes + */ + public MongoDBIndexGeneratorImpl(final MongoDBPersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + } + + /** + * Generate Mongo indexes using the annotations {@link Index} and {@link Unique} from the model classes provided. + * + * @param clazzes + * is a list of Siena {@link Model} classes + */ + @SuppressWarnings("rawtypes") + public void generate(final List clazzes) { + if (clazzes != null) { + for (final Class clazz : clazzes) { + if (ClassInfo.isModel(clazz)) { + final ClassInfo info = ClassInfo.getClassInfo(clazz); + final DBCollection collection = persistenceManager.getDatabase().getCollection(info.tableName); + for (final Field field : info.updateFields) { + + final Index index = field.getAnnotation(Index.class); + if (index != null) { + final String[] names = index.value(); + final DBObject newIndex = new BasicDBObject(); + for (final String name : names) { + if (newIndex.containsField(name) == false) { + newIndex.put(name, 1); + } + } + collection.ensureIndex(newIndex); + } + + final Unique unique = field.getAnnotation(Unique.class); + if (unique != null) { + final String[] names = unique.value(); + final DBObject newIndex = new BasicDBObject(); + for (final String name : names) { + if (newIndex.containsField(name) == false) { + newIndex.put(name, 1); + } + } + final DBObject uniqueIndex = new BasicDBObject(); + uniqueIndex.put("unique", true); + collection.ensureIndex(newIndex, uniqueIndex); + } + + } + } + } + } + } + + /** + * Gets the persistence manager being used by this generator. + * + * @return the persistenceManager + */ + protected MongoDBPersistenceManager getPersistenceManager() { + return persistenceManager; + } + +} diff --git a/source/src/main/java/siena/mongodb/MongoDBPersistenceManager.java b/source/src/main/java/siena/mongodb/MongoDBPersistenceManager.java new file mode 100644 index 0000000..4928603 --- /dev/null +++ b/source/src/main/java/siena/mongodb/MongoDBPersistenceManager.java @@ -0,0 +1,899 @@ +package siena.mongodb; + +import java.lang.reflect.Field; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.types.ObjectId; + +import siena.AbstractPersistenceManager; +import siena.ClassInfo; +import siena.Json; +import siena.Query; +import siena.QueryFilter; +import siena.QueryFilterSimple; +import siena.QueryOrder; +import siena.SienaException; +import siena.Util; +import siena.core.async.PersistenceManagerAsync; +import siena.embed.Embedded; +import siena.embed.JsonSerializer; +import siena.gae.Unindexed; + +import com.mongodb.BasicDBObject; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.Mongo; +import com.mongodb.MongoOptions; +import com.mongodb.ServerAddress; + +/** + * Persistence Manager for working with a Mongo DB. Each Siena model will become it's own collection. + *

+ * The following properties can be configured on Mongo database. A database name and at least one host name are required. + *

    + *
  • siena.mongodb.hostnames - a comma-delimited list of host names (or IP addresses) with or without port numbers (i.e. 127.0.0.1:27000,192.168.0.1)
  • + *
  • siena.mongodb.databaseName - name of the database (will be created automatically if it doesn't already exist)
  • + *
  • siena.mongodb.userName - required if auth is enabled
  • + *
  • siena.mongodb.password - required if auth is enabled
  • + *
  • siena.mongodb.autoConnectRetry
  • + *
  • siena.mongodb.connectionsPerHost
  • + *
  • siena.mongodb.connectTimeout
  • + *
  • siena.mongodb.maxWaitTime
  • + *
  • siena.mongodb.socketTimeout
  • + *
  • siena.mongodb.threadsAllowedToBlockForConnectionMultiplier
  • + *
+ *

+ * The Query operators supported are: + *

    + *
  • =
  • + *
  • !=
  • + *
  • <
  • + *
  • >
  • + *
  • <=
  • + *
  • >=
  • + *
  • IN
  • + *
  • NOT IN
  • + *
  • MOD
  • + *
  • ALL
  • + *
  • EXISTS
  • + *
  • TYPE
  • + *
  • SIZE
  • + *
  • OR
  • + *
  • NOR
  • + *
+ * + * @author pjarrell + * @author (modified by) T.hagikura + */ +public class MongoDBPersistenceManager extends AbstractPersistenceManager { + + private static final String ID_FIELD = "_id"; + private static final String PREFIX = "siena.mongodb."; + private static final Log LOGGER = LogFactory.getLog(MongoDBPersistenceManager.class); + private static final Map operators = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("=", null); + put("!=", "$ne"); + put("<", "$lt"); + put(">", "$gt"); + put("<=", "$lte"); + put(">=", "$gte"); + put(" IN", "$in"); + put(" NOT IN", "$nin"); + put(" MOD", "$mod"); + put(" ALL", "$all"); + put(" EXISTS", "$exists"); + put(" TYPE", "$type"); + put(" SIZE", "$size"); + put(" OR", "$or"); + put(" NOR", "$nor"); + } + }; + private static String[] supportedOperators; + + static { + supportedOperators = operators.keySet().toArray(new String[0]); + } + + private Mongo mongo; + private String databaseName; + private String userName; + private String password; + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#beginTransaction(int) + */ + @Override + public void beginTransaction(final int isolationLevel) { + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#closeConnection() + */ + @Override + public void closeConnection() { + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#commitTransaction() + */ + @Override + public void commitTransaction() { + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#count(siena.Query) + */ + @Override + public int count(final Query query) { + return prepare(query, true).count(); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#delete(java.lang.Object) + */ + @Override + public void delete(final Object obj) { + final Class clazz = obj.getClass(); + + ClassInfo info = ClassInfo.getClassInfo(clazz); + Field idField = info.getIdField(); + Object idVal = Util.readField(obj, idField); + if (idVal == null) { + throw new SienaException("Object does not exist : " + obj); + } + + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + final Object id = getId(obj); + final DBObject query = new BasicDBObject(ID_FIELD, new ObjectId(id.toString())); + // LOGGER.info("Query to delete (by object): " + query); + collection.remove(query); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#delete(siena.Query) + */ + @Override + public int delete(final Query query) { + int count = 0; + final Class clazz = query.getQueriedClass(); + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Query to delete: " + query); + } + final DBCursor cursor = prepare(query, true); + while (cursor.hasNext()) { + final DBObject entity = cursor.next(); + collection.remove(entity); + count += 1; + } + return count; + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetch(siena.Query) + */ + @Override + public List fetch(final Query query) { + return map(query, 0, prepare(query, false)); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetch(siena.Query, int) + */ + @Override + public List fetch(final Query query, final int limit) { + return map(query, 0, prepare(query, false).limit(limit)); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetch(siena.Query, int, java.lang.Object) + */ + @Override + public List fetch(final Query query, final int limit, final Object offset) { + return map(query, 0, prepare(query, false).skip((Integer) offset).limit(limit)); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetchKeys(siena.Query) + */ + @Override + public List fetchKeys(final Query query) { + return map(query, 0, prepare(query, true)); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetchKeys(siena.Query, int) + */ + @Override + public List fetchKeys(final Query query, final int limit) { + return map(query, 0, prepare(query, true).limit(limit)); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#fetchKeys(siena.Query, int, java.lang.Object) + */ + @Override + public List fetchKeys(final Query query, final int limit, final Object offset) { + return map(query, 0, prepare(query, true).skip((Integer) offset).limit(limit)); + } + + /** + * Fill in the properties in the entity using the model object. + * + * @param obj + * is the model to use as the source + * @param entity + * is the entity to store as the target + */ + protected void fillEntity(final Object obj, final DBObject entity) { + final Class clazz = obj.getClass(); + + for (final Field field : ClassInfo.getClassInfo(clazz).updateFields) { + final String property = ClassInfo.getColumnNames(field)[0]; + Object value = readField(obj, field); + final Class fieldClass = field.getType(); + if (ClassInfo.isModel(fieldClass)) { + if (value == null) { + entity.put(property, null); + } + else { + final Object id = getId(value); + entity.put(property, id); + } + } + else { + if (value != null) { + if (field.getType() == Json.class) { + value = value.toString(); + } + else if (field.getAnnotation(Embedded.class) != null) { + value = JsonSerializer.serialize(value).toString(); + } + } + final Unindexed ui = field.getAnnotation(Unindexed.class); + if (ui == null) { + entity.put(property, value); + } + else { + entity.put(property, value); + } + } + } + } + + /** + * Fill in the properties of the model from stored entity. + * + * @param obj + * is model to fill in as the target + * @param entity + * is the entity used to fill the model as the source + */ + protected void fillModel(final Object obj, final DBObject entity) { + final Class clazz = obj.getClass(); + + for (final Field field : ClassInfo.getClassInfo(clazz).updateFields) { + field.setAccessible(true); + final String property = ClassInfo.getColumnNames(field)[0]; + try { + final Class fieldClass = field.getType(); + if (ClassInfo.isModel(fieldClass)) { + final Object objectId = entity.get(property); + if (objectId != null) { + final Object value = fieldClass.newInstance(); + final Field id = ClassInfo.getIdField(fieldClass); + setId(id, value, objectId); + field.set(obj, value); + } + } + else { + setFromObject(obj, field, entity.get(property)); + } + } + catch (final Exception e) { + throw new SienaException(e); + } + } + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#get(java.lang.Object) + */ + @Override + public void get(final Object obj) { + final Class clazz = obj.getClass(); + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + Object id = getId(obj); + try { + id = new ObjectId(id.toString()); + final DBObject entity = collection.findOne(id); + fillModel(obj, entity); + } + catch (final Exception e) { + throw new SienaException(e); + } + } + + /** + * Gets the database specified. MongoDB will create the database if it doesn't already exist. + * + * @return + */ + protected DB getDatabase() { + if (mongo != null) { + final DB db = mongo.getDB(databaseName); + if (db != null && db.isAuthenticated() == false && userName != null && password != null) { + if (db.authenticate(userName.trim(), password.trim().toCharArray()) == false) { + throw new RuntimeException("Authentication failed using the configured credentials"); + } + } + return db; + } + else { + throw new RuntimeException("MongoDB not initialized"); + } + } + + /** + * Get the ID of the entity. + * + * @param entity + * is the stored entity with the ID + * @return + */ + protected Object getId(final DBObject entity) { + return entity.get(ID_FIELD); + } + + /** + * Get the ID of the model. + * + * @param obj + * is the the model with the ID + * @return + */ + protected Object getId(final Object obj) { + try { + final Field f = ClassInfo.getIdField(obj.getClass()); + return f.get(obj); + } + catch (final Exception e) { + throw new SienaException(e); + } + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#init(java.util.Properties) + */ + @Override + public void init(final Properties p) { + final String hostNames = p.getProperty(PREFIX + "hostnames"); + final List addresses = new LinkedList(); + if (hostNames != null && hostNames.length() > 0) { + final String[] hosts = hostNames.trim().split(","); + for (final String host : hosts) { + ServerAddress address = null; + if (host.contains(":")) { + final String[] parts = host.trim().split(":"); + if (parts.length == 2) { + try { + address = new ServerAddress(parts[0], Integer.valueOf(parts[1])); + } + catch (final Exception e) { + throw new RuntimeException("Port number is not a valid number", e); + } + } + else { + throw new RuntimeException("Invalid host format. The format should be \"server:port\""); + } + } + else { + try { + address = new ServerAddress(host); + } + catch (final UnknownHostException e) { + throw new RuntimeException("Unknown host name specified", e); + } + } + addresses.add(address); + } + } + + final MongoOptions options = new MongoOptions(); + final String autoConnectRetry = p.getProperty(PREFIX + "autoConnectRetry"); + if (autoConnectRetry != null && autoConnectRetry.length() > 0) { + options.autoConnectRetry = Boolean.valueOf(autoConnectRetry.trim()); + } + final String connectionsPerHost = p.getProperty(PREFIX + "connectionsPerHost"); + if (connectionsPerHost != null && connectionsPerHost.length() > 0) { + options.connectionsPerHost = Integer.valueOf(connectionsPerHost.trim()); + } + final String connectTimeout = p.getProperty(PREFIX + "connectTimeout"); + if (connectTimeout != null && connectTimeout.length() > 0) { + options.connectTimeout = Integer.valueOf(connectTimeout.trim()); + } + final String maxWaitTime = p.getProperty(PREFIX + "maxWaitTime"); + if (maxWaitTime != null && maxWaitTime.length() > 0) { + options.maxWaitTime = Integer.valueOf(maxWaitTime.trim()); + } + final String socketTimeout = p.getProperty(PREFIX + "socketTimeout"); + if (socketTimeout != null && socketTimeout.length() > 0) { + options.socketTimeout = Integer.valueOf(socketTimeout.trim()); + } + final String threadsAllowedToBlockForConnectionMultiplier = p.getProperty(PREFIX + "threadsAllowedToBlockForConnectionMultiplier"); + if (threadsAllowedToBlockForConnectionMultiplier != null && threadsAllowedToBlockForConnectionMultiplier.length() > 0) { + options.threadsAllowedToBlockForConnectionMultiplier = Integer.valueOf(threadsAllowedToBlockForConnectionMultiplier.trim()); + } + if (p.containsKey(PREFIX + "databaseName")) { + this.databaseName = p.getProperty(PREFIX + "databaseName"); + } + if (p.containsKey(PREFIX + "userName")) { + this.userName = p.getProperty(PREFIX + "userName"); + } + if (p.containsKey(PREFIX + "password")) { + this.password = p.getProperty(PREFIX + "password"); + } + + if (this.databaseName == null || this.databaseName.length() == 0) { + throw new RuntimeException("Database name is required"); + } + if (addresses.size() == 0) { + throw new RuntimeException("At least one valid server address is required"); + } + + // Now create the MongoDB + this.mongo = new Mongo(addresses, options); + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#insert(java.lang.Object) + */ + @Override + public void insert(final Object obj) { + final Class clazz = obj.getClass(); + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + final DBObject entity = new BasicDBObject(); + + ClassInfo info = ClassInfo.getClassInfo(clazz); + Field idField = info.getIdField(); + Object idVal = Util.readField(obj, idField); + if (idVal != null) { + throw new SienaException("Same key object already exists : " + obj); + } + + fillEntity(obj, entity); + collection.insert(entity); + setId(ClassInfo.getIdField(clazz), obj, getId(entity)); + } + + /** + * Handles a query including setting the next offset on a query. + * + * @param + * @param query + * is the query to execute + * @param offset + * is the offset requested + * @param cursor + * is the MongoDB cursor + * @return + */ + @SuppressWarnings("unchecked") + private List map(final Query query, final int offset, final DBCursor cursor) { + final Class clazz = query.getQueriedClass(); + final List result = (List) mapEntities(cursor, clazz); + query.setNextOffset(offset + result.size()); + return result; + } + + /** + * Convert the stored entities into models. + * + * @param + * @param cursor + * is the MongoDB cursor that resulted from an executed query + * @param clazz + * is the model class to convert to + * @return a list of model objects + */ + protected List mapEntities(final DBCursor cursor, final Class clazz) { + final List list = new LinkedList(); + final Field id = ClassInfo.getIdField(clazz); + while (cursor.hasNext()) { + final DBObject entity = cursor.next(); + T obj; + try { + obj = clazz.newInstance(); + fillModel(obj, entity); + list.add(obj); + setId(id, obj, getId(entity)); + } + catch (final SienaException e) { + throw e; + } + catch (final Exception e) { + throw new SienaException(e); + } + } + return list; + } + + /** + * Create a MongoDB query using the Siena query. + * + * @param + * @param query + * is the Siena query + * @param keysOnly + * determines whether or not just keys should be returned (lighter weight query) + * @return a MongoDB cursor + */ + protected DBCursor prepare(final Query query, final boolean keysOnly) { + final Class clazz = query.getQueriedClass(); + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + + final BasicDBObject queryObject = new BasicDBObject(); + final List filters = query.getFilters(); + for (final QueryFilter filter : filters) { + QueryFilterSimple simpleFilter = (QueryFilterSimple) filter; + //final Field f = filter.field; + final Field f = simpleFilter.field; + final String propertyName = ClassInfo.getColumnNames(f)[0]; + //Object value = filter.value; + Object value = simpleFilter.value; + //final String op = operators.get(filter.operator); + final String op = operators.get(simpleFilter.operator); + + if (value != null && ClassInfo.isModel(value.getClass())) { + Object id = getId(value); + if (op != null) { + id = new BasicDBObject(op, id); + } + queryObject.put(propertyName, id); + } + else { + if (ClassInfo.isId(f)) { + value = new ObjectId(value.toString()); + if (op != null) { + value = new BasicDBObject(op, value); + } + queryObject.put(ID_FIELD, value); + } + else { + if (op != null) { + value = new BasicDBObject(op, value); + } + queryObject.put(propertyName, value); + } + } + } + + final BasicDBObject sort = new BasicDBObject(); + final List orders = query.getOrders(); + for (final QueryOrder order : orders) { + final Field f = order.field; + if (ClassInfo.isId(f)) { + sort.put(ID_FIELD, 1); + } + else { + sort.put(ClassInfo.getColumnNames(f)[0], order.ascending ? 1 : -1); + } + } + + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Query using: " + queryObject); + } + + DBCursor cursor = null; + if (keysOnly) { + cursor = collection.find(queryObject, new BasicDBObject(ID_FIELD, 1)); + } + else { + cursor = collection.find(queryObject); + } + if (sort.isEmpty() == false) { + cursor = cursor.sort(sort); + } + return cursor; + } + + /** + * Get the value of a field property from a model object. + * + * @param object + * is the model object + * @param field + * is the field to read + * @return + */ + private Object readField(final Object object, final Field field) { + field.setAccessible(true); + try { + return field.get(object); + } + catch (final Exception e) { + throw new SienaException(e); + } + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#rollbackTransaction() + */ + @Override + public void rollbackTransaction() { + } + + /** + * Set the field value on a model object. + * + * @param object + * is the model instance + * @param f + * is the field on the model + * @param value + * is the value to set on the model + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + private void setFromObject(final Object object, final Field f, final Object value) throws IllegalArgumentException, IllegalAccessException { + Util.setFromObject(object, f, value); + } + + /** + * Set the ID from an entity onto the appropriate ID field on the model. + * + * @param f + * is the field on the model + * @param obj + * is the model instance + * @param id + * is the id to set + */ + private void setId(final Field f, final Object obj, final Object id) { + try { + Object value = id; + if (f.getType() == String.class) { + value = value.toString(); + } + f.setAccessible(true); + f.set(obj, value); + } + catch (final Exception e) { + throw new SienaException(e); + } + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#supportedOperators() + */ + @Override + public String[] supportedOperators() { + return supportedOperators; + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#update(java.lang.Object) + */ + @Override + public void update(final Object obj) { + try { + final Class clazz = obj.getClass(); + final DBCollection collection = getDatabase().getCollection(ClassInfo.getClassInfo(clazz).tableName); + final DBObject entity = new BasicDBObject(); + fillEntity(obj, entity); + final BasicDBObject queryObject = new BasicDBObject(); + queryObject.put(ID_FIELD, new ObjectId(getId(obj).toString())); + collection.update(queryObject, entity); + } + catch (final SienaException e) { + throw e; + } + catch (final Exception e) { + throw new SienaException(e); + } + } + + /* + * (non-Javadoc) + * @see siena.PersistenceManager#save(java.lang.Object) + */ + @Override + public void save(Object obj) { + Class clazz = obj.getClass(); + ClassInfo info = ClassInfo.getClassInfo(clazz); + Field idField = info.getIdField(); + Object idVal = Util.readField(obj, idField); + if (idVal == null) { + insert(obj); + } else { + update(obj); + } + } + + @Override + public int save(Object... paramArrayOfObject) { + return save(Arrays.asList(paramArrayOfObject)); + } + + @Override + public int save(Iterable paramIterable) { + int result = 0; + for (Iterator it=paramIterable.iterator(); it.hasNext();) { + try { + save(it.next()); + result++; + } catch (Exception e) { + LOGGER.error(e); + } + } + return result; + } + + @Override + public int insert(Object... paramArrayOfObject) { + return insert(Arrays.asList(paramArrayOfObject)); + } + + @Override + public int insert(Iterable paramIterable) { + int result = 0; + for (Iterator it=paramIterable.iterator(); it.hasNext();) { + try { + insert(it.next()); + result++; + } catch (Exception e) { + LOGGER.warn(e); + } + } + return result; + } + + @Override + public int delete(Object... paramArrayOfObject) { + return delete(Arrays.asList(paramArrayOfObject)); + } + + @Override + public int delete(Iterable paramIterable) { + int result = 0; + for (Iterator it=paramIterable.iterator(); it.hasNext();) { + try { + delete(it.next()); + result++; + } catch (Exception e) { + LOGGER.warn(e); + } + } + return result; + } + + @Override + public int deleteByKeys(Class paramClass, Object... paramArrayOfObject) { + throw new UnsupportedOperationException(); + } + + @Override + public int deleteByKeys(Class paramClass, Iterable paramIterable) { + throw new UnsupportedOperationException(); + } + + @Override + public int get(Object... paramArrayOfObject) { + throw new UnsupportedOperationException(); + } + + @Override + public int get(Iterable paramIterable) { + throw new UnsupportedOperationException(); + } + + @Override + public T getByKey(Class paramClass, Object paramObject) { + throw new UnsupportedOperationException(); + } + + @Override + public List getByKeys(Class paramClass, Object... paramArrayOfObject) { + throw new UnsupportedOperationException(); + } + + @Override + public List getByKeys(Class paramClass, Iterable paramIterable) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Object... paramArrayOfObject) { + return update(Arrays.asList(paramArrayOfObject)); + } + + @Override + public int update(Iterable paramIterable) { + int result = 0; + for (Iterator it=paramIterable.iterator(); it.hasNext();) { + try { + update(it.next()); + result++; + } catch (Exception e) { + LOGGER.warn(e); + } + } + return result; + } + + @Override + public void beginTransaction() { + //do nothing + } + + @Override + public int update(Query paramQuery, Map paramMap) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable iter(Query paramQuery) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable iter(Query paramQuery, int paramInt) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable iter(Query paramQuery, int paramInt, Object paramObject) { + throw new UnsupportedOperationException(); + } + + @Override + public void paginate(Query paramQuery) { + throw new UnsupportedOperationException(); + } + + @Override + public void nextPage(Query paramQuery) { + throw new UnsupportedOperationException(); + } + + @Override + public void previousPage(Query paramQuery) { + throw new UnsupportedOperationException(); + } + + @Override + public PersistenceManagerAsync async() { + throw new UnsupportedOperationException(); + } +} diff --git a/source/src/test/java/siena/mongodb/MongoDBPersistenceManagerTest.java b/source/src/test/java/siena/mongodb/MongoDBPersistenceManagerTest.java new file mode 100644 index 0000000..3665b73 --- /dev/null +++ b/source/src/test/java/siena/mongodb/MongoDBPersistenceManagerTest.java @@ -0,0 +1,347 @@ +package siena.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Properties; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import siena.PersistenceManager; +import siena.PersistenceManagerFactory; +import siena.mongodb.model.SienaMongoDbTestModel; + +/** + * Test class for MongoDBPersistenceManager + * + * Before you run test cases, be sure run the mongod process in your localhost(127.0.0.1) + * and db name "test" doesn't have any important data. + * + * @author T.hagikura + * + */ +public class MongoDBPersistenceManagerTest { + + private static PersistenceManager pm; + + public static PersistenceManager createPersistenceManager(final List> classes) throws Exception { + final Properties p = new Properties(); + p.setProperty("siena.mongodb.hostnames", "127.0.0.1"); + p.setProperty("siena.mongodb.databaseName", "test"); + + final MongoDBPersistenceManager pm = new MongoDBPersistenceManager(); + pm.init(p); + + return pm; + } + + @Test + public void testInsertAndDelete() { + SienaMongoDbTestModel model = createTestInstance(); + pm.insert(model); + List models = SienaMongoDbTestModel.all().fetch(); + + assertEquals(1, models.size()); + SienaMongoDbTestModel registered = models.get(0); + assertTrue(model.equals(registered)); + + registered.delete(); + models = SienaMongoDbTestModel.all().fetch(); + + assertEquals(0, models.size()); + } + + @Test + public void testSaveAndDelete() { + SienaMongoDbTestModel model = createTestInstance(); + pm.save(model); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(1, models.size()); + + SienaMongoDbTestModel registered = models.get(0); + assertTrue(model.equals(registered)); + + registered.stringField = "updatedBySave"; + pm.save(registered); // will be updated because same _id object is already inserted. + + models = SienaMongoDbTestModel.all().fetch(); + assertEquals(1, models.size()); + + SienaMongoDbTestModel afterSave = models.get(0); + assertTrue(registered.equals(afterSave)); + + afterSave.delete(); + models = SienaMongoDbTestModel.all().fetch(); + + assertEquals(0, models.size()); + } + + @Test + public void testUpdate() { + SienaMongoDbTestModel model = createTestInstance(); + pm.insert(model); + List models = SienaMongoDbTestModel.all().fetch(); + + assertEquals(1, models.size()); + SienaMongoDbTestModel registered = models.get(0); + assertTrue(model.equals(registered)); + + Integer changedInt = model.integerField + 10; + String changedStr = "changed"; + registered.integerField = changedInt; + registered.stringField = changedStr; + + registered.update(); + + models = SienaMongoDbTestModel.all().fetch(); + assertEquals(1, models.size()); + SienaMongoDbTestModel updatedModel = models.get(0); + assertTrue(registered.equals(updatedModel)); + + updatedModel.delete(); + models = SienaMongoDbTestModel.all().fetch(); + assertEquals(0, models.size()); + } + + @Test + public void testInsertIterable() { + Collection modelsToInsert = new ArrayList(); + SienaMongoDbTestModel model = createTestInstance(); + SienaMongoDbTestModel model2 = createTestInstance(); + modelsToInsert.add(model); + modelsToInsert.add(model2); + int insertCount = pm.insert(modelsToInsert); + + assertEquals(2, insertCount); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(2, models.size()); + + int sameModelInsertCount = pm.insert(modelsToInsert); // can not be inserted because same _id objects exists + assertEquals(0, sameModelInsertCount); + models = SienaMongoDbTestModel.all().fetch(); + assertEquals(2, models.size()); + + deleteExistingModel(); + } + + @Test + public void testDeleteIterable() { + Collection modelsToInsert = new ArrayList(); + SienaMongoDbTestModel model = createTestInstance(); + SienaMongoDbTestModel model2 = createTestInstance(); + modelsToInsert.add(model); + modelsToInsert.add(model2); + int insertCount = pm.insert(modelsToInsert); + assertEquals(2, insertCount); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(2, models.size()); + + int deleteCount = pm.delete(modelsToInsert); + assertEquals(2, deleteCount); + models = SienaMongoDbTestModel.all().fetch(); + assertEquals(0, models.size()); + + deleteExistingModel(); + } + + @Test + public void testUpdateIterable() { + Collection modelsToInsert = new ArrayList(); + SienaMongoDbTestModel model = createTestInstance(); + SienaMongoDbTestModel model2 = createTestInstance(); + modelsToInsert.add(model); + modelsToInsert.add(model2); + int insertCount = pm.insert(modelsToInsert); + assertEquals(2, insertCount); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(2, models.size()); + + model.stringField = "updateField1"; + model2.stringField = "updateField2"; + int updateCount = pm.update(modelsToInsert); + assertEquals(2, updateCount); + models = SienaMongoDbTestModel.all().order("stringField").fetch(); + assertEquals(2, models.size()); + SienaMongoDbTestModel registered1 = models.get(0); + SienaMongoDbTestModel registered2 = models.get(1); + assertTrue(model.equals(registered1)); + assertTrue(model2.equals(registered2)); + + deleteExistingModel(); + } + + @Test + public void testStringFilter() { + SienaMongoDbTestModel model = createTestInstance(); + pm.insert(model); + List models = SienaMongoDbTestModel.all().filter("stringField", "dummyString").fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("stringField", TEST_STRING).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("stringField !=", TEST_STRING).fetch(); + assertEquals(0, models.size()); + + deleteExistingModel(); + } + + @Test + public void testIntegerFilter() { + SienaMongoDbTestModel model = createTestInstance(); + pm.insert(model); + List models = SienaMongoDbTestModel.all().filter("integerField", 0).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField", TEST_INTEGER).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField !=", TEST_INTEGER).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField >", TEST_INTEGER).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField <", TEST_INTEGER).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField >", TEST_INTEGER - 1).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField <", TEST_INTEGER + 1).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField >=", TEST_INTEGER).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField <=", TEST_INTEGER).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField >=", TEST_INTEGER + 1).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("integerField <=", TEST_INTEGER - 1).fetch(); + assertEquals(0, models.size()); + + deleteExistingModel(); + } + + @Test + public void testDateFilter() { + SienaMongoDbTestModel model = createTestInstance(); + pm.insert(model); + List models = SienaMongoDbTestModel.all().filter("dateField", new Date()).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField", TEST_DATE).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField !=", TEST_DATE).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField >", TEST_DATE).fetch(); + assertEquals(0, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField <", TEST_DATE).fetch(); + assertEquals(0, models.size()); + + Calendar nextDateCal = new GregorianCalendar(); + nextDateCal.setTime(TEST_DATE); + nextDateCal.add(Calendar.DATE, 1); + Date nextDate = nextDateCal.getTime(); + + models = SienaMongoDbTestModel.all().filter("dateField <", nextDate).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField >=", TEST_DATE).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField <=", TEST_DATE).fetch(); + assertEquals(1, models.size()); + + models = SienaMongoDbTestModel.all().filter("dateField >=", nextDate).fetch(); + assertEquals(0, models.size()); + + deleteExistingModel(); + } + + @Test + public void testOrder() { + Collection modelsToInsert = new ArrayList(); + SienaMongoDbTestModel model = createTestInstance(); + SienaMongoDbTestModel model2 = createTestInstance(); + model.stringField = "updateField1"; + model2.stringField = "updateField2"; + modelsToInsert.add(model); + modelsToInsert.add(model2); + int insertCount = pm.insert(modelsToInsert); + assertEquals(2, insertCount); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(2, models.size()); + + // get models in ascending order by stringField + models = SienaMongoDbTestModel.all().order("stringField").fetch(); + assertEquals(2, models.size()); + SienaMongoDbTestModel registered1 = models.get(0); + SienaMongoDbTestModel registered2 = models.get(1); + assertTrue(model.equals(registered1)); + assertTrue(model2.equals(registered2)); + + // get models in descending order by stringField + models = SienaMongoDbTestModel.all().order("-stringField").fetch(); + assertEquals(2, models.size()); + registered1 = models.get(0); + registered2 = models.get(1); + assertTrue(model2.equals(registered1)); + assertTrue(model.equals(registered2)); + + deleteExistingModel(); + } + + @BeforeClass + public static void setUpOnce() throws Exception { + pm = installPersistenceManager(); + SienaMongoDbTestModel.deleteAll(); + } + + @AfterClass + public static void tearDownOnce() { + SienaMongoDbTestModel.deleteAll(); + } + + private static final Date TEST_DATE = new Date(); + private static final String TEST_STRING = "testStr"; + private static final Integer TEST_INTEGER = Integer.valueOf(10); + private static final Long TEST_LONG = Long.valueOf(20L); + private static final Double TEST_DOUBLE = Double.valueOf(20.02); + + private static void deleteExistingModel() { + SienaMongoDbTestModel.deleteAll(); + List models = SienaMongoDbTestModel.all().fetch(); + assertEquals(0, models.size()); + } + + private static SienaMongoDbTestModel createTestInstance() { + SienaMongoDbTestModel model = new SienaMongoDbTestModel(); + model.dateField = TEST_DATE; + model.doubleField = TEST_DOUBLE; + model.stringField = TEST_STRING; + model.integerField = TEST_INTEGER; + model.longField = TEST_LONG; + return model; + } + + private static PersistenceManager installPersistenceManager() throws Exception { + PersistenceManager pm = createPersistenceManager(null); + PersistenceManagerFactory.install(pm, SienaMongoDbTestModel.class); + return pm; + } +} diff --git a/source/src/test/java/siena/mongodb/model/SienaMongoDbTestModel.java b/source/src/test/java/siena/mongodb/model/SienaMongoDbTestModel.java new file mode 100644 index 0000000..d9db026 --- /dev/null +++ b/source/src/test/java/siena/mongodb/model/SienaMongoDbTestModel.java @@ -0,0 +1,91 @@ +package siena.mongodb.model; + +import java.util.Date; +import java.util.List; + +import siena.Id; +import siena.Model; +import siena.Query; + +/** + * Only test Model class for MongoDBPersistenceManagerTest + * + * @author T.hagikura + * + */ +public final class SienaMongoDbTestModel extends Model { + @Id // in Siena-MongoDb, @Id annotation column must be Object not Long, since generated _id from java-mongo-driver contains alphabetical characters. + public Object id; + public String stringField; + public Integer integerField; + public Long longField; + public Double doubleField; + public Date dateField; + + public static Query all() { + return Model.all(SienaMongoDbTestModel.class); + } + + public static void deleteAll() { + List models = all().fetch(); + for (SienaMongoDbTestModel model : models) { + model.delete(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((dateField == null) ? 0 : dateField.hashCode()); + result = prime * result + ((doubleField == null) ? 0 : doubleField.hashCode()); + result = prime * result + ((integerField == null) ? 0 : integerField.hashCode()); + result = prime * result + ((longField == null) ? 0 : longField.hashCode()); + result = prime * result + ((stringField == null) ? 0 : stringField.hashCode()); + return result; + } + + @Override + public String toString() { + return "SienaMongoDbTestModel [id=" + id + ", stringField=" + stringField + ", integerField=" + integerField + + ", longField=" + longField + ", doubleField=" + doubleField + ", dateField=" + dateField + "]"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + SienaMongoDbTestModel other = (SienaMongoDbTestModel) obj; + if (dateField == null) { + if (other.dateField != null) + return false; + } else if (!dateField.equals(other.dateField)) + return false; + if (doubleField == null) { + if (other.doubleField != null) + return false; + } else if (!doubleField.equals(other.doubleField)) + return false; + if (integerField == null) { + if (other.integerField != null) + return false; + } else if (!integerField.equals(other.integerField)) + return false; + if (longField == null) { + if (other.longField != null) + return false; + } else if (!longField.equals(other.longField)) + return false; + if (stringField == null) { + if (other.stringField != null) + return false; + } else if (!stringField.equals(other.stringField)) + return false; + return true; + } + +}