2018-10-29 17:19:37 +08:00
|
|
|
/*
|
2018-01-04 17:26:29 +00:00
|
|
|
* Copyright (C) 2014-2018 Lightbend Inc. <https://www.lightbend.com>
|
2016-05-24 10:20:28 +02:00
|
|
|
*/
|
2018-03-13 23:45:55 +09:00
|
|
|
|
2016-05-24 10:20:28 +02:00
|
|
|
package akka.stream.impl
|
|
|
|
|
|
2017-03-16 21:04:07 +02:00
|
|
|
import akka.annotation.InternalApi
|
2016-05-24 10:20:28 +02:00
|
|
|
import akka.stream.scaladsl.Framing.FramingException
|
|
|
|
|
import akka.util.ByteString
|
|
|
|
|
|
|
|
|
|
import scala.annotation.switch
|
|
|
|
|
|
2016-07-25 01:50:55 +02:00
|
|
|
/**
|
|
|
|
|
* INTERNAL API: Use [[akka.stream.scaladsl.JsonFraming]] instead.
|
|
|
|
|
*/
|
2017-03-16 21:04:07 +02:00
|
|
|
@InternalApi private[akka] object JsonObjectParser {
|
2016-05-24 10:20:28 +02:00
|
|
|
|
2016-07-25 01:50:55 +02:00
|
|
|
final val SquareBraceStart = '['.toByte
|
|
|
|
|
final val SquareBraceEnd = ']'.toByte
|
|
|
|
|
final val CurlyBraceStart = '{'.toByte
|
|
|
|
|
final val CurlyBraceEnd = '}'.toByte
|
2016-08-10 12:05:26 +01:00
|
|
|
final val DoubleQuote = '"'.toByte
|
2016-07-25 01:50:55 +02:00
|
|
|
final val Backslash = '\\'.toByte
|
|
|
|
|
final val Comma = ','.toByte
|
2016-05-24 10:20:28 +02:00
|
|
|
|
2018-06-26 16:44:28 +02:00
|
|
|
final val LineBreak = 10 // '\n'
|
|
|
|
|
final val LineBreak2 = 13 // '\r'
|
|
|
|
|
final val Tab = 9 // '\t'
|
|
|
|
|
final val Space = 32 // ' '
|
|
|
|
|
|
|
|
|
|
def isWhitespace(b: Byte): Boolean = (b: @switch) match {
|
|
|
|
|
case Space ⇒ true
|
|
|
|
|
case LineBreak ⇒ true
|
|
|
|
|
case LineBreak2 ⇒ true
|
|
|
|
|
case Tab ⇒ true
|
|
|
|
|
case _ ⇒ false
|
|
|
|
|
}
|
2016-05-24 10:20:28 +02:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-07-25 01:50:55 +02:00
|
|
|
* INTERNAL API: Use [[akka.stream.scaladsl.JsonFraming]] instead.
|
|
|
|
|
*
|
2016-05-24 10:20:28 +02:00
|
|
|
* **Mutable** framing implementation that given any number of [[ByteString]] chunks, can emit JSON objects contained within them.
|
2016-08-10 12:59:52 +02:00
|
|
|
* Typically JSON objects are separated by new-lines or commas, however a top-level JSON Array can also be understood and chunked up
|
2016-05-24 10:20:28 +02:00
|
|
|
* into valid JSON objects by this framing implementation.
|
|
|
|
|
*
|
|
|
|
|
* Leading whitespace between elements will be trimmed.
|
|
|
|
|
*/
|
2017-03-16 21:04:07 +02:00
|
|
|
@InternalApi private[akka] class JsonObjectParser(maximumObjectLength: Int = Int.MaxValue) {
|
2016-07-29 15:19:13 +02:00
|
|
|
import JsonObjectParser._
|
2016-05-24 10:20:28 +02:00
|
|
|
|
|
|
|
|
private var buffer: ByteString = ByteString.empty
|
|
|
|
|
|
|
|
|
|
private var pos = 0 // latest position of pointer while scanning for json object end
|
|
|
|
|
private var trimFront = 0 // number of chars to drop from the front of the bytestring before emitting (skip whitespace etc)
|
|
|
|
|
private var depth = 0 // counter of object-nesting depth, once hits 0 an object should be emitted
|
|
|
|
|
|
|
|
|
|
private var charsInObject = 0
|
|
|
|
|
private var completedObject = false
|
|
|
|
|
private var inStringExpression = false
|
|
|
|
|
private var isStartOfEscapeSequence = false
|
2016-12-08 06:35:53 -07:00
|
|
|
private var lastInput = 0.toByte
|
2016-05-24 10:20:28 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Appends input ByteString to internal byte string buffer.
|
|
|
|
|
* Use [[poll]] to extract contained JSON objects.
|
|
|
|
|
*/
|
|
|
|
|
def offer(input: ByteString): Unit =
|
|
|
|
|
buffer ++= input
|
|
|
|
|
|
|
|
|
|
def isEmpty: Boolean = buffer.isEmpty
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempt to locate next complete JSON object in buffered ByteString and returns `Some(it)` if found.
|
|
|
|
|
* May throw a [[akka.stream.scaladsl.Framing.FramingException]] if the contained JSON is invalid or max object size is exceeded.
|
|
|
|
|
*/
|
|
|
|
|
def poll(): Option[ByteString] = {
|
|
|
|
|
val foundObject = seekObject()
|
|
|
|
|
if (!foundObject) None
|
|
|
|
|
else
|
|
|
|
|
(pos: @switch) match {
|
|
|
|
|
case -1 | 0 ⇒ None
|
|
|
|
|
case _ ⇒
|
|
|
|
|
val (emit, buf) = buffer.splitAt(pos)
|
|
|
|
|
buffer = buf.compact
|
|
|
|
|
pos = 0
|
|
|
|
|
|
|
|
|
|
val tf = trimFront
|
|
|
|
|
trimFront = 0
|
|
|
|
|
|
|
|
|
|
if (tf == 0) Some(emit)
|
|
|
|
|
else {
|
|
|
|
|
val trimmed = emit.drop(tf)
|
|
|
|
|
if (trimmed.isEmpty) None
|
|
|
|
|
else Some(trimmed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return true if an entire valid JSON object was found, false otherwise */
|
|
|
|
|
private def seekObject(): Boolean = {
|
|
|
|
|
completedObject = false
|
|
|
|
|
val bufSize = buffer.size
|
|
|
|
|
while (pos != -1 && (pos < bufSize && pos < maximumObjectLength) && !completedObject)
|
|
|
|
|
proceed(buffer(pos))
|
|
|
|
|
|
|
|
|
|
if (pos >= maximumObjectLength)
|
|
|
|
|
throw new FramingException(s"""JSON element exceeded maximumObjectLength ($maximumObjectLength bytes)!""")
|
|
|
|
|
|
|
|
|
|
completedObject
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-08 06:35:53 -07:00
|
|
|
private def proceed(input: Byte): Unit = {
|
2016-05-24 10:20:28 +02:00
|
|
|
if (input == SquareBraceStart && outsideObject) {
|
|
|
|
|
// outer object is an array
|
|
|
|
|
pos += 1
|
|
|
|
|
trimFront += 1
|
|
|
|
|
} else if (input == SquareBraceEnd && outsideObject) {
|
|
|
|
|
// outer array completed!
|
|
|
|
|
pos = -1
|
|
|
|
|
} else if (input == Comma && outsideObject) {
|
|
|
|
|
// do nothing
|
|
|
|
|
pos += 1
|
|
|
|
|
trimFront += 1
|
|
|
|
|
} else if (input == Backslash) {
|
2017-07-05 12:11:17 +03:00
|
|
|
if (lastInput == Backslash & isStartOfEscapeSequence) isStartOfEscapeSequence = false
|
2016-12-08 06:35:53 -07:00
|
|
|
else isStartOfEscapeSequence = true
|
2016-05-24 10:20:28 +02:00
|
|
|
pos += 1
|
|
|
|
|
} else if (input == DoubleQuote) {
|
|
|
|
|
if (!isStartOfEscapeSequence) inStringExpression = !inStringExpression
|
|
|
|
|
isStartOfEscapeSequence = false
|
|
|
|
|
pos += 1
|
|
|
|
|
} else if (input == CurlyBraceStart && !inStringExpression) {
|
|
|
|
|
isStartOfEscapeSequence = false
|
|
|
|
|
depth += 1
|
|
|
|
|
pos += 1
|
|
|
|
|
} else if (input == CurlyBraceEnd && !inStringExpression) {
|
|
|
|
|
isStartOfEscapeSequence = false
|
|
|
|
|
depth -= 1
|
|
|
|
|
pos += 1
|
|
|
|
|
if (depth == 0) {
|
|
|
|
|
charsInObject = 0
|
|
|
|
|
completedObject = true
|
|
|
|
|
}
|
|
|
|
|
} else if (isWhitespace(input) && !inStringExpression) {
|
|
|
|
|
pos += 1
|
|
|
|
|
if (depth == 0) trimFront += 1
|
|
|
|
|
} else if (insideObject) {
|
|
|
|
|
isStartOfEscapeSequence = false
|
|
|
|
|
pos += 1
|
|
|
|
|
} else {
|
2016-07-29 16:29:50 +02:00
|
|
|
throw new FramingException(s"Invalid JSON encountered at position [$pos] of [$buffer]")
|
2016-05-24 10:20:28 +02:00
|
|
|
}
|
|
|
|
|
|
2016-12-08 06:35:53 -07:00
|
|
|
lastInput = input
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-24 10:20:28 +02:00
|
|
|
@inline private final def insideObject: Boolean =
|
|
|
|
|
!outsideObject
|
|
|
|
|
|
|
|
|
|
@inline private final def outsideObject: Boolean =
|
|
|
|
|
depth == 0
|
|
|
|
|
|
|
|
|
|
}
|