json/src/main/java/eu/jonahbauer/json/parser/JsonParser.java
2024-01-24 17:39:32 +01:00

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;
}
}
}
}