@@ -25,6 +25,14 @@ import org.openprojectx.bigdata.test.extensions.objectstore.ObjectStoreUploadSou
2525import org.openprojectx.bigdata.test.extensions.objectstore.S3BucketExtension
2626import org.openprojectx.bigdata.test.extensions.objectstore.S3UploadExtension
2727import org.openprojectx.bigdata.test.extensions.spark.SparkSqlPreparationExtensionProvider
28+ import org.tomlj.Toml
29+ import org.tomlj.TomlArray
30+ import org.tomlj.TomlTable
31+ import java.math.BigDecimal
32+ import java.time.LocalDate
33+ import java.time.LocalDateTime
34+ import java.time.LocalTime
35+ import java.time.OffsetDateTime
2836import java.util.ServiceLoader
2937
3038class BigDataExtensionsConfigLoader (
@@ -180,242 +188,34 @@ class BigDataExtensionsConfigLoader(
180188
181189private object TomlConfigParser {
182190 fun parse (text : String ): JsonObject {
183- val root: MutableMap <String , Any > = linkedMapOf()
184- var current: MutableMap <String , Any > = root
185- logicalLines(text).forEach { logicalLine ->
186- val index = logicalLine.line
187- val line = logicalLine.text
188- when {
189- line.isEmpty() -> Unit
190- line.startsWith(" [[" ) && line.endsWith(" ]]" ) ->
191- current = resolveArrayTable(root, line.substring(2 , line.length - 2 ).trim(), index)
192- line.startsWith(" [" ) && line.endsWith(" ]" ) ->
193- current = resolveTable(root, line.substring(1 , line.length - 1 ).trim(), index)
194- else -> {
195- val (key, value) = splitKeyValue(line, index)
196- current[key] = parseValue(value, index)
197- }
198- }
199- }
200- return toJson(root).jsonObject
201- }
202-
203- private data class TomlLine (val text : String , val line : Int )
204-
205- private fun logicalLines (text : String ): List <TomlLine > {
206- val lines = mutableListOf<TomlLine >()
207- val buffer = StringBuilder ()
208- var startLine = 0
209- var depth = 0
210- text.lineSequence().forEachIndexed { index, raw ->
211- val line = stripComment(raw).trim()
212- if (line.isEmpty() && buffer.isEmpty()) return @forEachIndexed
213- if (buffer.isEmpty()) startLine = index else buffer.append(' ' )
214- buffer.append(line)
215- depth + = nestingDelta(line)
216- if (depth == 0 ) {
217- lines + = TomlLine (buffer.toString().trim(), startLine)
218- buffer.clear()
219- }
220- }
221- require(buffer.isEmpty()) { " TOML line ${startLine + 1 } : unclosed array or inline table" }
222- return lines
223- }
224-
225- private fun resolveTable (root : MutableMap <String , Any >, path : String , line : Int ): MutableMap <String , Any > {
226- var current = root
227- parsePath(path, line).forEach { segment ->
228- current = when (val existing = current[segment]) {
229- null -> linkedMapOf<String , Any >().also { current[segment] = it }
230- is MutableMap <* , * > -> existing.asStringMap(line)
231- is MutableList <* > -> existing.lastOrNull().asStringMap(line)
232- else -> error(" TOML line ${line + 1 } : '$segment ' is not a table" )
233- }
234- }
235- return current
236- }
237-
238- private fun resolveArrayTable (root : MutableMap <String , Any >, path : String , line : Int ): MutableMap <String , Any > {
239- val segments = parsePath(path, line)
240- var current = root
241- segments.dropLast(1 ).forEach { segment ->
242- current = when (val existing = current[segment]) {
243- null -> linkedMapOf<String , Any >().also { current[segment] = it }
244- is MutableMap <* , * > -> existing.asStringMap(line)
245- is MutableList <* > -> existing.lastOrNull().asStringMap(line)
246- else -> error(" TOML line ${line + 1 } : '$segment ' is not a table" )
247- }
191+ val result = Toml .parse(text)
192+ require(! result.hasErrors()) {
193+ result.errors().joinToString(separator = System .lineSeparator()) { it.toString() }
194+ }
195+ return result.toJsonObject()
196+ }
197+
198+ private fun TomlTable.toJsonObject (): JsonObject =
199+ JsonObject (keySet().associateWith { key -> get(key).toJsonElement() })
200+
201+ private fun TomlArray.toJsonArray (): JsonArray =
202+ JsonArray ((0 until size()).map { index -> get(index).toJsonElement() })
203+
204+ private fun Any?.toJsonElement (): JsonElement =
205+ when (this ) {
206+ null -> JsonPrimitive (" null" )
207+ is String -> JsonPrimitive (this )
208+ is Boolean -> JsonPrimitive (this )
209+ is Long -> JsonPrimitive (this )
210+ is Double -> JsonPrimitive (this )
211+ is BigDecimal -> JsonPrimitive (this )
212+ is TomlTable -> toJsonObject()
213+ is TomlArray -> toJsonArray()
214+ is OffsetDateTime ,
215+ is LocalDateTime ,
216+ is LocalDate ,
217+ is LocalTime ,
218+ -> JsonPrimitive (toString())
219+ else -> error(" Unsupported TOML value type: ${this ::class } " )
248220 }
249- val name = segments.last()
250- val array = when (val existing = current[name]) {
251- null -> mutableListOf<MutableMap <String , Any >>().also { current[name] = it }
252- is MutableList <* > -> existing.asMutableList(line)
253- else -> error(" TOML line ${line + 1 } : '$name ' is not an array of tables" )
254- }
255- return linkedMapOf<String , Any >().also { array + = it }
256- }
257-
258- private fun parseValue (value : String , line : Int ): Any {
259- val trimmed = value.trim()
260- return when {
261- trimmed.startsWith(' "' ) && trimmed.endsWith(' "' ) -> parseString(trimmed, line)
262- trimmed.startsWith(" {" ) && trimmed.endsWith(" }" ) -> parseInlineTable(trimmed, line)
263- trimmed.startsWith(" [" ) && trimmed.endsWith(" ]" ) -> parseArray(trimmed, line)
264- trimmed == " true" -> true
265- trimmed == " false" -> false
266- trimmed.toIntOrNull() != null -> trimmed.toInt()
267- trimmed.toLongOrNull() != null -> trimmed.toLong()
268- trimmed.toDoubleOrNull() != null -> trimmed.toDouble()
269- else -> error(" TOML line ${line + 1 } : unsupported value '$trimmed '" )
270- }
271- }
272-
273- private fun parseInlineTable (value : String , line : Int ): Map <String , Any > {
274- val body = value.substring(1 , value.length - 1 ).trim()
275- if (body.isEmpty()) return emptyMap()
276- return splitTopLevel(body, ' ,' ).associate { item ->
277- val (key, itemValue) = splitKeyValue(item, line)
278- key to parseValue(itemValue, line)
279- }
280- }
281-
282- private fun parseArray (value : String , line : Int ): List <Any > {
283- val body = value.substring(1 , value.length - 1 ).trim()
284- if (body.isEmpty()) return emptyList()
285- return splitTopLevel(body, ' ,' ).map { parseValue(it, line) }
286- }
287-
288- private fun splitKeyValue (lineText : String , line : Int ): Pair <String , String > {
289- val index = lineText.indexOfTopLevel(' =' )
290- require(index > 0 ) { " TOML line ${line + 1 } : expected key = value" }
291- return parseKey(lineText.substring(0 , index).trim(), line) to lineText.substring(index + 1 ).trim()
292- }
293-
294- private fun parseKey (key : String , line : Int ): String {
295- require(key.isNotEmpty()) { " TOML line ${line + 1 } : empty key" }
296- return if (key.startsWith(' "' ) && key.endsWith(' "' )) parseString(key, line) else key
297- }
298-
299- private fun parsePath (path : String , line : Int ): List <String > =
300- path.split(' .' ).map { parseKey(it.trim(), line) }.also {
301- require(it.isNotEmpty() && it.none(String ::isEmpty)) { " TOML line ${line + 1 } : invalid table path '$path '" }
302- }
303-
304- private fun parseString (value : String , line : Int ): String {
305- val body = value.substring(1 , value.length - 1 )
306- val result = StringBuilder ()
307- var escaped = false
308- body.forEach { char ->
309- if (escaped) {
310- result.append(
311- when (char) {
312- ' b' -> ' \b '
313- ' t' -> ' \t '
314- ' n' -> ' \n '
315- ' f' -> ' \u000C '
316- ' r' -> ' \r '
317- ' "' -> ' "'
318- ' \\ ' -> ' \\ '
319- else -> error(" TOML line ${line + 1 } : unsupported escape '\\ $char '" )
320- },
321- )
322- escaped = false
323- } else if (char == ' \\ ' ) {
324- escaped = true
325- } else {
326- result.append(char)
327- }
328- }
329- require(! escaped) { " TOML line ${line + 1 } : dangling string escape" }
330- return result.toString()
331- }
332-
333- private fun stripComment (raw : String ): String {
334- var inString = false
335- var escaped = false
336- raw.forEachIndexed { index, char ->
337- when {
338- escaped -> escaped = false
339- char == ' \\ ' && inString -> escaped = true
340- char == ' "' -> inString = ! inString
341- char == ' #' && ! inString -> return raw.substring(0 , index)
342- }
343- }
344- return raw
345- }
346-
347- private fun String.indexOfTopLevel (target : Char ): Int {
348- var inString = false
349- var escaped = false
350- var depth = 0
351- forEachIndexed { index, char ->
352- when {
353- escaped -> escaped = false
354- char == ' \\ ' && inString -> escaped = true
355- char == ' "' -> inString = ! inString
356- ! inString && (char == ' {' || char == ' [' ) -> depth++
357- ! inString && (char == ' }' || char == ' ]' ) -> depth--
358- ! inString && depth == 0 && char == target -> return index
359- }
360- }
361- return - 1
362- }
363-
364- private fun splitTopLevel (value : String , delimiter : Char ): List <String > {
365- val items = mutableListOf<String >()
366- var start = 0
367- var inString = false
368- var escaped = false
369- var depth = 0
370- value.forEachIndexed { index, char ->
371- when {
372- escaped -> escaped = false
373- char == ' \\ ' && inString -> escaped = true
374- char == ' "' -> inString = ! inString
375- ! inString && (char == ' {' || char == ' [' ) -> depth++
376- ! inString && (char == ' }' || char == ' ]' ) -> depth--
377- ! inString && depth == 0 && char == delimiter -> {
378- items + = value.substring(start, index).trim()
379- start = index + 1
380- }
381- }
382- }
383- items + = value.substring(start).trim()
384- return items.filter { it.isNotEmpty() }
385- }
386-
387- private fun nestingDelta (value : String ): Int {
388- var inString = false
389- var escaped = false
390- var delta = 0
391- value.forEach { char ->
392- when {
393- escaped -> escaped = false
394- char == ' \\ ' && inString -> escaped = true
395- char == ' "' -> inString = ! inString
396- ! inString && (char == ' {' || char == ' [' ) -> delta++
397- ! inString && (char == ' }' || char == ' ]' ) -> delta--
398- }
399- }
400- return delta
401- }
402-
403- private fun toJson (value : Any ): JsonElement = when (value) {
404- is String -> JsonPrimitive (value)
405- is Boolean -> JsonPrimitive (value)
406- is Int -> JsonPrimitive (value)
407- is Long -> JsonPrimitive (value)
408- is Double -> JsonPrimitive (value)
409- is Map <* , * > -> JsonObject (value.mapKeys { it.key as String }.mapValues { toJson(it.value ? : " null" ) })
410- is List <* > -> JsonArray (value.map { toJson(it ? : " null" ) })
411- else -> error(" Unsupported TOML value type: ${value::class } " )
412- }
413-
414- @Suppress(" UNCHECKED_CAST" )
415- private fun Any?.asStringMap (line : Int ): MutableMap <String , Any > =
416- this as ? MutableMap <String , Any > ? : error(" TOML line ${line + 1 } : expected table" )
417-
418- @Suppress(" UNCHECKED_CAST" )
419- private fun Any?.asMutableList (line : Int ): MutableList <MutableMap <String , Any >> =
420- this as ? MutableList <MutableMap <String , Any >> ? : error(" TOML line ${line + 1 } : expected array of tables" )
421221}
0 commit comments