Created
August 5, 2014 14:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
import SprayJsonCollections._
to bring the collection provider into scope.Tested with spray-json 1.2.5 and ReactiveMongo 0.10 – requires: