diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala index 2cad502da..1114a590c 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala @@ -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 @@ -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._ @@ -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]) @@ -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] = diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala index d554dbcdc..89e02f7bb 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala @@ -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 { @@ -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 { diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala index 25440a645..2b5bde40d 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala @@ -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 diff --git a/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala b/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala index a479045d8..62cb66be9 100644 --- a/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala +++ b/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala @@ -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] = ( @@ -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("""{