package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Small JSON value parser used by nested trigger-variable containers. */
final class TriggerJsonValueParser {
private final String source;
private int position;
private TriggerJsonValueParser(String source) {
this.source = source;
}
static Object parse(String source) {
if (source == null || source.length() > 65_536) {
throw new IllegalArgumentException("structured variable value is too long");
}
TriggerJsonValueParser parser = new TriggerJsonValueParser(source);
Object value = parser.value();
parser.whitespace();
if (parser.position != source.length()) throw parser.invalid("unexpected trailing content");
return value;
}
private Object value() {
whitespace();
if (position >= source.length()) throw invalid("expected a JSON value");
return switch (source.charAt(position)) {
case '"' -> string();
case '[' -> array();
case '{' -> object();
case 't' -> literal("true", Boolean.TRUE);
case 'f' -> literal("false", Boolean.FALSE);
case 'n' -> literal("null", null);
default -> number();
};
}
private List<Object> array() {
position++;
List<Object> result = new ArrayList<>();
whitespace();
if (consume(']')) return List.of();
while (true) {
result.add(value());
whitespace();
if (consume(']')) return Collections.unmodifiableList(new ArrayList<>(result));
require(',');
}
}
private Map<String, Object> object() {
position++;
Map<String, Object> result = new LinkedHashMap<>();
whitespace();
if (consume('}')) return Map.of();
while (true) {
whitespace();
if (position >= source.length() || source.charAt(position) != '"') {
throw invalid("dictionary keys must be JSON strings");
}
String key = string();
whitespace();
require(':');
if (result.containsKey(key)) {
throw invalid("duplicate dictionary key " + key);
}
result.put(key, value());
whitespace();
if (consume('}')) return Collections.unmodifiableMap(new LinkedHashMap<>(result));
require(',');
}
}
private String string() {
require('"');
StringBuilder result = new StringBuilder();
while (position < source.length()) {
char current = source.charAt(position++);
if (current == '"') return result.toString();
if (current < 0x20) throw invalid("control character in JSON string");
if (current != '\\') {
result.append(current);
continue;
}
if (position >= source.length()) throw invalid("unfinished JSON escape");
char escaped = source.charAt(position++);
switch (escaped) {
case '"', '\\', '/' -> result.append(escaped);
case 'b' -> result.append('\b');
case 'f' -> result.append('\f');
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case 't' -> result.append('\t');
case 'u' -> result.append(unicode());
default -> throw invalid("unsupported JSON escape");
}
}
throw invalid("unterminated JSON string");
}
private char unicode() {
if (position + 4 > source.length()) throw invalid("unfinished Unicode escape");
try {
char result = (char) Integer.parseInt(source.substring(position, position + 4), 16);
position += 4;
return result;
} catch (NumberFormatException invalid) {
throw invalid("invalid Unicode escape");
}
}
private Object number() {
int start = position;
if (consume('-') && position >= source.length()) throw invalid("unfinished number");
if (consume('0')) {
// A leading zero is complete unless a fraction or exponent follows.
} else {
digits();
}
boolean decimal = false;
if (consume('.')) {
decimal = true;
digits();
}
if (position < source.length() && (source.charAt(position) == 'e' || source.charAt(position) == 'E')) {
decimal = true;
position++;
if (position < source.length() && (source.charAt(position) == '+' || source.charAt(position) == '-')) position++;
digits();
}
String raw = source.substring(start, position);
try {
if (!decimal) return Long.parseLong(raw);
double value = Double.parseDouble(raw);
if (!Double.isFinite(value)) throw invalid("number must be finite");
return value;
} catch (NumberFormatException invalid) {
throw invalid("invalid JSON number");
}
}
private void digits() {
int start = position;
while (position < source.length() && Character.isDigit(source.charAt(position))) position++;
if (position == start) throw invalid("expected a digit");
}
private Object literal(String literal, Object value) {
if (!source.startsWith(literal, position)) throw invalid("invalid JSON value");
position += literal.length();
return value;
}
private void whitespace() {
while (position < source.length() && Character.isWhitespace(source.charAt(position))) position++;
}
private boolean consume(char expected) {
if (position < source.length() && source.charAt(position) == expected) {
position++;
return true;
}
return false;
}
private void require(char expected) {
if (!consume(expected)) throw invalid("expected '" + expected + "'");
}
private IllegalArgumentException invalid(String message) {
return new IllegalArgumentException(message + " at character " + (position + 1));
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Small JSON value parser used by nested trigger-variable containers. */
final class TriggerJsonValueParser {
private final String source;
private int position;
private TriggerJsonValueParser(String source) {
this.source = source;
}
static Object parse(String source) {
if (source == null || source.length() > 65_536) {
throw new IllegalArgumentException("structured variable value is too long");
}
TriggerJsonValueParser parser = new TriggerJsonValueParser(source);
Object value = parser.value();
parser.whitespace();
if (parser.position != source.length()) throw parser.invalid("unexpected trailing content");
return value;
}
private Object value() {
whitespace();
if (position >= source.length()) throw invalid("expected a JSON value");
return switch (source.charAt(position)) {
case '"' -> string();
case '[' -> array();
case '{' -> object();
case 't' -> literal("true", Boolean.TRUE);
case 'f' -> literal("false", Boolean.FALSE);
case 'n' -> literal("null", null);
default -> number();
};
}
private List<Object> array() {
position++;
List<Object> result = new ArrayList<>();
whitespace();
if (consume(']')) return List.of();
while (true) {
result.add(value());
whitespace();
if (consume(']')) return Collections.unmodifiableList(new ArrayList<>(result));
require(',');
}
}
private Map<String, Object> object() {
position++;
Map<String, Object> result = new LinkedHashMap<>();
whitespace();
if (consume('}')) return Map.of();
while (true) {
whitespace();
if (position >= source.length() || source.charAt(position) != '"') {
throw invalid("dictionary keys must be JSON strings");
}
String key = string();
whitespace();
require(':');
if (result.containsKey(key)) {
throw invalid("duplicate dictionary key " + key);
}
result.put(key, value());
whitespace();
if (consume('}')) return Collections.unmodifiableMap(new LinkedHashMap<>(result));
require(',');
}
}
private String string() {
require('"');
StringBuilder result = new StringBuilder();
while (position < source.length()) {
char current = source.charAt(position++);
if (current == '"') return result.toString();
if (current < 0x20) throw invalid("control character in JSON string");
if (current != '\\') {
result.append(current);
continue;
}
if (position >= source.length()) throw invalid("unfinished JSON escape");
char escaped = source.charAt(position++);
switch (escaped) {
case '"', '\\', '/' -> result.append(escaped);
case 'b' -> result.append('\b');
case 'f' -> result.append('\f');
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case 't' -> result.append('\t');
case 'u' -> result.append(unicode());
default -> throw invalid("unsupported JSON escape");
}
}
throw invalid("unterminated JSON string");
}
private char unicode() {
if (position + 4 > source.length()) throw invalid("unfinished Unicode escape");
try {
char result = (char) Integer.parseInt(source.substring(position, position + 4), 16);
position += 4;
return result;
} catch (NumberFormatException invalid) {
throw invalid("invalid Unicode escape");
}
}
private Object number() {
int start = position;
if (consume('-') && position >= source.length()) throw invalid("unfinished number");
if (consume('0')) {
// A leading zero is complete unless a fraction or exponent follows.
} else {
digits();
}
boolean decimal = false;
if (consume('.')) {
decimal = true;
digits();
}
if (position < source.length() && (source.charAt(position) == 'e' || source.charAt(position) == 'E')) {
decimal = true;
position++;
if (position < source.length() && (source.charAt(position) == '+' || source.charAt(position) == '-')) position++;
digits();
}
String raw = source.substring(start, position);
try {
if (!decimal) return Long.parseLong(raw);
double value = Double.parseDouble(raw);
if (!Double.isFinite(value)) throw invalid("number must be finite");
return value;
} catch (NumberFormatException invalid) {
throw invalid("invalid JSON number");
}
}
private void digits() {
int start = position;
while (position < source.length() && Character.isDigit(source.charAt(position))) position++;
if (position == start) throw invalid("expected a digit");
}
private Object literal(String literal, Object value) {
if (!source.startsWith(literal, position)) throw invalid("invalid JSON value");
position += literal.length();
return value;
}
private void whitespace() {
while (position < source.length() && Character.isWhitespace(source.charAt(position))) position++;
}
private boolean consume(char expected) {
if (position < source.length() && source.charAt(position) == expected) {
position++;
return true;
}
return false;
}
private void require(char expected) {
if (!consume(expected)) throw invalid("expected '" + expected + "'");
}
private IllegalArgumentException invalid(String message) {
return new IllegalArgumentException(message + " at character " + (position + 1));
}
}