Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package play.api.libs.json.jackson

import java.io.InputStream
import java.io.OutputStream
import java.io.StringWriter

import scala.annotation.switch
import scala.annotation.tailrec
Expand All @@ -18,9 +17,9 @@ import com.fasterxml.jackson.core.JsonFactoryBuilder
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonTokenId
import com.fasterxml.jackson.core.StreamWriteFeature
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.core.json.JsonWriteFeature
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter

import com.fasterxml.jackson.databind.Module.SetupContext
import com.fasterxml.jackson.databind._
Expand Down Expand Up @@ -314,12 +313,6 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) {
this.currentMapper = mapper
}

private def stringJsonGenerator(out: StringWriter) =
mapper().getFactory.createGenerator(out)

private def stringJsonGenerator(out: OutputStream) =
mapper().getFactory.createGenerator(out)

def parseJsValue(data: Array[Byte]): JsValue =
mapper().readValue(data, classOf[JsValue])

Expand All @@ -329,53 +322,27 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) {
def parseJsValue(stream: InputStream): JsValue =
mapper().readValue(stream, classOf[JsValue])

private def withStringWriter[T](f: StringWriter => T): T = {
val sw = new StringWriter()

try {
f(sw)
} catch {
case err: Throwable => throw err
} finally {
if (sw != null) try {
sw.close()
} catch {
case _: Throwable => ()
}
def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String = {
val writer = mapper().writer()
val configuredWriter = if (escapeNonASCII) {
writer.`with`(JsonWriteFeature.ESCAPE_NON_ASCII)
} else {
writer
}
}

def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String =
withStringWriter { sw =>
val gen = stringJsonGenerator(sw)

if (escapeNonASCII) {
gen.enable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature)
}

mapper().writeValue(gen, jsValue)
sw.flush()
sw.getBuffer.toString
}
configuredWriter.writeValueAsString(jsValue)
}

def prettyPrint(jsValue: JsValue): String = withStringWriter { sw =>
val gen = stringJsonGenerator(sw).setPrettyPrinter(
new DefaultPrettyPrinter()
)
def prettyPrint(jsValue: JsValue): String = {
val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter()

writer.writeValue(gen, jsValue)
sw.flush()
sw.getBuffer.toString
writer.writeValueAsString(jsValue)
}

def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = {
val gen = stringJsonGenerator(stream).setPrettyPrinter(
new DefaultPrettyPrinter()
)
val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter()

writer.writeValue(gen, jsValue)
val writer: ObjectWriter = mapper()
.writerWithDefaultPrettyPrinter()
.without(StreamWriteFeature.AUTO_CLOSE_TARGET)
writer.writeValue(stream, jsValue)
}

def jsValueToBytes(jsValue: JsValue): Array[Byte] =
Expand Down
53 changes: 42 additions & 11 deletions play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import java.util.TimeZone

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.{ ArrayNode, NumericNode, ObjectNode }
import play.api.libs.functional.syntax._
import play.api.libs.json.Json._
import play.api.libs.json.jackson.JacksonJson
import play.api.libs.json.jackson.{ JacksonJson, PlayJsonMapperModule }

class JsonSpec extends org.specs2.mutable.Specification {

Expand Down Expand Up @@ -481,36 +482,66 @@ class JsonSpec extends org.specs2.mutable.Specification {
}

"Serialize and deserialize Jackson ObjectNodes" in {
val on = mapper
val on: ObjectNode = mapper
.createObjectNode()
.put("foo", 1)
.put("bar", "two")
val json = Json.obj("foo" -> 1, "bar" -> "two")
val json = Json.obj("foo" -> 1, "bar" -> "two")
val deserialized: JsResult[JsonNode] = fromJson[JsonNode](json)

toJson(on).must_==(json) and (
fromJson[JsonNode](json).map(_.toString).must_==(JsSuccess(on.toString))
deserialized.map(_.isInstanceOf[ObjectNode]).must_==(JsSuccess(true))
) and (
deserialized.map(_.toString).must_==(JsSuccess(on.toString))
)
}

"Serialize and deserialize Jackson ArrayNodes" in {
val an = mapper
val an: ArrayNode = mapper
.createArrayNode()
.add("one")
.add(2)
val json = Json.arr("one", 2)
val json = Json.arr("one", 2)
val deserialized: JsResult[JsonNode] = fromJson[JsonNode](json)

toJson(an).must(equalTo(json)) and (
fromJson[JsonNode](json).map(_.toString).must_==(JsSuccess(an.toString))
deserialized.map(_.isInstanceOf[ArrayNode]).must_==(JsSuccess(true))
) and (
deserialized.map(_.toString).must_==(JsSuccess(an.toString))
)
}

"Deserialize integer JsNumber as Jackson number node" in {
val jsNum = JsNumber(new java.math.BigDecimal("50"))
fromJson[JsonNode](jsNum).map(_.toString).must_==(JsSuccess("50"))
val jsNum = JsNumber(new java.math.BigDecimal("50"))
val deserialized: JsResult[JsonNode] = fromJson[JsonNode](jsNum)

deserialized.map(_.isInstanceOf[NumericNode]).must_==(JsSuccess(true)) and (
deserialized.map(_.toString).must_==(JsSuccess("50"))
)
}

"Deserialize float JsNumber as Jackson number node" in {
val jsNum = JsNumber(new java.math.BigDecimal("12.345"))
fromJson[JsonNode](jsNum).map(_.toString).must_==(JsSuccess("12.345"))
val jsNum = JsNumber(new java.math.BigDecimal("12.345"))
val deserialized: JsResult[JsonNode] = fromJson[JsonNode](jsNum)

deserialized.map(_.isInstanceOf[NumericNode]).must_==(JsSuccess(true)) and (
deserialized.map(_.toString).must_==(JsSuccess("12.345"))
)
}

"Use a custom ObjectMapper subclass for ASCII serialization" in {
val jacksonJson = JacksonJson(JsonConfig.settings)
val customMapper = new ObjectMapper() {}

jacksonJson.setObjectMapper(customMapper)
customMapper.registerModule(new PlayJsonMapperModule())

jacksonJson
.generateFromJsValue(JsString("é"), escapeNonASCII = true)
.mustEqual("\"\\u00E9\"")

jacksonJson.setObjectMapper(null)
jacksonJson.mapper().eq(customMapper).mustEqual(false)
}

"Serialize JsNumbers with integers correctly" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ sealed trait JsonFacade {
* writes the result to an output stream.
*
* $jsonParam
* @param stream the stream to write to.
* @param stream the stream to write to; it is not closed by this method.
*/
def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ class JsonSharedSpec
with org.scalatest.TryValues
with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks {

private class CloseTrackingOutputStream extends ByteArrayOutputStream {
private var closed = false

def isClosed: Boolean = closed

override def close(): Unit = {
closed = true
super.close()
}
}

case class User(id: Long, name: String, friends: List[User])

implicit val UserFormat: Format[User] = (
Expand Down Expand Up @@ -344,15 +355,16 @@ class JsonSharedSpec
}""")
}

"JSON pretty print to stream" in json { js =>
"JSON pretty print to stream without closing it" in json { js =>
def jo = js.obj(
"key1" -> "toto",
"key2" -> js.obj("key21" -> "tata", "key22" -> 123),
"key3" -> js.arr(1, "tutu")
)

val stream = new ByteArrayOutputStream()
val stream = new CloseTrackingOutputStream()
js.prettyPrintToStream(jo, stream)
stream.isClosed.mustEqual(false)
stream
.toString("UTF-8")
.mustEqual("""{
Expand Down
Loading