Permalink
Browse files

Added interface for configuring an ObjectMapper

  • Loading branch information...
jroper committed Mar 27, 2012
1 parent 328ac17 commit 780a07d76c8848647bc1546c1b20e3aa331e118f
View
@@ -59,13 +59,36 @@ Configuration
mongodb.credentials="user:pass"
# Configure the servers
mongodb.servers=host1.example.com:27017,host2.example.com,host3.example.com:19999
+ # Configure a custom ObjectMapper to use
+ mongodb.objectMapperConfigurer=foo.bar.MyObjectMapperConfigurer
The database name defaults to play. The servers defaults to localhost. Specifying a port number is optional, it defaults to the default MongoDB port. If you specify one server, MongoDB will be used as a single server, if you specify multiple, it will be used as a replica set.
+Configuring the object mapper
+-----------------------------
+
+If you specify an object mapper configurer, it must be a class with a noarg constructor that implements the trait ``ObjectMapperConfigurer``. This trait has two methods, one for configuring the global object mapper that will be used for all collections, and another for configuring object mappers per collection. An example implementation might look like this:
+
+ class MyObjectMapperConfigurer extends ObjectMapperConfigurer {
+ def configure(defaultMapper: ObjectMapper) =
+ defaultMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ def configure(globalMapper: ObjectMapper, collectionName: String, objectType: Class[_], keyType: Class[_]) = {
+ if (collectionName == "something") {
+ // Because object mapper is mutable, and doesn't provide a simple way to just copy it's configuration, if
+ // you want to configure one for a specific collection, you probably have to create a new one from scratch.
+ val mapper = configure(MongoJacksonMapperModule.configure(new ObjectMapper).withModule(new DefaultScalaModule))
+ return mapper.configure(DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
+ }
+ return globalMapper
+ }
+ }
+
Features
--------
* Manages lifecycle of MongoDB connection pool
* Caches JacksonDBCollection instances, so looking up a JacksonDBCollection is cheap
* Configures Jackson to use the FasterXML ``DefaultScalaModule``, so scala mapping works out of the box.
+* Allows configuring a custom object mapper
View
@@ -11,7 +11,7 @@ scalaVersion := "2.9.1"
// Dependencies
libraryDependencies ++= Seq(
- "net.vz.mongodb.jackson" % "mongo-jackson-mapper" % "1.4.0",
+ "net.vz.mongodb.jackson" % "mongo-jackson-mapper" % "1.4.1",
"com.fasterxml" % "jackson-module-scala" % "1.9.3",
"play" %% "play" % "2.0"
)
@@ -47,7 +47,7 @@ class MongoDBPlugin(val app: Application) extends Plugin {
private val cache = new ConcurrentHashMap[(String, Class[_], Class[_]), JacksonDBCollection[_, _]]()
- private lazy val (mongo, db, mapper) = {
+ private lazy val (mongo, db, globalMapper, configurer) = {
// Configure MongoDB
// DB server string is comma separated, with optional port number after a colon
val mongoDbServers = app.configuration.getString("mongodb.servers").getOrElse("localhost")
@@ -64,14 +64,14 @@ class MongoDBPlugin(val app: Application) extends Plugin {
val mongo = mongoDbServers.split(',') map {
// Convert each server string to a ServerAddress, matching based on arguments
_.split(':') match {
- case Array(host: String) => new ServerAddress(host)
- case Array(host: String, Port(port)) => new ServerAddress(host, port)
+ case Array(host) => new ServerAddress(host)
+ case Array(host, Port(port)) => new ServerAddress(host, port)
case _ => throw new IllegalArgumentException("mongodb.servers must be a comma separated list of hostnames with" +
" optional port numbers after a colon, eg 'host1.example.org:1111,host2.example.org'")
}
} match {
- case Array(single: ServerAddress) => new Mongo(single)
- case multiple: Array[ServerAddress] => new Mongo(multiple.toList)
+ case Array(single) => new Mongo(single)
+ case multiple => new Mongo(multiple.toList)
}
// Load database
@@ -92,19 +92,24 @@ class MongoDBPlugin(val app: Application) extends Plugin {
}
}
- // Configure the object mapper
- var mapper = new ObjectMapper;
- mapper = mapper.withModule(MongoJacksonMapperModule.INSTANCE)
- // This is needed by mongo jackson mapper and the module system doesn't support adding these
- mapper.setHandlerInstantiator(new MongoJacksonHandlerInstantiator(new MongoAnnotationIntrospector(mapper.getDeserializationConfig)))
- mapper.withModule(new DefaultScalaModule)
- (mongo, db, mapper)
+ // Look up the object mapper configurer
+ val configurer = app.configuration.getString("mongodb.objectMapperConfigurer") map {
+ Class.forName(_).asSubclass(classOf[ObjectMapperConfigurer]).newInstance
+ }
+
+ // Configure the default object mapper
+ val defaultMapper = MongoJacksonMapperModule.configure(new ObjectMapper).withModule(new DefaultScalaModule)
+
+ val globalMapper = configurer map {_.configure(defaultMapper)} getOrElse defaultMapper
+
+ (mongo, db, globalMapper, configurer)
}
def getCollection[T, K](name: String, entityType: Class[T], keyType: Class[K]): JacksonDBCollection[T, K] = {
if (cache.containsKey((name, entityType, keyType))) {
cache.get((name, entityType, keyType)).asInstanceOf[JacksonDBCollection[T, K]]
} else {
+ val mapper = configurer map {_.configure(globalMapper, name, entityType, keyType)} getOrElse globalMapper
val coll = JacksonDBCollection.wrap(db.getCollection(name), entityType, keyType, mapper)
cache.putIfAbsent((name, entityType, keyType), coll)
coll
@@ -0,0 +1,34 @@
+package play.modules.mongodb.jackson
+
+import org.codehaus.jackson.map.ObjectMapper
+
+/**
+ * Configures an ObjectMapper. Implementations must have a no argument constructor.
+ */
+trait ObjectMapperConfigurer {
+
+ /**
+ * Configure the given ObjectMapper for global use. This will be called once, on application startup. You may either
+ * modify the object mapper passed in, or create a completely new one. If you create a completely new one, then you
+ * need to ensure that the MongoJacksonMapperModule is registered, using the MongoJacksonMapperModule.configure()
+ * method.
+ *
+ * @param defaultMapper The default object mapper
+ * @return The object mapper to use globally
+ */
+ def configure(defaultMapper: ObjectMapper): ObjectMapper
+
+ /**
+ * Configure an ObjectMapper for use by a particular collection, type and key combination. This will be called once
+ * for each collection name, object type and key type looked up. Note that since ObjectMapper is mutable, if you want
+ * to create a configuration specific to a particular collection, then you should probably be creating a new
+ * ObjectMapper, ensuring that MongoJacksonMapperModule is registered.
+ *
+ * @param globalMapper The global object mapper
+ * @param collectionName The name of the collection being mapped
+ * @param objectType The type of the object the collection is being mapped to
+ * @param keyType The type of the key of the object
+ * @return The object mapper to use for this collection, type and key type combination
+ */
+ def configure(globalMapper: ObjectMapper, collectionName: String, objectType: Class[_], keyType: Class[_]): ObjectMapper
+}
@@ -4,10 +4,11 @@ import org.specs2.mutable._
import play.api.test._
import play.api.test.Helpers._
import net.vz.mongodb.jackson.Id
-import com.mongodb.Mongo
import util.Random
import reflect.BeanProperty
import org.codehaus.jackson.annotate.JsonProperty
+import org.codehaus.jackson.map.{DeserializationConfig, ObjectMapper}
+import com.mongodb.{BasicDBObject, Mongo}
class MongoDBSpec extends Specification {
@@ -68,22 +69,65 @@ class MongoDBSpec extends Specification {
result.values must_== obj.values
}
}
+
+ "use a custom global configurer when configured" in new Setup {
+ implicit val app = fakeApp(Map("mongodb.objectMapperConfigurer" -> classOf[MockGlobalConfigurer].getName))
+ running(app) {
+ val coll = MongoDB.collection(collName, classOf[MockObject], classOf[String])
+ coll.getDbCollection.save(new BasicDBObject("_id", "someid").append("values", "single"))
+ // This will throw an exception if the custom object mapper isn't used
+ val result = coll.findOneById("someid")
+ result must_!= null
+ result.id must_== "someid"
+ result.values must_== List("single")
+ }
+ }
+
+ "use a custom per collection configurer when configured" in new Setup {
+ implicit val app = fakeApp(Map("mongodb.objectMapperConfigurer" -> classOf[MockPerCollectionConfigurer].getName))
+ running(app) {
+ val coll = MongoDB.collection(collName, classOf[MockObject], classOf[String])
+ coll.getDbCollection.save(new BasicDBObject("_id", "someid").append("values", "single"))
+ // This will throw an exception if the custom object mapper isn't used
+ val result = coll.findOneById("someid")
+ result must_!= null
+ result.id must_== "someid"
+ result.values must_== List("single")
+ }
+ }
}
trait Setup extends After {
def fakeApp(o: Map[String, String]) = {
FakeApplication(additionalConfiguration = o ++ Map("ehcacheplugin" -> "disabled",
"mongodbJacksonMapperCloseOnStop" -> "disabled"))
}
+
val collName = "mockcoll" + new Random().nextInt(10000)
+
def after {
val mongo = new Mongo()
mongo.getDB("play").getCollection(collName).drop()
}
}
+
}
class MockObject(@Id val id: String,
@JsonProperty("values") @BeanProperty val values: List[String]) {
@Id def getId = id;
+}
+
+class MockGlobalConfigurer extends ObjectMapperConfigurer {
+ def configure(defaultMapper: ObjectMapper) =
+ defaultMapper.configure(DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
+
+ def configure(globalMapper: ObjectMapper, collectionName: String, objectType: Class[_], keyType: Class[_]) = globalMapper
+}
+
+class MockPerCollectionConfigurer extends ObjectMapperConfigurer {
+ def configure(defaultMapper: ObjectMapper) = defaultMapper
+
+ def configure(globalMapper: ObjectMapper, collectionName: String, objectType: Class[_], keyType: Class[_]) =
+ globalMapper.configure(DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
}

0 comments on commit 780a07d

Please sign in to comment.