Skip to content

Instantly share code, notes, and snippets.

@rhodri
Created August 5, 2014 14:35
Show Gist options
  • Save rhodri/2c067c1de0707260ebc0 to your computer and use it in GitHub Desktop.
Save rhodri/2c067c1de0707260ebc0 to your computer and use it in GitHub Desktop.
Adapter for ReactiveMongo and spray-json. Includes required Collection and QueryBuilder classes. Derived from https://gist.github.com/nevang/4690568
import scala.util._
import java.nio.ByteBuffer
import org.apache.commons.codec.binary.{Base64, Hex}
import org.jboss.netty.buffer.ChannelBuffers
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ DateTime, DateTimeZone }
import reactivemongo.api.collections._
import reactivemongo.api._
import reactivemongo.bson._
import reactivemongo.bson.buffer.ReadableBuffer
import spray.json._
trait JsonGenericHandlers extends GenericHandlers[JsObject, JsonReader, JsonWriter] {
object JsonBufferReader extends BufferReader[JsObject] {
override def read(buffer: ReadableBuffer) = {
JsBSONReader.readObject(BSONDocument.read(buffer))
}
}
object JsonBufferWriter extends BufferWriter[JsObject] {
override def write[B <: reactivemongo.bson.buffer.WritableBuffer](document: JsObject, buffer: B): B = {
BSONDocument.write(JsBSONWriter.writeObject(document), buffer)
buffer
}
}
override def StructureBufferReader = JsonBufferReader
override def StructureBufferWriter = JsonBufferWriter
case class JsonStructureReader[T](reader: JsonReader[T]) extends GenericReader[JsObject, T] {
override def read(doc: JsObject) = reader.read(doc)
}
case class JsonStructureWriter[T](writer: JsonWriter[T]) extends GenericWriter[T, JsObject] {
override def write(t: T) = writer.write(t).asJsObject
}
override def StructureReader[T](reader: JsonReader[T]) = JsonStructureReader(reader)
override def StructureWriter[T](writer: JsonWriter[T]) = JsonStructureWriter(writer)
}
trait SprayJsonCollections {
case class SprayJsonCollection(db : DB, name : String, failoverStrategy : FailoverStrategy)
extends GenericCollection[JsObject, JsonReader, JsonWriter]
with JsonGenericHandlers {
def genericQueryBuilder = SprayJsonQueryBuilder(this, failoverStrategy)
}
case class SprayJsonQueryBuilder(
collection: Collection,
failover: FailoverStrategy,
queryOption: Option[JsObject] = None,
sortOption: Option[JsObject] = None,
projectionOption: Option[JsObject] = None,
hintOption: Option[JsObject] = None,
explainFlag: Boolean = false,
snapshotFlag: Boolean = false,
commentString: Option[String] = None,
options: QueryOpts = QueryOpts()) extends GenericQueryBuilder[JsObject, JsonReader, JsonWriter]
with JsonGenericHandlers {
override type Self = SprayJsonQueryBuilder
override def merge: JsObject = {
if (!sortOption.isDefined && !hintOption.isDefined && !explainFlag && !snapshotFlag && !commentString.isDefined)
queryOption.getOrElse(JsObject())
else
JsObject(
"$query" -> queryOption.getOrElse(JsObject()),
"$comment" -> commentString.map(JsString(_)).getOrElse(JsNull),
"$orderby" -> sortOption.getOrElse(JsNull),
"$hint" -> hintOption.getOrElse(JsNull),
"$explain" -> JsBoolean(explainFlag),
"$snapshot" -> JsBoolean(snapshotFlag))
}
override def structureReader: JsonReader[JsObject] = DefaultJsonProtocol.RootJsObjectFormat
override def copy(queryOption: Option[JsObject], sortOption: Option[JsObject], projectionOption: Option[JsObject],
hintOption: Option[JsObject], explainFlag: Boolean, snapshotFlag: Boolean,
commentString: Option[String], options: QueryOpts, failover: FailoverStrategy) = {
SprayJsonQueryBuilder(collection, failover, queryOption, sortOption, projectionOption,
hintOption, explainFlag, snapshotFlag, commentString, options)
}
}
implicit object SprayJsonCollectionProducer extends GenericCollectionProducer[JsObject, JsonReader, JsonWriter, SprayJsonCollection] {
def apply(db: DB, name: String, failoverStrategy: FailoverStrategy) = new SprayJsonCollection(db, name, failoverStrategy)
}
}
object SprayJsonCollections extends SprayJsonCollections
/** Readers
*
* @see http://docs.mongodb.org/manual/reference/mongodb-extended-json/
*/
object JsBSONReader {
def readObject(doc: BSONDocument) = JsObject(doc.elements.toSeq.map(readElement): _*)
def readElement(e: BSONElement): (String, JsValue) = e._1 -> readValue(e._2)
def readValue(bsonVal : BSONValue) : JsValue = bsonVal match {
case BSONString(value) => JsString(value)
case BSONInteger(value) => JsNumber(value)
case BSONLong(value) => JsNumber(value)
case BSONDouble(value) => JsNumber(value)
case BSONBoolean(true) => JsTrue
case BSONBoolean(false) => JsFalse
case BSONNull => JsNull
case doc: BSONDocument => readObject(doc)
case arr: BSONArray => readArray(arr)
case oid @ BSONObjectID(value) => JsObject("$oid" -> JsString(oid.stringify))
case BSONDateTime(value) => JsObject("$date" -> JsString(isoFormatter.print(value)))
case bb: BSONBinary => readBSONBinary(bb)
case BSONRegex(value, flags) => JsObject("$regex" -> JsString(value), "$options" -> JsString(flags))
case BSONTimestamp(value) => JsObject("$timestamp" -> JsObject(
"t" -> JsNumber(value.toInt), "i" -> JsNumber((value >>> 32).toInt)))
case BSONUndefined => JsObject("$undefined" -> JsTrue)
// case BSONMinKey => JsObject("$minKey" -> JsNumber(1)) // Bug on reactivemongo
case BSONMaxKey => JsObject("$maxKey" -> JsNumber(1))
// case BSONDBPointer(value, id) => JsObject("$ref" -> JsString(value), "$id" -> JsString(Hex.encodeHexString(id))) // Not implemented
// NOT STANDARD AT ALL WITH JSON and MONGO
case BSONJavaScript(value) => JsObject("$js" -> JsString(value))
case BSONSymbol(value) => JsObject("$sym" -> JsString(value))
case BSONJavaScriptWS(value) => JsObject("$jsws" -> JsString(value))
}
def readArray(array: BSONArray) = JsArray(array.values.toSeq.map(readValue): _*)
def readBSONBinary(bb: BSONBinary) = {
val arr = new Array[Byte](bb.value.readable)
bb.value.readBytes(arr)
val sub = ByteBuffer.allocate(4).putInt(bb.subtype.value).array
JsObject("$binary" -> JsString(Base64.encodeBase64String(arr)),
"$type" -> JsString(Hex.encodeHexString(sub)))
}
val isoFormatter = ISODateTimeFormat.dateTime.withZone(DateTimeZone.UTC)
}
/** Writers
*
* @see http://docs.mongodb.org/manual/reference/mongodb-extended-json/
*/
object JsBSONWriter {
def writeObject(obj: JsObject): BSONDocument = BSONDocument(obj.fields.map(writePair))
def writeArray(arr: JsArray): BSONArray =
BSONArray(arr.elements.zipWithIndex.map(p => writePair(p._2.toString, p._1)).map(_._2))
def writePair(p: (String, JsValue)): (String, BSONValue) = (p._1, writeValue(p._2))
def writeValue(jsVal : JsValue) : BSONValue = jsVal match {
case JsString(str @ IsoDateTime(y, m, d, h, mi, s, ms)) => manageDate(y, m, d, h, mi, s, ms) match {
case Success(dt) => dt
case Failure(_) => BSONString(str)
}
case JsString(str) => BSONString(str)
case JsNumber(value) => BSONDouble(value.doubleValue)
case obj: JsObject => manageSpecials(obj)
case arr: JsArray => writeArray(arr)
case JsTrue => BSONBoolean(true)
case JsFalse => BSONBoolean(false)
case JsNull => BSONNull
}
def manageDate(year: String, month: String, day: String, hour: String, minute: String, second: String, milli: String) =
Try(BSONDateTime((new DateTime(year.toInt, month.toInt, day.toInt, hour.toInt,
minute.toInt, second.toInt, milli.toInt, DateTimeZone.UTC)).getMillis))
def manageSpecials(obj: JsObject): BSONValue =
if (obj.fields.size > 2) writeObject(obj)
else (obj.fields.toList match {
case ("$oid", JsString(str)) :: Nil => Try(BSONObjectID(Hex.decodeHex(str.toArray)))
case ("$undefined", JsTrue) :: Nil => Success(BSONUndefined)
// case ("$minKey", JsNumber(n)) :: Nil if n == 1 => Success(BSONMinKey) // Bug on reactivemongo
case ("$maxKey", JsNumber(n)) :: Nil if n == 1 => Success(BSONMaxKey)
case ("$js", JsString(str)) :: Nil => Success(BSONJavaScript(str))
case ("$sym", JsString(str)) :: Nil => Success(BSONSymbol(str))
case ("$jsws", JsString(str)) :: Nil => Success(BSONJavaScriptWS(str))
case ("$timestamp", ts: JsObject) :: Nil => manageTimestamp(ts)
case ("$regex", JsString(r)) :: ("$options", JsString(o)) :: Nil =>
Success(BSONRegex(r, o))
case ("$binary", JsString(d)) :: ("$type", JsString(t)) :: Nil =>
Try(BSONBinary(ChannelBuffers.wrappedBuffer(Base64.decodeBase64(d)).array(),
findSubtype(Hex.decodeHex(t.toArray))))
// case ("$ref", JsString(v)) :: ("$id", JsString(i)) :: Nil => // Not implemented
// Try(BSONDBPointer(v, Hex.decodeHex(i.toArray)))
case _ => Success(writeObject(obj))
}) match {
case Success(v) => v
case Failure(_) => writeObject(obj)
}
def manageTimestamp(o: JsObject) = o.fields.toList match {
case ("t", JsNumber(t)) :: ("i", JsNumber(i)) :: Nil =>
Success(BSONTimestamp((t.toLong & 4294967295L) | (i.toLong << 32)))
case _ => Failure(new IllegalArgumentException("Illegal timestamp value"))
}
def findSubtype(bytes: Array[Byte]) =
ByteBuffer.wrap(bytes).getInt match {
case 0x00 => Subtype.GenericBinarySubtype
case 0x01 => Subtype.FunctionSubtype
case 0x02 => Subtype.OldBinarySubtype
case 0x03 => Subtype.UuidSubtype
case 0x05 => Subtype.Md5Subtype
// case 0X80 => Subtype.UserDefinedSubtype // Bug on reactivemongo
case _ => throw new IllegalArgumentException("unsupported binary subtype")
}
val IsoDateTime = """^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z$""".r
}
@rhodri
Copy link
Author

rhodri commented Aug 5, 2014

import SprayJsonCollections._ to bring the collection provider into scope.

Tested with spray-json 1.2.5 and ReactiveMongo 0.10 – requires:

"commons-codec" % "commons-codec" % "1.9",
"joda-time" % "joda-time" % "2.3"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment