Skip to content

Commit 206fe2c

Browse files
authored
GEOMESA-3516 Check user data parsing before updateSchema (#3410)
1 parent 4beffd7 commit 206fe2c

File tree

5 files changed

+67
-42
lines changed

5 files changed

+67
-42
lines changed

geomesa-index-api/src/main/scala/org/locationtech/geomesa/index/geotools/MetadataBackedDataStore.scala

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,18 @@ abstract class MetadataBackedDataStore(config: NamespaceConfig) extends DataStor
138138
// set the enabled indices
139139
preSchemaCreate(sft)
140140

141+
// compute the metadata values - IMPORTANT: encode type has to be called after all user data is set
142+
val encodedAttributes = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
143+
// validate we can read out the encoded sft - invalid user data keys can break the parsing
144+
try { SimpleFeatureTypes.createType("", encodedAttributes) } catch {
145+
case NonFatal(e) => throw new IllegalArgumentException("Invalid schema:", e)
146+
}
147+
val metadataMap = Map(
148+
AttributesKey -> encodedAttributes,
149+
StatsGenerationKey -> GeoToolsDateFormat.format(Instant.now().atOffset(ZoneOffset.UTC))
150+
)
141151
try {
142152
// write out the metadata to the catalog table
143-
// compute the metadata values - IMPORTANT: encode type has to be called after all user data is set
144-
val metadataMap = Map(
145-
AttributesKey -> SimpleFeatureTypes.encodeType(sft, includeUserData = true),
146-
StatsGenerationKey -> GeoToolsDateFormat.format(Instant.now().atOffset(ZoneOffset.UTC))
147-
)
148153
metadata.insert(sft.getTypeName, metadataMap)
149154

150155
// reload the sft so that we have any default metadata,
@@ -252,6 +257,12 @@ abstract class MetadataBackedDataStore(config: NamespaceConfig) extends DataStor
252257
// validation and normalization of the schema
253258
preSchemaUpdate(sft, previousSft)
254259

260+
val encodedAttributes = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
261+
// validate we can read out the encoded sft - invalid user data keys can break the parsing
262+
try { SimpleFeatureTypes.createType("", encodedAttributes) } catch {
263+
case NonFatal(e) => throw new IllegalArgumentException("Invalid schema:", e)
264+
}
265+
255266
// if all is well, update the metadata - first back it up
256267
if (FastConverter.convertOrElse[java.lang.Boolean](sft.getUserData.get(Configs.UpdateBackupMetadata), true)) {
257268
metadata.backup(typeName.getLocalPart)
@@ -265,7 +276,7 @@ abstract class MetadataBackedDataStore(config: NamespaceConfig) extends DataStor
265276
}
266277
}
267278
// now insert the new spec string
268-
metadata.insert(sft.getTypeName, AttributesKey, SimpleFeatureTypes.encodeType(sft, includeUserData = true))
279+
metadata.insert(sft.getTypeName, AttributesKey, encodedAttributes)
269280

270281
onSchemaUpdated(sft, previousSft)
271282
} finally {

geomesa-index-api/src/test/scala/org/locationtech/geomesa/index/geotools/GeoMesaDataStoreAlterSchemaTest.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,27 @@ class GeoMesaDataStoreAlterSchemaTest extends Specification {
222222

223223
ds.checkSchemaCompatibility("test", toSft(missingConfig)) mustEqual SchemaCompatibility.Unchanged
224224
}
225+
"prevent creating a schema with corrupt metadata" in {
226+
val ds = new TestGeoMesaDataStore(true)
227+
val spec = "name:String,age:Integer,dtg:Date,*geom:Point:srid=4326"
228+
val sft = SimpleFeatureTypes.createType("test", spec)
229+
sft.getUserData.put("geomesa.foo=bar", "baz")
230+
ds.createSchema(sft) must throwAn[IllegalArgumentException]
231+
ds.getSchema("test") must beNull
232+
}
233+
"prevent updates that would corrupt the schema metadata" in {
234+
val ds = new TestGeoMesaDataStore(true)
235+
val spec = "name:String,age:Integer,dtg:Date,*geom:Point:srid=4326"
236+
ds.createSchema(SimpleFeatureTypes.createType("test", spec))
237+
238+
val sft = SimpleFeatureTypes.mutable(ds.getSchema("test"))
239+
sft.getUserData.put("geomesa.foo=bar", "baz")
240+
241+
ds.updateSchema("test", sft) must throwAn[IllegalArgumentException]
242+
243+
ds.getSchema("test") must not(beNull)
244+
SimpleFeatureTypes.encodeType(ds.getSchema("test")) mustEqual spec
245+
}
225246
}
226247
}
227248

geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/SimpleFeatureTypes.scala

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,12 @@ object SimpleFeatureTypes {
285285

286286
def encodeUserData(sft: SimpleFeatureType): String = {
287287
val prefixes = sft.getUserDataPrefixes
288-
val result = new StringBuilder(";")
289-
sft.getUserData.asScala.foreach { case (k: AnyRef, v: AnyRef) =>
290-
if (v != null && prefixes.exists(k.toString.startsWith)) {
291-
result.append(encodeUserData(k, v)).append(",")
292-
}
288+
val toEncode = sft.getUserData.asScala.collect {
289+
case (k, v) if v != null && prefixes.exists(k.toString.startsWith) => encodeUserData(k, v)
290+
}
291+
if (toEncode.isEmpty) { "" } else {
292+
toEncode.toSeq.sorted.mkString(";", ",", "")
293293
}
294-
if (result.lengthCompare(1) > 0) { result.substring(0, result.length - 1) } else { "" }
295294
}
296295

297296
def encodeUserData(data: java.util.Map[_ <: AnyRef, _ <: AnyRef]): String = {

geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/sft/SimpleFeatureSpecParser.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,27 @@ object SimpleFeatureSpecParser {
3636

3737
private def parse[T](spec: String, rule: Rule1[T], report: Boolean): T = {
3838
if (spec == null) {
39-
throw new IllegalArgumentException("Invalid spec string: null")
39+
throw new IllegalArgumentException("Invalid spec: null")
4040
}
4141
val runner = if (report) { ReportingParseRunner(rule) } else { BasicParseRunner(rule) }
4242
val parsing = runner.run(spec.stripMarginAndWhitespace())
43-
parsing.result.getOrElse(throw new ParsingException(constructErrorMessage(parsing, report)))
43+
parsing.result.getOrElse(throw new ParsingException(constructErrorMessage(parsing, report, spec)))
4444
}
4545

46-
private def constructErrorMessage[T](result: ParsingResult[T], report: Boolean): String = {
47-
lazy val fallback = s"Invalid spec string: ${ErrorUtils.printParseErrors(result)}"
46+
private def constructErrorMessage[T](result: ParsingResult[T], report: Boolean, spec: String): String = {
47+
lazy val fallback = s"Invalid spec: ${ErrorUtils.printParseErrors(result)}\n$spec"
4848
if (!report) { fallback } else {
4949
result.parseErrors.collectFirst { case e: InvalidInputError =>
5050
import scala.collection.JavaConverters._
51+
// add a caret pointing to the invalid location
52+
val specAndIndex = s"$spec\n${Seq.fill(math.max(0, e.getStartIndex - 1))(" ").mkString}^"
5153
// determine what paths the parser partially matched
5254
val matchers = e.getFailedMatchers.asScala.map(getFailedMatcher).distinct
5355
if (matchers.isEmpty) {
54-
s"Invalid spec string at index ${e.getStartIndex}."
56+
s"Invalid spec at index ${e.getStartIndex}\n$specAndIndex"
5557
} else {
5658
val expected = if (matchers.lengthCompare(1) > 0) { s"one of: ${matchers.mkString(", ")}" } else { matchers.head }
57-
s"Invalid spec string at index ${e.getStartIndex}. Expected $expected."
59+
s"Invalid spec at index ${e.getStartIndex} - expected $expected\n$specAndIndex"
5860
}
5961
}.getOrElse(fallback)
6062
}

geomesa-utils-parent/geomesa-utils/src/test/scala/org/locationtech/geomesa/utils/geotools/SimpleFeatureTypesTest.scala

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,20 @@ class SimpleFeatureTypesTest extends Specification {
6060
}
6161
"encode an sft properly with geomesa user data" >> {
6262
val encoded = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
63-
encoded must startWith("id:Integer,dtg:Date,*geom:Point:srid=4326;")
64-
encoded must contain("geomesa.index.dtg='dtg'")
65-
encoded must contain("geomesa.table.sharing='true'")
66-
encoded must not(contain("hello="))
63+
encoded mustEqual "id:Integer,dtg:Date,*geom:Point:srid=4326;geomesa.index.dtg='dtg',geomesa.table.sharing='true'"
6764
}
6865
"encode an sft properly with specified user data" >> {
6966
import org.locationtech.geomesa.utils.geotools.RichSimpleFeatureType.RichSimpleFeatureType
7067
sft.setUserDataPrefixes(Seq("hello"))
7168
val encoded = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
72-
encoded must startWith("id:Integer,dtg:Date,*geom:Point:srid=4326;")
73-
encoded must contain("geomesa.user-data.prefix='hello'")
74-
encoded must contain("geomesa.index.dtg='dtg'")
75-
encoded must contain("geomesa.table.sharing='true'")
76-
encoded must contain("hello='goodbye'")
69+
encoded mustEqual "id:Integer,dtg:Date,*geom:Point:srid=4326;" +
70+
"geomesa.index.dtg='dtg',geomesa.table.sharing='true',geomesa.user-data.prefix='hello',hello='goodbye'"
71+
}
72+
"encode an sft and preserve spaces in user data" >> {
73+
val sft = SimpleFeatureTypes.createType("test", "name:String,dtg:Date,*geom:Point:srid=4326;geomesa.foo='1 day'")
74+
sft.getUserData.get("geomesa.foo") mustEqual "1 day"
75+
val encoded = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
76+
SimpleFeatureTypes.createType("test", encoded).getUserData.get("geomesa.foo") mustEqual "1 day"
7777
}
7878
}
7979

@@ -362,16 +362,16 @@ class SimpleFeatureTypesTest extends Specification {
362362

363363
"return meaningful error messages" >> {
364364
Try(SimpleFeatureTypes.createType("test", null)) must
365-
beAFailedTry.withThrowable[IllegalArgumentException](Pattern.quote("Invalid spec string: null"))
365+
beAFailedTry.withThrowable[IllegalArgumentException](Pattern.quote("Invalid spec: null"))
366366
val failures = Seq(
367-
("foo:Strong", "7. Expected attribute type binding"),
368-
("foo:String,*bar:String", "16. Expected geometry type binding"),
369-
("foo:String,bar:String;;", "22. Expected one of: specification, feature type option, end of spec"),
370-
("foo:String,bar,baz:String", "14. Expected one of: attribute name, attribute, attribute type binding, geometry type binding"),
371-
("foo:String:bar,baz:String", "14. Expected attribute option")
367+
("foo:Strong", "7 - expected attribute type binding"),
368+
("foo:String,*bar:String", "16 - expected geometry type binding"),
369+
("foo:String,bar:String;;", "22 - expected one of: specification, feature type option, end of spec"),
370+
("foo:String,bar,baz:String", "14 - expected one of: attribute name, attribute, attribute type binding, geometry type binding"),
371+
("foo:String:bar,baz:String", "14 - expected attribute option")
372372
)
373373
forall(failures) { case (spec, message) =>
374-
val pattern = Pattern.quote(s"Invalid spec string at index $message.")
374+
val pattern = Pattern.quote(s"Invalid spec at index $message\n$spec\n") + " *\\^"
375375
val result = Try(SimpleFeatureTypes.createType("test", spec))
376376
result must beAFailedTry.withThrowable[IllegalArgumentException](pattern)
377377
}
@@ -664,13 +664,5 @@ class SimpleFeatureTypesTest extends Specification {
664664
"geomesa.table.sharing.prefix" -> "\u0001"
665665
)
666666
}
667-
668-
"preserve spaces in user data" >> {
669-
val sft = SimpleFeatureTypes.createType("test", "name:String,dtg:Date,*geom:Point:srid=4326;geomesa.foo='1 day'")
670-
sft.getUserData.get("geomesa.foo") mustEqual "1 day"
671-
val encoded = SimpleFeatureTypes.encodeType(sft, includeUserData = true)
672-
SimpleFeatureTypes.createType("test", encoded).getUserData.get("geomesa.foo") mustEqual "1 day"
673-
}
674667
}
675-
676668
}

0 commit comments

Comments
 (0)