diff --git a/build.gradle b/build.gradle index da1405c..83d0f41 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ ext { repositories { maven { url "http://repo.springsource.org/libs-snapshot" } + mavenCentral() } dependencies { @@ -25,6 +26,7 @@ dependencies { compile("org.springframework:spring-core:$springVersion") compile("org.springframework:spring-beans:$springVersion") compile("org.springframework:spring-context:$springVersion") + optional("org.springframework.data:spring-data-mongodb:1.2.1.RELEASE") optional("org.springframework:spring-jdbc:$springVersion") optional("org.springframework:spring-jms:$springVersion") optional("org.springframework:spring-web:$springVersion") @@ -44,6 +46,11 @@ dependencies { testCompile("junit:junit:4.10") testRuntime("org.hsqldb:hsqldb-j5:2.2.4") testRuntime("log4j:log4j:1.2.16") + + // MongoDB test + testCompile("de.flapdoodle.embed:de.flapdoodle.embed.mongo:1.33") + testCompile("com.google.guava:guava:14.0.1") + testCompile("org.slf4j:slf4j-api:1.7.5") } tasks.withType(ScalaCompile) { diff --git a/src/main/scala/org/springframework/scala/data/mongodb/core/MongoConversions.scala b/src/main/scala/org/springframework/scala/data/mongodb/core/MongoConversions.scala new file mode 100644 index 0000000..4105f6b --- /dev/null +++ b/src/main/scala/org/springframework/scala/data/mongodb/core/MongoConversions.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * 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.springframework.scala.data.mongodb.core + +import com.mongodb.{DBCollection, DB, DBObject} +import org.springframework.data.mongodb.core.{DbCallback, DocumentCallbackHandler, CollectionCallback} + +/** + * A collection of implicit conversions useful while working with the Spring Data MongoDB. + * + * @author Henryk Konsek + */ +object MongoConversions { + + /** + * Converts function into [[org.springframework.data.mongodb.core.DocumentCallbackHandler]]. + * + * @param dbObjectHandler [[com.mongodb.DBObject]] => [[scala.Unit]] function to be converted into + * [[org.springframework.data.mongodb.core.DocumentCallbackHandler]] + * @return [[com.mongodb.DBObject]] => [[scala.Unit]] function wrapped into + * [[org.springframework.data.mongodb.core.DocumentCallbackHandler]] + */ + implicit def asDocumentCallback(dbObjectHandler: DBObject => Unit): DocumentCallbackHandler = + new DocumentCallbackHandler { + def processDocument(dbObject: DBObject) { + dbObjectHandler(dbObject) + } + } + + /** + * Converts function into [[org.springframework.data.mongodb.core.DbCallback]]. + * + * @param dbHandler [[com.mongodb.DB]] => T function to be converted to + * [[org.springframework.data.mongodb.core.DbCallback]] + * @tparam T return type of the [[org.springframework.data.mongodb.core.DbCallback]] + * @return [[com.mongodb.DB]] => T function wrapped into [[org.springframework.data.mongodb.core.DbCallback]] + */ + implicit def asDatabaseCallback[T](dbHandler: DB => T): DbCallback[T] = + new DbCallback[T] { + def doInDB(db: DB): T = dbHandler(db) + } + + /** + * Converts function into [[org.springframework.data.mongodb.core.CollectionCallback]]. + * + * @param collectionHandler [[com.mongodb.DBCollection]] => T function to be converted into the + * [[org.springframework.data.mongodb.core.CollectionCallback]] + * @tparam T return type of the [[org.springframework.data.mongodb.core.CollectionCallback]] + * @return [[com.mongodb.DBCollection]] => T function wrapped into + * [[org.springframework.data.mongodb.core.CollectionCallback]] + */ + implicit def asCollectionCallback[T](collectionHandler: DBCollection => T): CollectionCallback[T] = + new CollectionCallback[T] { + def doInCollection(collection: DBCollection): T = collectionHandler(collection) + } + +} \ No newline at end of file diff --git a/src/main/scala/org/springframework/scala/data/mongodb/core/MongoTemplate.scala b/src/main/scala/org/springframework/scala/data/mongodb/core/MongoTemplate.scala new file mode 100644 index 0000000..29e7c1b --- /dev/null +++ b/src/main/scala/org/springframework/scala/data/mongodb/core/MongoTemplate.scala @@ -0,0 +1,234 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * 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.springframework.scala.data.mongodb.core + +import com.mongodb._ +import MongoConversions._ +import org.springframework.data.mongodb.core.{MongoTemplate => JavaMongoTemplate, FindAndModifyOptions, IndexOperations, CollectionOptions} +import org.springframework.data.mongodb.core.query.{Criteria, Update, NearQuery, Query} +import org.springframework.scala.util.TypeTagUtils._ +import org.springframework.data.mongodb.core.geo.GeoResults +import org.springframework.data.mongodb.core.mapreduce.{GroupByResults, GroupBy, MapReduceOptions, MapReduceResults} +import scala.collection.JavaConversions._ +import scala.reflect.ClassTag + +/** + * Scala friendly wrapper around the [[org.springframework.data.mongodb.core.MongoTemplate]] instance. Takes advantage + * of functions and Scala types, and exposing only the most commonly required operations in order to simplify the + * template usage. + * + * @param mongoTemplate [[org.springframework.data.mongodb.core.MongoTemplate]] instance to be wrapped + * + * @author Henryk Konsek + */ +class MongoTemplate(mongoTemplate: JavaMongoTemplate) { + + def executeCommand(jsonCommand: String): CommandResult = + mongoTemplate.executeCommand(jsonCommand) + + def executeCommand(command: DBObject): CommandResult = + mongoTemplate.executeCommand(command) + + def executeCommand(command: DBObject, options: Int): CommandResult = + mongoTemplate.executeCommand(command, options) + + def executeQuery(query: Query, collectionName: String)(dbObjectCallback: DBObject => Unit) { + mongoTemplate.executeQuery(query, collectionName, dbObjectCallback) + } + + def execute[T](dbCallback: DB => T): T = + mongoTemplate.execute(dbCallback) + + def execute[T](entityClass: Class[_])(collectionCallback: DBCollection => T): T = + mongoTemplate.execute(entityClass, collectionCallback) + + def execute[T](collectionName: String)(collectionCallback: DBCollection => T): T = + mongoTemplate.execute(collectionName, collectionCallback) + + def executeInSession[T](dbCallback: DB => T): T = + mongoTemplate.executeInSession(dbCallback) + + def createCollection[T: ClassTag]: DBCollection = + mongoTemplate.createCollection(typeToClass[T]) + + def createCollection[T: ClassTag](collectionOptions: CollectionOptions) = + mongoTemplate.createCollection(typeToClass[T], collectionOptions) + + def createCollection(collectionName: String): DBCollection = + mongoTemplate.createCollection(collectionName) + + def createCollection(collectionName: String, collectionOptions: CollectionOptions): DBCollection = + mongoTemplate.createCollection(collectionName, collectionOptions) + + def getCollection(collectionName: String): DBCollection = + mongoTemplate.getCollection(collectionName) + + def collectionExists[T: ClassTag]: Boolean = + mongoTemplate.collectionExists(typeToClass[T]) + + def collectionExists(collectionName: String): Boolean = + mongoTemplate.collectionExists(collectionName) + + def dropCollection[T: ClassTag]() { + mongoTemplate.dropCollection(typeToClass[T]) + } + + def dropCollection(collectionName: String) { + mongoTemplate.dropCollection(collectionName) + } + + def indexOps(collectionName: String): IndexOperations = + mongoTemplate.indexOps(collectionName) + + def indexOps[T: ClassTag]: IndexOperations = + mongoTemplate.indexOps(typeToClass[T]) + + def findOne[T: ClassTag](query: Query): Option[T] = + Option(mongoTemplate.findOne(query, typeToClass[T])) + + def findOne[T: ClassTag](query: Query, collectionName: String): Option[T] = + Option(mongoTemplate.findOne(query, typeToClass[T], collectionName)) + + def find[T: ClassTag](query: Query): Seq[T] = + mongoTemplate.find(query, typeToClass[T]) + + def find[T: ClassTag](query: Query, collectionName: String): Seq[T] = + mongoTemplate.find(query, typeToClass[T], collectionName) + + def findById[T: ClassTag](id: Any): Option[T] = + Option(mongoTemplate.findById(id, typeToClass[T])) + + def findById[T: ClassTag](id: Any, collectionName: String): Option[T] = + Option(mongoTemplate.findById(id, typeToClass[T], collectionName)) + + def geoNear[T: ClassTag](near: NearQuery): GeoResults[T] = + mongoTemplate.geoNear(near, typeToClass[T]) + + def geoNear[T: ClassTag](near: NearQuery, collectionName: String): GeoResults[T] = + mongoTemplate.geoNear(near, typeToClass[T], collectionName) + + def findAndModify[T: ClassTag](query: Query, update: Update): Option[T] = + Option(mongoTemplate.findAndModify(query, update, typeToClass[T])) + + def findAndModify[T: ClassTag](query: Query, update: Update, collectionName: String): Option[T] = + Option(mongoTemplate.findAndModify(query, update, typeToClass[T], collectionName)) + + def findAndModify[T: ClassTag](query: Query, update: Update, options: FindAndModifyOptions): Option[T] = + Option(mongoTemplate.findAndModify(query, update, options, typeToClass[T])) + + def findAndModify[T: ClassTag](query: Query, update: Update, options: FindAndModifyOptions, collectionName: String): Option[T] = + Option(mongoTemplate.findAndModify(query, update, options, typeToClass[T], collectionName)) + + def findAndRemove[T: ClassTag](query: Query): Option[T] = + Option(mongoTemplate.findAndRemove(query, typeToClass[T])) + + def findAndRemove[T: ClassTag](query: Query, collectionName: String): Option[T] = + Option(mongoTemplate.findAndRemove(query, typeToClass[T], collectionName)) + + def count[T: ClassTag](query: Option[Query]): Long = + mongoTemplate.count(query.getOrElse(null), typeToClass[T]) + + def count(query: Option[Query], collectionName: String): Long = + mongoTemplate.count(query.getOrElse(null), collectionName) + + def insert(objectToSave: Any) { + mongoTemplate.insert(objectToSave) + } + + def insert(objectToSave: Any, collectionName: String) { + mongoTemplate.insert(objectToSave, collectionName) + } + + def insert[T: ClassTag](batchToSave: Seq[_ <: AnyRef]) { + mongoTemplate.insert(batchToSave, typeToClass[T]) + } + + def insert(batchToSave: Seq[_ <: AnyRef], collectionName: String) { + mongoTemplate.insert(batchToSave, collectionName) + } + + def insert(batchToSave: Seq[_ <: AnyRef]) { + mongoTemplate.insert(batchToSave) + } + + def save(objectToSave: Any) { + mongoTemplate.save(objectToSave) + } + + def save(objectToSave: Any, collectionName: String) { + mongoTemplate.save(objectToSave, collectionName) + } + + def upsert[T: ClassTag](query: Query, update: Update): WriteResult = + mongoTemplate.upsert(query, update, typeToClass[T]) + + def upsert(query: Query, update: Update, collectionName: String): WriteResult = + mongoTemplate.upsert(query, update, collectionName) + + def updateFirst[T: ClassTag](query: Query, update: Update): WriteResult = + mongoTemplate.updateFirst(query, update, typeToClass[T]) + + def updateFirst(query: Query, update: Update, collectionName: String): WriteResult = + mongoTemplate.updateFirst(query, update, collectionName) + + def updateMulti[T: ClassTag](query: Query, update: Update): WriteResult = + mongoTemplate.updateMulti(query, update, typeToClass[T]) + + def updateMulti(query: Query, update: Update, collectionName: String): WriteResult = + mongoTemplate.updateMulti(query, update, collectionName) + + def remove(document: Any) { + mongoTemplate.remove(document) + } + + def remove(document: Any, collection: String) { + mongoTemplate.remove(document, collection) + } + + def remove[T: ClassTag](query: Query) { + mongoTemplate.remove(query, typeToClass[T]) + } + + def remove(query: Query, collectionName: String) { + mongoTemplate.remove(query, collectionName) + } + + def findAll[T: ClassTag]: Seq[T] = + mongoTemplate.findAll(typeToClass[T]) + + def findAll[T: ClassTag](collectionName: String) = + mongoTemplate.findAll(typeToClass[T], collectionName) + + def mapReduce[T: ClassTag](inputCollectionName: String, mapFunction: String, reduceFunction: String): MapReduceResults[T] = + mongoTemplate.mapReduce(inputCollectionName, mapFunction, reduceFunction, typeToClass[T]) + + def mapReduce[T: ClassTag](inputCollectionName: String, mapFunction: String, reduceFunction: String, options: MapReduceOptions): MapReduceResults[T] = + mongoTemplate.mapReduce(inputCollectionName, mapFunction, reduceFunction, options, typeToClass[T]) + + def mapReduce[T: ClassTag](query: Query, inputCollectionName: String, mapFunction: String, reduceFunction: String): MapReduceResults[T] = + mongoTemplate.mapReduce(query, inputCollectionName, mapFunction, reduceFunction, typeToClass[T]) + + def mapReduce[T: ClassTag](query: Query, inputCollectionName: String, mapFunction: String, reduceFunction: String, options: MapReduceOptions): MapReduceResults[T] = + mongoTemplate.mapReduce(query, inputCollectionName, mapFunction, reduceFunction, options, typeToClass[T]) + + def group[T: ClassTag](inputCollectionName: String, groupBy: GroupBy): GroupByResults[T] = + mongoTemplate.group(inputCollectionName, groupBy, typeToClass[T]) + + def group[T: ClassTag](criteria: Criteria, inputCollectionName: String, groupBy: GroupBy): GroupByResults[T] = + mongoTemplate.group(criteria, inputCollectionName, groupBy, typeToClass[T]) + +} \ No newline at end of file diff --git a/src/test/scala/org/springframework/scala/data/mongodb/core/MongoTemplateTests.scala b/src/test/scala/org/springframework/scala/data/mongodb/core/MongoTemplateTests.scala new file mode 100644 index 0000000..8d7289c --- /dev/null +++ b/src/test/scala/org/springframework/scala/data/mongodb/core/MongoTemplateTests.scala @@ -0,0 +1,205 @@ +/* + * Copyright 2011-2013 the original author or authors. + * + * 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.springframework.scala.data.mongodb.core + +import com.mongodb.{DBCollection, DB, BasicDBObject, Mongo} +import de.flapdoodle.embed.mongo.config.MongodConfig +import de.flapdoodle.embed.mongo.distribution.Version +import de.flapdoodle.embed.process.runtime.Network.localhostIsIPv6 +import de.flapdoodle.embed.mongo.MongodStarter.getDefaultInstance +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfter, FunSuite} +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Criteria.where +import org.springframework.data.mongodb.core.{MongoTemplate => JavaMongoTemplate, CollectionOptions} + +@RunWith(classOf[JUnitRunner]) +class MongoTemplateTests extends FunSuite with BeforeAndAfter with BeforeAndAfterAll { + + // Mongo fixtures + + val mongoConfig = new MongodConfig(Version.Main.PRODUCTION, 27017, localhostIsIPv6) + val mongoDaemon = getDefaultInstance.prepare(mongoConfig).start() + + val mongoTemplate = new MongoTemplate(new JavaMongoTemplate(new Mongo(), "testDb")) + + override def afterAll(configMap: Map[String, Any]) { + mongoDaemon.stop() + } + + // Data fixtures + + val fooCollectionName = "foo" + + val foo = Foo("bar") + + before { + mongoTemplate.dropCollection[Foo]() + } + + // Tests + + test("Should execute json command.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.executeCommand("{count: 'foo'}").getLong("n") + } + } + + test("Should execute DBObject command.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.executeCommand(new BasicDBObject("count", fooCollectionName)).getLong("n") + } + } + + test("Should execute with DB callback.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.execute { + db: DB => + db.getCollection(fooCollectionName).count + } + } + } + + test("Should execute with DBCollection callback and entity class.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.execute(classOf[Foo]) { + col: DBCollection => + col.count() + } + } + } + + test("Should execute with DBCollection callback and collection name.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.execute(fooCollectionName) { + col: DBCollection => + col.count() + } + } + } + + test("Should execute in session.") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.executeInSession { + db: DB => + db.getCollection(fooCollectionName).count + } + } + } + + test("Should create collection from type.") { + expectResult(true) { + mongoTemplate.dropCollection[Foo]() + mongoTemplate.createCollection[Foo] + mongoTemplate.collectionExists[Foo] + } + } + + test("Should create collection from type with options.") { + val maxCollectionSize = 1 + expectResult(maxCollectionSize) { + val fooCollectionOptions = new CollectionOptions(1, maxCollectionSize, true) + mongoTemplate.createCollection[Foo](fooCollectionOptions) + mongoTemplate.insert(foo) + mongoTemplate.insert(foo) + mongoTemplate.count[Foo](None) + } + } + + test("Should create collection from String.") { + expectResult(true) { + val collectionName = "bar" + mongoTemplate.createCollection(collectionName) + mongoTemplate.collectionExists(collectionName) + } + } + + test("Should create collection from String with options.") { + val maxCollectionSize = 1 + expectResult(maxCollectionSize) { + val fooCollectionOptions = new CollectionOptions(1, maxCollectionSize, true) + mongoTemplate.createCollection(fooCollectionName, fooCollectionOptions) + mongoTemplate.insert(foo) + mongoTemplate.insert(foo) + mongoTemplate.count[Foo](None) + } + } + + test("Should get collection") { + expectResult(1) { + mongoTemplate.insert(foo) + mongoTemplate.getCollection(fooCollectionName).getCount + } + } + + test("Should find one.") { + expectResult(Some(foo)) { + mongoTemplate.insert(foo) + mongoTemplate.findOne[Foo](new Query(where("bar").is(foo.bar))) + } + } + + test("Should find one (none).") { + expectResult(None) { + mongoTemplate.findOne[Foo](new Query(where("bar").is("randomValue"))) + } + } + + test("Should find all.") { + expectResult(Seq(foo)) { + mongoTemplate.insert(foo) + mongoTemplate.findAll[Foo] + } + } + + test("Should find all (empty).") { + expectResult(Seq()) { + mongoTemplate.findAll[Foo] + } + } + + test("Should find seq by query.") { + expectResult(Seq(foo)) { + mongoTemplate.insert(foo) + mongoTemplate.find[Foo](new Query(where("bar").is(foo.bar))) + } + } + + test("Should find seq by query (empty).") { + expectResult(Seq()) { + mongoTemplate.find[Foo](new Query(where("bar").is("randomValue"))) + } + } + + test("Should drop collection.") { + expectResult(0) { + mongoTemplate.insert(foo) + mongoTemplate.dropCollection[Foo]() + mongoTemplate.count[Foo](None) + } + } + +} + +case class Foo(bar: String) \ No newline at end of file