232 lines
8.5 KiB
Java
232 lines
8.5 KiB
Java
package eu.jonahbauer.json.parser;
|
|
|
|
import eu.jonahbauer.json.*;
|
|
import eu.jonahbauer.json.parser.tokenizer.JsonTokenizer;
|
|
import eu.jonahbauer.json.parser.tokenizer.JsonTokenizerImpl;
|
|
import eu.jonahbauer.json.token.*;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.io.IOException;
|
|
import java.io.Reader;
|
|
import java.io.UncheckedIOException;
|
|
import java.util.*;
|
|
|
|
public final class JsonParser {
|
|
|
|
private final @NotNull JsonTokenizer tokenizer;
|
|
|
|
public JsonParser(@NotNull String json) {
|
|
this(new JsonTokenizerImpl(json));
|
|
}
|
|
|
|
public JsonParser(@NotNull Reader reader) {
|
|
this(new JsonTokenizerImpl(reader));
|
|
}
|
|
|
|
public JsonParser(@NotNull StringTemplate json) {
|
|
this(new JsonTokenizerImpl(json));
|
|
}
|
|
|
|
JsonParser(@NotNull JsonTokenizer tokenizer) {
|
|
this.tokenizer = Objects.requireNonNull(tokenizer, "tokenizer");
|
|
}
|
|
|
|
public @Nullable JsonValue parse() throws JsonProcessingException {
|
|
try {
|
|
JsonValue out = readJsonValue();
|
|
if (tokenizer.next() != null) throw new JsonProcessingException(
|
|
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
|
"unexpected trailing entries"
|
|
);
|
|
return out;
|
|
} catch (IOException ex) {
|
|
throw new UncheckedIOException(ex);
|
|
}
|
|
}
|
|
|
|
private @Nullable JsonValue readJsonValue() throws IOException {
|
|
Deque<Context> stack = new ArrayDeque<>();
|
|
Context context = null;
|
|
|
|
while (true) {
|
|
JsonValue value;
|
|
|
|
JsonToken token = tokenizer.next();
|
|
if (context != null && token == context.end()) {
|
|
stack.pop();
|
|
value = context.build();
|
|
context = stack.peek();
|
|
} else {
|
|
// consume value separator
|
|
if (context != null && !context.isEmpty()) {
|
|
if (token != JsonPunctuation.VALUE_SEPARATOR) {
|
|
throw unexpectedToken(token, context.end(), JsonPunctuation.VALUE_SEPARATOR);
|
|
}
|
|
token = tokenizer.next();
|
|
}
|
|
|
|
// read object key and consume value separator
|
|
if (context instanceof Context.ObjectContext obj) {
|
|
String key = readJsonObjectKey(obj, token);
|
|
if ((token = tokenizer.next()) != JsonPunctuation.NAME_SEPARATOR) {
|
|
throw unexpectedToken(token, JsonPunctuation.NAME_SEPARATOR);
|
|
}
|
|
token = tokenizer.next();
|
|
obj.setKey(key);
|
|
}
|
|
|
|
if (token == JsonPunctuation.BEGIN_OBJECT) {
|
|
stack.push(context = new Context.ObjectContext());
|
|
continue;
|
|
} else if (token == JsonPunctuation.BEGIN_ARRAY) {
|
|
stack.push(context = new Context.ArrayContext());
|
|
continue;
|
|
} else {
|
|
// read value
|
|
value = switch (token) {
|
|
case JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY -> throw new AssertionError();
|
|
case JsonNull.NULL -> null;
|
|
case JsonValue v -> v;
|
|
case JsonStringTemplate template -> template.asString();
|
|
case JsonPlaceholder(var object) -> JsonValue.valueOf(object);
|
|
case JsonPunctuation _ -> throw unexpectedStartOfValue(token);
|
|
case null -> throw unexpectedEndOfFile();
|
|
};
|
|
}
|
|
}
|
|
|
|
if (context == null) {
|
|
return value;
|
|
} else {
|
|
context.add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private @NotNull String readJsonObjectKey(@NotNull Context.ObjectContext context, @Nullable JsonToken token) {
|
|
return switch (token) {
|
|
case JsonString string -> string.value();
|
|
case JsonStringTemplate template -> template.asString().value();
|
|
case JsonPlaceholder(var object) -> switch (object) {
|
|
case CharSequence chars -> chars.toString();
|
|
case null -> throw new JsonConversionException("cannot convert null to json key");
|
|
default -> throw new JsonConversionException(STR."cannot convert object of type \{object.getClass()} to json key");
|
|
};
|
|
case null -> throw unexpectedEndOfFile();
|
|
default -> throw unexpectedStartOfKey(context, token);
|
|
};
|
|
}
|
|
|
|
private @NotNull JsonProcessingException unexpectedStartOfKey(@NotNull Context.ObjectContext context, @Nullable JsonToken token) {
|
|
var expected = new ArrayList<JsonTokenKind>();
|
|
expected.add(JsonString.KIND);
|
|
if (context.isEmpty()) {
|
|
expected.add(JsonPunctuation.END_OBJECT);
|
|
}
|
|
if (tokenizer.hasTemplateSupport()) {
|
|
expected.add(JsonStringTemplate.KIND);
|
|
expected.add(JsonPlaceholder.KIND);
|
|
}
|
|
return unexpectedToken(token, expected.toArray(JsonTokenKind[]::new));
|
|
}
|
|
|
|
private @NotNull JsonProcessingException unexpectedStartOfValue(@Nullable JsonToken token) {
|
|
var expected = new ArrayList<JsonTokenKind>();
|
|
expected.add(JsonPunctuation.BEGIN_OBJECT);
|
|
expected.add(JsonPunctuation.BEGIN_ARRAY);
|
|
expected.add(JsonBoolean.TRUE);
|
|
expected.add(JsonBoolean.FALSE);
|
|
expected.add(JsonNull.NULL);
|
|
expected.add(JsonString.KIND);
|
|
expected.add(JsonNumber.KIND);
|
|
if (tokenizer.hasTemplateSupport()) {
|
|
expected.add(JsonStringTemplate.KIND);
|
|
expected.add(JsonPlaceholder.KIND);
|
|
}
|
|
return unexpectedToken(token, expected.toArray(JsonTokenKind[]::new));
|
|
}
|
|
|
|
private @NotNull JsonProcessingException unexpectedEndOfFile() {
|
|
return new JsonProcessingException(tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(), "unexpected end of file");
|
|
}
|
|
|
|
private @NotNull JsonProcessingException unexpectedToken(@Nullable JsonToken token, @NotNull JsonTokenKind @NotNull... expected) {
|
|
if (expected.length == 1) {
|
|
throw new JsonProcessingException(
|
|
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
|
STR."unexpected token: \{token} (expected \{expected[0]})"
|
|
);
|
|
} else {
|
|
throw new JsonProcessingException(
|
|
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
|
STR."unexpected token: \{token} (expected one of \{Arrays.toString(expected)})"
|
|
);
|
|
}
|
|
}
|
|
|
|
private interface Context {
|
|
boolean isEmpty();
|
|
void add(@Nullable JsonValue value);
|
|
@NotNull JsonValue build();
|
|
@NotNull JsonPunctuation end();
|
|
|
|
class ObjectContext implements Context {
|
|
private final SequencedMap<String, JsonValue> map = new LinkedHashMap<>();
|
|
private String key;
|
|
|
|
public void setKey(@NotNull String key) {
|
|
if (this.key != null) throw new IllegalStateException();
|
|
this.key = key;
|
|
}
|
|
|
|
@Override
|
|
public void add(@Nullable JsonValue value) {
|
|
if (key == null) throw new IllegalStateException();
|
|
map.put(key, value);
|
|
key = null;
|
|
}
|
|
|
|
@Override
|
|
public boolean isEmpty() {
|
|
return map.isEmpty();
|
|
}
|
|
|
|
@Override
|
|
public @NotNull JsonObject build() {
|
|
if (this.key != null) throw new IllegalStateException();
|
|
return new JsonObject(map);
|
|
}
|
|
|
|
@Override
|
|
public @NotNull JsonPunctuation end() {
|
|
return JsonPunctuation.END_OBJECT;
|
|
}
|
|
}
|
|
|
|
class ArrayContext implements Context {
|
|
private final List<JsonValue> list = new ArrayList<>();
|
|
|
|
@Override
|
|
public void add(@Nullable JsonValue value) {
|
|
list.add(value);
|
|
}
|
|
|
|
@Override
|
|
public boolean isEmpty() {
|
|
return list.isEmpty();
|
|
}
|
|
|
|
@Override
|
|
public @NotNull JsonArray build() {
|
|
return new JsonArray(list);
|
|
}
|
|
|
|
@Override
|
|
public @NotNull JsonPunctuation end() {
|
|
return JsonPunctuation.END_ARRAY;
|
|
}
|
|
}
|
|
}
|
|
}
|