
commonMain.com.nfeld.jsonpathkt.PathCompiler.kt Maven / Gradle / Ivy
package com.nfeld.jsonpathkt
import com.nfeld.jsonpathkt.tokens.ArrayAccessorToken
import com.nfeld.jsonpathkt.tokens.ArrayLengthBasedRangeAccessorToken
import com.nfeld.jsonpathkt.tokens.DeepScanArrayAccessorToken
import com.nfeld.jsonpathkt.tokens.DeepScanLengthBasedArrayAccessorToken
import com.nfeld.jsonpathkt.tokens.DeepScanObjectAccessorToken
import com.nfeld.jsonpathkt.tokens.DeepScanWildcardToken
import com.nfeld.jsonpathkt.tokens.MultiArrayAccessorToken
import com.nfeld.jsonpathkt.tokens.MultiObjectAccessorToken
import com.nfeld.jsonpathkt.tokens.ObjectAccessorToken
import com.nfeld.jsonpathkt.tokens.Token
import com.nfeld.jsonpathkt.tokens.WildcardToken
internal object PathCompiler {
/**
* @param path Path string to compile
* @return List of [Token] to read against a JSON
*/
@Throws(IllegalArgumentException::class, IllegalStateException::class)
fun compile(path: String): List {
require(path.isNotBlank()) {
"Path cannot be empty"
}
val tokens = mutableListOf()
var isDeepScan = false
var isWildcard = false
val keyBuilder = StringBuilder()
fun resetForNextToken() {
isDeepScan = false
isWildcard = false
keyBuilder.clear()
}
fun addObjectAccessorToken() {
val key = keyBuilder.toString()
val token = when {
isDeepScan && isWildcard -> DeepScanWildcardToken()
isDeepScan -> DeepScanObjectAccessorToken(listOf(key))
isWildcard -> WildcardToken()
else -> ObjectAccessorToken(key)
}
tokens.add(token)
}
val len = path.length
var i = if (path.firstOrNull() == '$') 1 else 0 // $ symbol is optional
while (i < len) {
val c = path[i]
val next = path.getOrNull(i + 1)
when {
c == '*' && isDeepScan -> {
isWildcard = true
}
c == '.' -> {
if (keyBuilder.isNotEmpty() || isWildcard) {
addObjectAccessorToken()
resetForNextToken()
}
// check if it's followed by another dot. This means the following key will be used in deep scan
when (next) {
'.' -> {
isDeepScan = true
++i
}
'*' -> {
isWildcard = true
++i
}
null -> throw IllegalArgumentException("Unexpected ending with dot")
}
}
c == '[' -> {
if (keyBuilder.isNotEmpty() || isWildcard) {
addObjectAccessorToken()
resetForNextToken()
}
val closingBracketIndex = findMatchingClosingBracket(path, i)
// i+1 checks to make sure atleast one char in the brackets
require(closingBracketIndex > i + 1) {
"Expecting closing array bracket with a value inside"
}
val token = compileBracket(path, i, closingBracketIndex)
if (isDeepScan) {
val deepScanToken: Token? = when (token) {
is WildcardToken -> DeepScanWildcardToken()
is ObjectAccessorToken -> DeepScanObjectAccessorToken(listOf(token.key))
is MultiObjectAccessorToken -> DeepScanObjectAccessorToken(token.keys)
is ArrayAccessorToken -> DeepScanArrayAccessorToken(listOf(token.index))
is MultiArrayAccessorToken -> DeepScanArrayAccessorToken(token.indices)
is ArrayLengthBasedRangeAccessorToken -> DeepScanLengthBasedArrayAccessorToken(
token.startIndex,
token.endIndex,
token.offsetFromEnd,
)
else -> null
}
deepScanToken?.let { tokens.add(it) }
resetForNextToken()
} else {
tokens.add(token)
}
i = closingBracketIndex
}
else -> keyBuilder.append(c)
}
++i
}
if (keyBuilder.isNotEmpty() || isWildcard) {
addObjectAccessorToken()
}
return tokens.toList()
}
/**
* @param path original path
* @param openingIndex opening bracket index we are to search matching closing bracket for
* @return closing bracket index, or -1 if not found
*/
fun findMatchingClosingBracket(path: String, openingIndex: Int): Int {
var isQuoteOpened = false
var isSingleQuote = false // either single quote or double quote opened if isQuoteOpened
var i = openingIndex + 1
val len = path.length
while (i < len) {
val c = path[i]
val next = path.getOrNull(i + 1)
when {
c == '\'' || c == '"' -> {
when {
!isQuoteOpened -> {
isQuoteOpened = true
isSingleQuote = c == '\''
}
isSingleQuote && c == '\'' -> {
isQuoteOpened = false
}
!isSingleQuote && c == '"' -> {
isQuoteOpened = false
}
}
}
c == ']' && !isQuoteOpened -> return i
c == '\\' && isQuoteOpened -> {
if (next == '\'' || next == '\\' || next == '"') {
++i // skip this char so we don't process escaped quote
} else {
requireNotNull(next) {
"Unexpected char at end of path"
}
}
}
}
++i
}
return -1
}
/**
* Compile path expression inside of brackets
*
* @param path original path
* @param openingIndex index of opening bracket
* @param closingIndex index of closing bracket
* @return Compiled [Token]
*/
fun compileBracket(path: String, openingIndex: Int, closingIndex: Int): Token {
// isObjectAccessor is separate from expectingClosingQuote because the second you open a quote, it's always an object,
// but we we can have multiple keys and thus multiple quotes opened for that object.
var isObjectAccessor = false // once this is set, it cant be anything else
var isNegativeArrayAccessor = false // supplements isArrayAccessor
var isQuoteOpened = false // means we found an opening quote, so we expect a closing one to be valid
var isSingleQuote = false // either single quote or double quote opened
var hasStartColon = false // found colon in beginning
var hasEndColon = false // found colon in end
var isRange = false // has starting and ending range. There will be two keys containing indices of each
var isWildcard = false
var i = openingIndex + 1
var lastChar: Char = path[openingIndex]
val keys = mutableListOf()
val keyBuilder = StringBuilder()
fun buildAndAddKey() {
var key = keyBuilder.toString()
if (!isObjectAccessor && isNegativeArrayAccessor) {
key = "-$key"
isNegativeArrayAccessor = false
}
keys.add(key)
keyBuilder.clear()
}
fun getNextCharIgnoringWhitespace(): Char {
for (n in i + 1..closingIndex) {
val c = path[n]
if (c == ' ' && !isQuoteOpened) {
continue
}
return c
}
error("Shouldn't reach this point")
}
fun isBracketNext() = getNextCharIgnoringWhitespace() == ']'
fun isBracketBefore() = lastChar == '['
while (i < closingIndex) {
val c = path[i]
var setLastChar = true
when {
c == ' ' && !isQuoteOpened -> {
// skip empty space that's not enclosed in quotes
setLastChar = false
}
c == ':' && !isQuoteOpened -> {
if (isBracketBefore() && isBracketNext()) {
hasStartColon = true
hasEndColon = true
} else if (isBracketBefore()) {
hasStartColon = true
} else if (isBracketNext()) {
hasEndColon = true
// keybuilder should have a key...
buildAndAddKey()
} else if (keyBuilder.isNotEmpty()) {
buildAndAddKey() // becomes starting index of range
isRange = true
}
}
c == '-' && !isObjectAccessor -> {
isNegativeArrayAccessor = true
}
c == ',' && !isQuoteOpened -> {
// object accessor would have added key on closing quote
if (!isObjectAccessor && keyBuilder.isNotEmpty()) {
buildAndAddKey()
}
}
c == '\\' && isQuoteOpened -> {
when (val nextChar = path[i + 1]) {
'\\', '\'', '"' -> {
keyBuilder.append(nextChar)
++i
}
}
}
c == '\'' && isQuoteOpened && isSingleQuote -> { // only valid inside array bracket and ending
buildAndAddKey()
isQuoteOpened = false
}
c == '"' && isQuoteOpened && !isSingleQuote -> { // only valid inside array bracket and ending
buildAndAddKey()
isQuoteOpened = false
}
(c == '\'' || c == '"') && !isNegativeArrayAccessor && !isQuoteOpened -> {
isQuoteOpened = true
isSingleQuote = c == '\''
isObjectAccessor = true
}
c == '*' && !isQuoteOpened && isBracketBefore() && isBracketNext() -> {
isWildcard = true
}
c.isDigit() && !isQuoteOpened || isObjectAccessor && isQuoteOpened -> keyBuilder.append(c)
else -> throw IllegalArgumentException("Unexpected char, char=$c, index=$i")
}
++i
if (setLastChar) {
lastChar = c
}
}
if (keyBuilder.isNotEmpty()) {
buildAndAddKey()
}
val token: Token? = if (isObjectAccessor) {
if (keys.size > 1) {
MultiObjectAccessorToken(keys)
} else {
keys.firstOrNull()?.let {
ObjectAccessorToken(it)
}
}
} else {
when {
isWildcard -> WildcardToken()
isRange -> {
val start = keys[0].toInt(10)
val end = keys[1].toInt(10) // exclusive
val isEndNegative = end < 0
if (start < 0 || isEndNegative) {
val offsetFromEnd = if (isEndNegative) end else 0
val endIndex = if (!isEndNegative) end else null
ArrayLengthBasedRangeAccessorToken(start, endIndex, offsetFromEnd)
} else {
MultiArrayAccessorToken(IntRange(start, end - 1).toList())
}
}
hasStartColon && hasEndColon -> {
// take entire list from beginning to end
ArrayLengthBasedRangeAccessorToken(0, null, 0)
}
hasStartColon -> {
val end = keys[0].toInt(10) // exclusive
if (end < 0) {
// take all from beginning to last minus $end
ArrayLengthBasedRangeAccessorToken(0, null, end)
} else {
// take all from beginning of list up to $end
MultiArrayAccessorToken(IntRange(0, end - 1).toList())
}
}
hasEndColon -> {
val start = keys[0].toInt(10)
ArrayLengthBasedRangeAccessorToken(start)
}
keys.size == 1 -> ArrayAccessorToken(keys[0].toInt(10))
keys.size > 1 -> MultiArrayAccessorToken(keys.map { it.toInt(10) })
else -> null
}
}
token?.let {
return it
}
throw IllegalArgumentException("Not a valid path")
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy