Skip to content

Commit 257f760

Browse files
committed
Add support for writing to an output stream
Added JsonFacade functions writeToStream and prettyPrintToStream to support writing to an OutputStream without first writing the full string to memory. The JVM implementation forwards these calls to the appropriate functions in Jackson. For now, the non-JVM implementations will build up the full string in memory. Ideally the strings should be written out as they are built up, but this would require a refactoring of the fromJs() function to support an interface to feed each string value and minimize the number of conversions to UTF-8 (for toBytes and writing to stream) or avoid conversions (for String). For now, this fulfills the interface guarantee while providing the same level of functionality as before. Fixes playframework#1126
1 parent 6e6314e commit 257f760

File tree

7 files changed

+116
-3
lines changed

7 files changed

+116
-3
lines changed

play-json/js-native/src/main/scala/StaticBindingNonJvm.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ private[json] object StaticBindingNonJvm {
5454
arraySep = ("[ ", ", ", " ]")
5555
)
5656

57+
// TODO: Write to the stream when traversing JsValue without buffering the whole string.
58+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
59+
stream.write(prettyPrint(jsValue).getBytes("UTF-8"))
60+
5761
def toBytes(jsValue: JsValue): Array[Byte] =
5862
generateFromJsValue(jsValue, false).getBytes("UTF-8")
5963

64+
// TODO: Write to the stream when traversing JsValue without buffering the whole string.
65+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
66+
stream.write(toBytes(jsValue))
67+
6068
def fromJs(
6169
jsValue: JsValue,
6270
escapeNonASCII: Boolean,

play-json/js/src/main/scala/StaticBinding.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ object StaticBinding {
2626

2727
def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)
2828

29+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
30+
StaticBindingNonJvm.prettyPrintToStream(jsValue, stream)
31+
2932
def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)
3033

34+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
35+
StaticBindingNonJvm.writeToStream(jsValue, stream)
36+
3137
@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String =
3238
if (!escapeNonASCII) JSON.stringify(s, null) else StaticBindingNonJvm.escapeStr(JSON.stringify(s, null))
3339

play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package play.api.libs.json
66

77
import play.api.libs.json.jackson.JacksonJson
88

9+
import java.io.OutputStream
10+
911
object StaticBinding {
1012

1113
/** Parses a [[JsValue]] from raw data. */
@@ -25,6 +27,12 @@ object StaticBinding {
2527

2628
def prettyPrint(jsValue: JsValue): String = JacksonJson.get.prettyPrint(jsValue)
2729

30+
def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit =
31+
JacksonJson.get.prettyPrintToStream(jsValue, stream)
32+
2833
def toBytes(jsValue: JsValue): Array[Byte] =
2934
JacksonJson.get.jsValueToBytes(jsValue)
35+
36+
def writeToStream(jsValue: JsValue, stream: OutputStream): Unit =
37+
JacksonJson.get.writeJsValueToStream(jsValue, stream)
3038
}

play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package play.api.libs.json.jackson
66

77
import java.io.InputStream
8+
import java.io.OutputStream
89
import java.io.StringWriter
910

1011
import scala.annotation.switch
@@ -286,7 +287,10 @@ private[json] case class JacksonJson(jsonConfig: JsonConfig) {
286287

287288
private val jsonFactory = new JsonFactory(mapper)
288289

289-
private def stringJsonGenerator(out: java.io.StringWriter) =
290+
private def stringJsonGenerator(out: StringWriter) =
291+
jsonFactory.createGenerator(out)
292+
293+
private def stringJsonGenerator(out: OutputStream) =
290294
jsonFactory.createGenerator(out)
291295

292296
def parseJsValue(data: Array[Byte]): JsValue =
@@ -338,9 +342,21 @@ private[json] case class JacksonJson(jsonConfig: JsonConfig) {
338342
sw.getBuffer.toString
339343
}
340344

345+
def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = {
346+
val gen = stringJsonGenerator(stream).setPrettyPrinter(
347+
new DefaultPrettyPrinter()
348+
)
349+
val writer: ObjectWriter = mapper.writerWithDefaultPrettyPrinter()
350+
351+
writer.writeValue(gen, jsValue)
352+
}
353+
341354
def jsValueToBytes(jsValue: JsValue): Array[Byte] =
342355
mapper.writeValueAsBytes(jsValue)
343356

357+
def writeJsValueToStream(jsValue: JsValue, stream: OutputStream): Unit =
358+
mapper.writeValue(stream, jsValue)
359+
344360
def jsValueToJsonNode(jsValue: JsValue): JsonNode =
345361
mapper.valueToTree(jsValue)
346362

play-json/native/src/main/scala/StaticBinding.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@ object StaticBinding {
3838

3939
def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)
4040

41+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
42+
StaticBindingNonJvm.prettyPrintToStream(jsValue, stream)
43+
4144
def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)
4245

46+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
47+
StaticBindingNonJvm.writeToStream(jsValue, stream)
48+
4349
@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String = {
4450
def escaped(c: Char) = c match {
4551
case '\b' => "\\b"

play-json/shared/src/main/scala/play/api/libs/json/Json.scala

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
package play.api.libs.json
66

7-
import java.io.InputStream
7+
import java.io.{ InputStream, OutputStream }
88

99
import scala.collection.mutable.{ Builder => MBuilder }
1010

@@ -87,6 +87,14 @@ sealed trait JsonFacade {
8787
*/
8888
def toBytes(json: JsValue): Array[Byte]
8989

90+
/**
91+
* Writes a [[JsValue]] to an output stream.
92+
*
93+
* $jsonParam
94+
* @param stream the stream to write to.
95+
*/
96+
def writeToStream(json: JsValue, stream: OutputStream): Unit
97+
9098
/**
9199
* Converts a [[JsValue]] to its string representation,
92100
* escaping all non-ascii characters using `\u005CuXXXX` syntax.
@@ -140,6 +148,16 @@ sealed trait JsonFacade {
140148
*/
141149
def prettyPrint(json: JsValue): String
142150

151+
/**
152+
* Converts a [[JsValue]] to its pretty string representation using default
153+
* pretty printer (line feeds after each fields and 2-spaces indentation) and
154+
* writes the result to an output stream.
155+
*
156+
* $jsonParam
157+
* @param stream the stream to write to.
158+
*/
159+
def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit
160+
143161
/**
144162
* Converts any writeable value to a [[JsValue]].
145163
*
@@ -186,7 +204,7 @@ sealed trait JsonFacade {
186204
* Helper functions to handle JsValues.
187205
*
188206
* @define macroOptions @tparam Opts the compile-time options
189-
* @define macroTypeParam @tparam A the type for which the handler must be materialized
207+
* @define macroTypeParam @tparam The type for which the handler must be materialized
190208
* @define macroWarning If any missing implicit is discovered, compiler will break with corresponding error.
191209
*/
192210
object Json extends JsonFacade with JsMacros with JsValueMacros {
@@ -207,13 +225,17 @@ object Json extends JsonFacade with JsMacros with JsValueMacros {
207225

208226
def toBytes(json: JsValue): Array[Byte] = StaticBinding.toBytes(json)
209227

228+
def writeToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.writeToStream(json, stream)
229+
210230
// We use unicode \u005C for a backlash in comments, because Scala will replace unicode escapes during lexing
211231
// anywhere in the program.
212232
def asciiStringify(json: JsValue): String =
213233
StaticBinding.generateFromJsValue(json, true)
214234

215235
def prettyPrint(json: JsValue): String = StaticBinding.prettyPrint(json)
216236

237+
def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.prettyPrintToStream(json, stream)
238+
217239
def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o)
218240

219241
def toJsObject[T](o: T)(implicit

play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import org.scalacheck.Gen
1313
import org.scalatest.matchers.must.Matchers
1414
import org.scalatest.wordspec.AnyWordSpec
1515

16+
import java.io.ByteArrayOutputStream
17+
1618
class JsonSharedSpec
1719
extends AnyWordSpec
1820
with Matchers
@@ -178,6 +180,29 @@ class JsonSharedSpec
178180

179181
(success \ "price").mustEqual(JsDefined(JsString("2.5 €")))
180182
}
183+
184+
"write to output stream the UTF-8 representation" in json { js =>
185+
val json = js.parse("""
186+
|{
187+
| "name": "coffee",
188+
| "symbol": "☕",
189+
| "price": "2.5 €"
190+
|}
191+
""".stripMargin)
192+
193+
val stream = new ByteArrayOutputStream()
194+
js.writeToStream(json, stream)
195+
val string = stream.toString("UTF-8")
196+
val parsedJson = js.tryParse(string)
197+
198+
parsedJson.isSuccess.mustEqual(true)
199+
200+
val success = parsedJson.success.value
201+
202+
(success \ "symbol").mustEqual(JsDefined(JsString("")))
203+
204+
(success \ "price").mustEqual(JsDefined(JsString("2.5 €")))
205+
}
181206
}
182207

183208
"Complete JSON should create full object" when {
@@ -316,6 +341,28 @@ class JsonSharedSpec
316341
}""")
317342
}
318343

344+
"JSON pretty print to stream" in json { js =>
345+
def jo = js.obj(
346+
"key1" -> "toto",
347+
"key2" -> js.obj("key21" -> "tata", "key22" -> 123),
348+
"key3" -> js.arr(1, "tutu")
349+
)
350+
351+
val stream = new ByteArrayOutputStream()
352+
js.prettyPrintToStream(jo, stream)
353+
stream
354+
.toString("UTF-8")
355+
.replace("\r\n", "\n")
356+
.mustEqual("""{
357+
"key1" : "toto",
358+
"key2" : {
359+
"key21" : "tata",
360+
"key22" : 123
361+
},
362+
"key3" : [ 1, "tutu" ]
363+
}""")
364+
}
365+
319366
"asciiStringify should escape non-ascii characters" in json { js =>
320367
def jo = js.obj(
321368
"key1" -> "\u2028\u2029\u2030",

0 commit comments

Comments
 (0)