Skip to content

Instantly share code, notes, and snippets.

@nevang
Last active March 8, 2016 22:29
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save nevang/4690568 to your computer and use it in GitHub Desktop.
Save nevang/4690568 to your computer and use it in GitHub Desktop.
Reader and writer in order to work with spray-json and reactivemongo. Based on https://github.com/zenexity/Play-ReactiveMongo.
import spray.json._
import reactivemongo.bson._
import reactivemongo.bson.handlers.{ BSONReader, BSONWriter, RawBSONWriter }
import scala.util.{ Try, Success, Failure }
import org.apache.commons.codec.binary.Hex
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ DateTime, DateTimeZone }
import java.nio.ByteBuffer
import org.jboss.netty.buffer.ChannelBuffers
/** Readers and Writers for Reactivemongo BSON and Spray Json.
*
* Credits:
* It is based on the work Stephane Godbillon and Pascal Voitot on play reactivemongo plugin
* @see https://github.com/zenexity/Play-ReactiveMongo
*/
trait JsHandling {
//** Helper which allow to get json values from reactivemongo. */
implicit object JsObjectReader extends BSONReader[JsObject] {
def fromBSON(doc: BSONDocument): JsObject = JsBSONReader.readObject(doc.toTraversable)
}
/** Helper which allow to send json values to reactivemongo. */
object JsObjectWriter extends BSONWriter[JsObject] {
def toBSON(doc: JsObject) = JsBSONWriter.writeObject(doc)
}
/** Helper which allow to send json values to reactivemongo. */
implicit object JsValueWriter extends RawBSONWriter[JsValue] {
def write(doc: JsValue) = {
doc match {
case o: JsObject => JsBSONWriter.writeObject(o).makeBuffer
case a: JsArray => JsBSONWriter.writeArray(a).makeBuffer
case _ => throw new RuntimeException("JsValue can only be JsObject/JsArray")
}
}
}
}
/** Readers
*
* @see http://docs.mongodb.org/manual/reference/mongodb-extended-json/
*/
object JsBSONReader {
def readObject(doc: TraversableBSONDocument) = JsObject(doc.iterator.toSeq.map(readElement): _*)
def readElement(e: BSONElement): (String, JsValue) = e.name -> (e.value 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: TraversableBSONDocument => readObject(doc)
case doc: AppendableBSONDocument => readObject(doc.toTraversable)
case arr: TraversableBSONArray => readArray(arr)
case arr: AppendableBSONArray => readArray(arr.toTraversable)
case oid @ BSONObjectID(value) => JsObject("$oid" -> JsString(oid.stringify))
case BSONDateTime(value) => JsString(isoFormatter.print(value)) // Doesn't follow mongdb-extended
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: TraversableBSONArray) = JsArray(array.iterator.toSeq.map(readElement(_)._2): _*)
def readBSONBinary(bb: BSONBinary) = {
val arr = new Array[Byte](bb.value.readableBytes())
bb.value.readBytes(arr)
val sub = ByteBuffer.allocate(4).putInt(bb.subtype.value).array
JsObject("$binary" -> JsString(Hex.encodeHexString(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).toSeq: _*)
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, p._2 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(Hex.decodeHex(d.toArray)),
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
}
@rleibman
Copy link

I made some changes to make it compile with the latest reactive mongo library: https://gist.github.com/rleibman/7103325d0193be268ed7

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