Skip to content

Commit

Permalink
GEOMESA-3317 Fix GeoJSON export for JSON-type attributes (locationtec…
Browse files Browse the repository at this point in the history
  • Loading branch information
elahrvivaz authored Dec 7, 2023
1 parent d3e0a6d commit 045fca0
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 6 deletions.
2 changes: 1 addition & 1 deletion docs/user/datastores/json.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ feature type:
SimpleFeatureType sft = SimpleFeatureTypes.createType("mySft", spec);
dataStore.createSchema(sft);
JSON attributes are still strings, and are set as any other strings:
JSON attributes are still strings, and are set as any other strings. The value must be a valid JSON object or array:

.. code-block:: java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.locationtech.jts.geom.Geometry
import org.opengis.feature.simple.{SimpleFeature, SimpleFeatureType}

import java.io.Writer
import java.util.regex.Pattern
import java.util.{Base64, Date, UUID}

/**
Expand Down Expand Up @@ -90,12 +91,14 @@ object GeoJsonSerializer extends LazyLogging {
import org.locationtech.geomesa.utils.geotools.ObjectType
import org.locationtech.geomesa.utils.geotools.ObjectType.ObjectType

private val geometryWriter = {
private val GeometryWriter = {
val writer = new org.locationtech.jts.io.geojson.GeoJsonWriter()
writer.setEncodeCRS(false)
writer
}

private val JsonObjectPattern = Pattern.compile("^\\s*[{\\[]")

/**
* Create a JsonWriter from a standard writer.
*
Expand Down Expand Up @@ -153,8 +156,16 @@ object GeoJsonSerializer extends LazyLogging {
}

private class JsonStringWriter(name: String, i: Int) extends JsonAttributeWriter {
override def apply(feature: SimpleFeature, writer: JsonWriter): Unit =
writer.name(name).beginObject().jsonValue(feature.getAttribute(i).asInstanceOf[String]).endObject()
override def apply(feature: SimpleFeature, writer: JsonWriter): Unit = {
writer.name(name)
feature.getAttribute(i).asInstanceOf[String] match {
case null => writer.nullValue()
// note: this check isn't exhaustive, and may still produce invalid json if the value is badly formatted
case obj if JsonObjectPattern.matcher(obj).find() => writer.jsonValue(obj.trim)
// note: we don't support primitive types, but this is here to prevent bad input from producing invalid json
case primitive => writer.value(primitive)
}
}
}

private class NumberWriter(name: String, i: Int) extends JsonAttributeWriter {
Expand Down Expand Up @@ -182,7 +193,7 @@ object GeoJsonSerializer extends LazyLogging {
writer.name(name)
val geom = feature.getAttribute(i).asInstanceOf[Geometry]
if (geom == null) { writer.nullValue() } else {
writer.jsonValue(geometryWriter.write(geom))
writer.jsonValue(GeometryWriter.write(geom))
}
}
}
Expand Down Expand Up @@ -255,7 +266,7 @@ object GeoJsonSerializer extends LazyLogging {
case ObjectType.FLOAT => (writer, elem) => writer.value(elem.asInstanceOf[Number])
case ObjectType.DOUBLE => (writer, elem) => writer.value(elem.asInstanceOf[Number])
case ObjectType.DATE => (writer, elem) => writer.value(DateParsing.formatDate(elem.asInstanceOf[Date]))
case ObjectType.GEOMETRY => (writer, elem) => writer.jsonValue(geometryWriter.write(elem.asInstanceOf[Geometry]))
case ObjectType.GEOMETRY => (writer, elem) => writer.jsonValue(GeometryWriter.write(elem.asInstanceOf[Geometry]))
case ObjectType.BYTES => (writer, elem) => writer.value(Base64.getEncoder.encodeToString(elem.asInstanceOf[Array[Byte]]))
case ObjectType.BOOLEAN => (writer, elem) => writer.value(elem.asInstanceOf[java.lang.Boolean])
case _ => (writer, elem) => writer.value(elem.toString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,57 @@ class GeoJsonSerializerTest extends Specification {
|"dtg":null}
|}""".stripMargin.replaceAll("\n", "")
}

"serialize a json attribute" in {
val spec = "age:Integer,name:String,dtg:Date,*geom:Point:srid=4326,extra:String:json=true"
val sft = SimpleFeatureTypes.createType("testType", spec)

val sf = new ScalaSimpleFeature(sft, "1")
sf.setAttribute("age", "1")
sf.setAttribute("name", "foo")
sf.setAttribute("dtg", "2013-01-02T00:00:00.000Z")
sf.setAttribute("geom", "POINT(45.0 49.0)")

// (attribute value, rendered json) pairs
val extras = Seq(
("""{"a":1,"b":2}""", """{"a":1,"b":2}"""),
("""["a","b"]""", """["a","b"]"""),
(null, "null"),
("", "\"\""),
("abc", "\"abc\"")
)

def expectedFeature(extra: String): String =
s"""{
|"type":"Feature",
|"id":"1",
|"geometry":{"type":"Point","coordinates":[45,49]},
|"properties":{
|"age":1,
|"name":"foo",
|"dtg":"2013-01-02T00:00:00.000Z",
|"extra":$extra
|}
|}""".stripMargin.replaceAll("\n", "")

val expected = extras.map { case (_, rendered) => expectedFeature(rendered) }

val serializer = new GeoJsonSerializer(sft)

val out = new StringWriter()
WithClose(GeoJsonSerializer.writer(out)) { writer =>
serializer.startFeatureCollection(writer)
extras.foreach { case (attr, _) =>
sf.setAttribute("extra", attr)
serializer.write(writer, sf)
}
serializer.endFeatureCollection(writer)
}

val serialized = out.toString

serialized mustEqual s"""{"type":"FeatureCollection","features":[${expected.mkString(",")}]}"""
}

}
}

0 comments on commit 045fca0

Please sign in to comment.