Skip to content

Commit b0cbc62

Browse files
authored
GEOMESA-3543 Switch to jackson for json parsing (#3458)
1 parent 6577d36 commit b0cbc62

File tree

6 files changed

+152
-94
lines changed

6 files changed

+152
-94
lines changed

.github/workflows/dash.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ jobs:
5252
echo "Detected pom change, double-checking dependencies"
5353
mvn $MAVEN_COMPILE_NO_OP_ARGS $MAVEN_CLI_OPTS
5454
./build/scripts/calculate-cqs.sh
55-
git diff --exit-code --quiet || {
55+
git diff --exit-code --quiet build/dependencies.txt || {
5656
echo 'Detected dependency changes - please run `./build/scripts/calculate-cqs.sh` and commit the results'
57+
git diff build/dependencies.txt
5758
exit 1
5859
}
5960
echo "Dependencies are ok, skipping dash check"

geomesa-features/geomesa-feature-kryo/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
<artifactId>kryo-shaded</artifactId>
2929
</dependency>
3030
<dependency>
31-
<groupId>org.json4s</groupId>
32-
<artifactId>json4s-native_${scala.binary.version}</artifactId>
31+
<groupId>com.fasterxml.jackson.core</groupId>
32+
<artifactId>jackson-databind</artifactId>
3333
</dependency>
3434
<dependency>
3535
<groupId>com.jayway.jsonpath</groupId>

geomesa-features/geomesa-feature-kryo/src/main/scala/org/locationtech/geomesa/features/kryo/json/KryoJsonPath.scala

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
package org.locationtech.geomesa.features.kryo.json
1010

1111
import com.esotericsoftware.kryo.io.Input
12+
import com.fasterxml.jackson.databind.node.ArrayNode
1213
import com.typesafe.scalalogging.LazyLogging
13-
import org.json4s.JsonAST._
14-
import org.json4s.native.JsonMethods.{parse => _, _}
1514
import org.locationtech.geomesa.features.kryo.json.KryoJsonPath.ValuePointer
1615

16+
import java.util.Collections
17+
1718
/**
1819
* Deserializes the results of json-paths. Not thread-safe. The input may end up positioned at arbitrary locations
1920
*
@@ -218,12 +219,10 @@ class KryoJsonPath(in: Input, root: ValuePointer) extends LazyLogging {
218219
}
219220
}
220221

221-
object KryoJsonPath {
222+
object KryoJsonPath extends LazyLogging {
222223

223224
import KryoJsonSerialization._
224225

225-
import scala.collection.JavaConverters._
226-
227226
/**
228227
* Pointer to a serialized value
229228
*
@@ -258,12 +257,10 @@ object KryoJsonPath {
258257
* @return value
259258
*/
260259
private def readPathValue(in: Input, typed: Byte, position: Int): Any = {
261-
import org.json4s.native.JsonMethods._
262-
263260
in.setPosition(position)
264261
typed match {
265262
case StringByte => readString(in)
266-
case DocByte => compact(render(readDocument(in)))
263+
case DocByte => mapper.writeValueAsString(readDocument(in))
267264
case ArrayByte => unwrapArray(readArray(in))
268265
case DoubleByte => in.readDouble()
269266
case IntByte => in.readInt()
@@ -280,17 +277,48 @@ object KryoJsonPath {
280277
* @param array array to unwrap
281278
* @return
282279
*/
283-
private def unwrapArray(array: JArray): java.util.List[Any] = {
284-
array.arr.map {
285-
case JString(s) => s
286-
case j: JObject => compact(render(j))
287-
case j: JArray => unwrapArray(j)
288-
case JDouble(d) => d
289-
case JInt(i) if i.isValidInt => i.intValue // note: this check needs to be a separate line to avoid auto-casting to long
290-
case JInt(i) => i.longValue
291-
case JLong(i) => i
292-
case JNull => null
293-
case JBool(b) => b
294-
}.asJava
280+
private def unwrapArray(array: ArrayNode): java.util.List[Any] = {
281+
val list = new java.util.ArrayList[Any](array.size())
282+
var i = 0
283+
while (i < array.size()) {
284+
val value = array.get(i)
285+
val unwrapped =
286+
if (value.isTextual || value.isBinary) {
287+
value.asText()
288+
} else if (value.isObject) {
289+
mapper.writeValueAsString(value)
290+
} else if (value.isArray) {
291+
unwrapArray(value.asInstanceOf[ArrayNode])
292+
} else if (value.isDouble || value.isFloat) {
293+
value.asDouble()
294+
} else if (value.isInt || value.isShort) {
295+
value.intValue()
296+
} else if (value.isLong) {
297+
value.longValue()
298+
} else if (value.isNull) {
299+
null
300+
} else if (value.isBoolean) {
301+
value.booleanValue()
302+
} else if (value.isBigDecimal) {
303+
value.asDouble()
304+
} else if (value.isBigInteger) {
305+
if (value.canConvertToInt) {
306+
value.intValue()
307+
} else if (value.canConvertToLong) {
308+
value.longValue()
309+
} else {
310+
logger.warn(s"Skipping int value that does not fit in a long: $value")
311+
null
312+
}
313+
} else {
314+
logger.warn(s"Unhandled JsonNode: $value")
315+
null
316+
}
317+
if (unwrapped != null) {
318+
list.add(unwrapped)
319+
}
320+
i += 1
321+
}
322+
Collections.unmodifiableList(list)
295323
}
296324
}

geomesa-features/geomesa-feature-kryo/src/main/scala/org/locationtech/geomesa/features/kryo/json/KryoJsonSerialization.scala

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
package org.locationtech.geomesa.features.kryo.json
1010

1111
import com.esotericsoftware.kryo.io.{Input, Output}
12+
import com.fasterxml.jackson.databind.JsonNode
13+
import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode}
1214
import com.typesafe.scalalogging.LazyLogging
13-
import org.json4s.JsonAST._
14-
import org.json4s.native.JsonMethods.{parse => _}
1515
import org.locationtech.geomesa.features.kryo.json.JsonPathParser._
1616

1717
import java.nio.charset.StandardCharsets
@@ -85,11 +85,9 @@ object KryoJsonSerialization extends LazyLogging {
8585
* @param json json string to serialize - must be a json object
8686
*/
8787
def serialize(out: Output, json: String): Unit = {
88-
import org.json4s._
89-
import org.json4s.native.JsonMethods._
9088
val obj = if (json == null) { null } else {
9189
try {
92-
parse(json)
90+
mapper.readTree(json)
9391
} catch {
9492
case NonFatal(e) =>
9593
logger.warn(s"Error parsing json:\n$json", e)
@@ -105,11 +103,13 @@ object KryoJsonSerialization extends LazyLogging {
105103
* @param out output to write to
106104
* @param json object to serialize
107105
*/
108-
def serialize(out: Output, json: JValue): Unit = {
109-
json match {
110-
case null | JNull => out.write(BooleanFalse)
111-
case j: JObject => out.write(BooleanTrue); writeDocument(out, j)
112-
case j => out.write(NonDoc); writeValue(out, "", j)
106+
def serialize(out: Output, json: JsonNode): Unit = {
107+
if (json == null || json.isNull) {
108+
out.write(BooleanFalse)
109+
} else if (json.isObject) {
110+
out.write(BooleanTrue); writeDocument(out, json)
111+
} else {
112+
out.write(NonDoc); writeValue(out, "", json)
113113
}
114114
}
115115

@@ -122,12 +122,11 @@ object KryoJsonSerialization extends LazyLogging {
122122
* @return json as a string
123123
*/
124124
def deserializeAndRender(in: Input): String = {
125-
import org.json4s.native.JsonMethods._
126125
val json = deserialize(in)
127126
if (json == null) {
128127
null
129128
} else {
130-
compact(render(json))
129+
mapper.writeValueAsString(json)
131130
}
132131
}
133132

@@ -139,7 +138,7 @@ object KryoJsonSerialization extends LazyLogging {
139138
* @param in input, pointing to the start of the json object
140139
* @return parsed json object
141140
*/
142-
def deserialize(in: Input): JValue = {
141+
def deserialize(in: Input): JsonNode = {
143142
try {
144143
in.readByte match {
145144
case BooleanFalse => null
@@ -176,19 +175,19 @@ object KryoJsonSerialization extends LazyLogging {
176175

177176
// primitive writing functions - in general will write a byte identifying the type, the key and then the value
178177

179-
private def writeDocument(out: Output, name: String, value: JObject): Unit = {
178+
private def writeDocument(out: Output, name: String, value: JsonNode): Unit = {
180179
out.writeByte(DocByte)
181180
out.writeName(name)
182181
writeDocument(out, value)
183182
}
184183

185-
// write a document without a name - used for the outer-most object which doesn't have a key
186-
private def writeDocument(out: Output, value: JObject): Unit = {
184+
// write a document without a name - used for the outermost object which doesn't have a key
185+
private def writeDocument(out: Output, value: JsonNode): Unit = {
187186
val start = out.position()
188187
// write a placeholder that we will overwrite when we go back to write total length
189188
// note: don't just modify position, as that doesn't expand the buffer correctly
190189
out.writeInt(0)
191-
value.obj.foreach { case (name, elem) => writeValue(out, name, elem) }
190+
value.forEachEntry { case (name, elem) => writeValue(out, name, elem) }
192191
out.writeByte(TerminalByte) // marks the end of our object
193192
// go back and write the total length
194193
val end = out.position()
@@ -197,74 +196,82 @@ object KryoJsonSerialization extends LazyLogging {
197196
out.setPosition(end)
198197
}
199198

200-
private def writeValue(out: Output, name: String, value: JValue): Unit = {
201-
value match {
202-
case v: JString => writeString(out, name, v)
203-
case v: JObject => writeDocument(out, name, v)
204-
case v: JArray => writeArray(out, name, v)
205-
case v: JDouble => writeDouble(out, name, v)
206-
case v: JInt => writeInt(out, name, v)
207-
case v: JLong => writeLong(out, name, v)
208-
case JNull => writeNull(out, name)
209-
case v: JBool => writeBoolean(out, name, v)
210-
case v: JDecimal => writeDecimal(out, name, v)
199+
private def writeValue(out: Output, name: String, value: JsonNode): Unit = {
200+
if (value.isTextual || value.isBinary) {
201+
writeString(out, name, value.asText())
202+
} else if (value.isObject) {
203+
writeDocument(out, name, value)
204+
} else if (value.isArray) {
205+
writeArray(out, name, value)
206+
} else if (value.isDouble || value.isFloat) {
207+
writeDouble(out, name, value.asDouble())
208+
} else if (value.isInt || value.isShort) {
209+
writeInt(out, name, value.intValue())
210+
} else if (value.isLong) {
211+
writeLong(out, name, value.longValue())
212+
} else if (value.isNull) {
213+
writeNull(out, name)
214+
} else if (value.isBoolean) {
215+
writeBoolean(out, name, value.booleanValue())
216+
} else if (value.isBigDecimal) {
217+
writeDouble(out, name, value.asDouble())
218+
} else if (value.isBigInteger) {
219+
if (value.canConvertToInt) {
220+
writeInt(out, name, value.intValue())
221+
} else if (value.canConvertToLong) {
222+
writeLong(out, name, value.longValue())
223+
} else {
224+
logger.warn(s"Skipping int value that does not fit in a long: $value")
225+
}
226+
} else {
227+
logger.warn(s"Unhandled JsonNode: $value")
211228
}
212229
}
213230

214-
private def writeArray(out: Output, name: String, value: JArray): Unit = {
231+
private def writeArray(out: Output, name: String, value: JsonNode): Unit = {
215232
out.writeByte(ArrayByte)
216233
out.writeName(name)
217234
// we store as an object where array index is the key
218-
var i = -1
219-
val withKeys = value.arr.map { element => i += 1; (i.toString, element) } // note: side-effect in map
220-
writeDocument(out, JObject(withKeys))
235+
var i = 0
236+
val obj = mapper.createObjectNode()
237+
while (i < value.size()) {
238+
obj.set(i.toString, value.get(i))
239+
i += 1
240+
}
241+
writeDocument(out, obj)
221242
}
222243

223-
private def writeString(out: Output, name: String, value: JString): Unit = {
244+
private def writeString(out: Output, name: String, value: String): Unit = {
224245
out.writeByte(StringByte)
225246
out.writeName(name)
226-
val bytes = value.values.getBytes(StandardCharsets.UTF_8)
247+
val bytes = value.getBytes(StandardCharsets.UTF_8)
227248
out.writeInt(bytes.length)
228249
out.write(bytes)
229250
out.writeByte(TerminalByte)
230251
}
231252

232-
private def writeDecimal(out: Output, name: String, value: JDecimal): Unit = {
253+
private def writeDouble(out: Output, name: String, value: Double): Unit = {
233254
out.writeByte(DoubleByte)
234255
out.writeName(name)
235-
out.writeDouble(value.values.toDouble)
256+
out.writeDouble(value)
236257
}
237258

238-
private def writeDouble(out: Output, name: String, value: JDouble): Unit = {
239-
out.writeByte(DoubleByte)
259+
private def writeInt(out: Output, name: String, value: Int): Unit = {
260+
out.writeByte(IntByte)
240261
out.writeName(name)
241-
out.writeDouble(value.values)
262+
out.writeInt(value)
242263
}
243264

244-
private def writeInt(out: Output, name: String, value: JInt): Unit = {
245-
if (value.values.isValidInt) {
246-
out.writeByte(IntByte)
247-
out.writeName(name)
248-
out.writeInt(value.values.intValue)
249-
} else if (value.values.isValidLong) {
250-
out.writeByte(LongByte)
251-
out.writeName(name)
252-
out.writeLong(value.values.longValue)
253-
} else {
254-
logger.warn(s"Skipping int value that does not fit in a long: $value")
255-
}
256-
}
257-
258-
private def writeLong(out: Output, name: String, value: JLong): Unit = {
265+
private def writeLong(out: Output, name: String, value: Long): Unit = {
259266
out.writeByte(LongByte)
260267
out.writeName(name)
261-
out.writeLong(value.values)
268+
out.writeLong(value)
262269
}
263270

264-
private def writeBoolean(out: Output, name: String, v: JBool): Unit = {
271+
private def writeBoolean(out: Output, name: String, value: Boolean): Unit = {
265272
out.writeByte(BooleanByte)
266273
out.writeName(name)
267-
out.writeByte(if (v.values) BooleanTrue else BooleanFalse)
274+
out.writeByte(if (value) BooleanTrue else BooleanFalse)
268275
}
269276

270277
private def writeNull(out: Output, name: String): Unit = {
@@ -275,33 +282,39 @@ object KryoJsonSerialization extends LazyLogging {
275282
// primitive reading/skipping methods corresponding to the write methods above
276283
// assumes that the indicator byte and name have already been read
277284

278-
private[json] def readDocument(in: Input): JObject = {
285+
private[json] def readDocument(in: Input): ObjectNode = {
279286
val end = in.position() + in.readInt() - 1 // last byte is the terminal byte
280-
val elements = scala.collection.mutable.ArrayBuffer.empty[JField]
287+
val obj = mapper.createObjectNode()
281288
while (in.position() < end) {
282-
elements.append(readValue(in))
289+
val (k, v) = readValue(in)
290+
obj.set(k, v)
283291
}
284292
in.skip(1) // skip over terminal byte
285-
JObject(elements.toList)
293+
obj
286294
}
287295

288-
private[json] def readValue(in: Input): JField = {
296+
private[json] def readValue(in: Input): (String, JsonNode) = {
289297
val switch = in.readByte()
290298
val name = in.readName()
291299
val value = switch match {
292-
case StringByte => JString(readString(in))
300+
case StringByte => mapper.getNodeFactory.textNode(readString(in))
293301
case DocByte => readDocument(in)
294302
case ArrayByte => readArray(in)
295-
case DoubleByte => JDouble(in.readDouble())
296-
case IntByte => JInt(in.readInt())
297-
case LongByte => JLong(in.readLong())
298-
case NullByte => JNull
299-
case BooleanByte => JBool(readBoolean(in))
303+
case DoubleByte => mapper.getNodeFactory.numberNode(in.readDouble())
304+
case IntByte => mapper.getNodeFactory.numberNode(in.readInt())
305+
case LongByte => mapper.getNodeFactory.numberNode(in.readLong())
306+
case NullByte => mapper.getNodeFactory.nullNode()
307+
case BooleanByte => mapper.getNodeFactory.booleanNode(readBoolean(in))
300308
}
301-
JField(name, value)
309+
(name, value)
302310
}
303311

304-
private[json] def readArray(in: Input): JArray = JArray(readDocument(in).obj.map(_._2))
312+
private[json] def readArray(in: Input): ArrayNode = {
313+
val obj = readDocument(in)
314+
val array = mapper.getNodeFactory.arrayNode(obj.size())
315+
obj.forEachEntry { case (_, v) => array.add(v) }
316+
array
317+
}
305318

306319
private[json] def skipDocument(in: Input): Unit = in.skip(in.readInt - 4) // length includes bytes storing length
307320

0 commit comments

Comments
 (0)