Compare commits
No commits in common. "4d9a3ef4abd27d67a1fc8f2b9b636e6f8e449ae3" and "5a5d1492a57199474a143eeb4d28f9061465afd7" have entirely different histories.
4d9a3ef4ab
...
5a5d1492a5
25
README.md
25
README.md
@ -1,27 +1,2 @@
|
|||||||
# json
|
# json
|
||||||
|
|
||||||
A simple JSON library with support for `StringTemplate`s which were available in JDK 21 and 22 as a preview
|
|
||||||
feature and removed again in JDK 23. The library allows placeholders to be used as keys, values and inside strings.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
```java
|
|
||||||
var parameter = "numbers";
|
|
||||||
var value = List.of(1, 2, 3, 4);
|
|
||||||
var name = "World";
|
|
||||||
|
|
||||||
var json = JSON_OBJECT."""
|
|
||||||
{
|
|
||||||
\{parameter}: \{value},
|
|
||||||
"string": "Hello \{name}!"
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
assertEquals(3, json.getArray("numbers").getNumber(2));
|
|
||||||
assertEquals(JsonString.valueOf("Hello World!"), json.get("string"));
|
|
||||||
|
|
||||||
System.out.println(json.toPrettyJsonString());
|
|
||||||
// {
|
|
||||||
// "numbers": [1, 2, 3, 4],
|
|
||||||
// "string": "Hello World!"
|
|
||||||
// }
|
|
||||||
```
|
|
@ -8,18 +8,20 @@ description = "json"
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(22)
|
version = 21
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(libs.annotations)
|
api(libs.annotations)
|
||||||
|
|
||||||
compileOnly(libs.lombok)
|
compileOnly(libs.lombok)
|
||||||
annotationProcessor(libs.lombok)
|
annotationProcessor(libs.lombok)
|
||||||
|
testImplementation(libs.bundles.junit)
|
||||||
testImplementation(libs.junit.jupiter)
|
|
||||||
testRuntimeOnly(libs.junit.platform.launcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
@ -1,409 +0,0 @@
|
|||||||
package eu.jonahbauer.json;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
|
||||||
import org.jetbrains.annotations.Contract;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Java representation of a JSON array.
|
|
||||||
* @param elements the array's elements
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public record JsonArray(
|
|
||||||
@NotNull List<? extends @Nullable JsonValue> elements
|
|
||||||
) implements JsonValue, List<@Nullable JsonValue> {
|
|
||||||
public static final @NotNull JsonArray EMPTY = new JsonArray(List.of());
|
|
||||||
|
|
||||||
public JsonArray {
|
|
||||||
Objects.requireNonNull(elements, "elements");
|
|
||||||
elements = Util.defensiveCopy(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull JsonArray of(@Nullable JsonValue @NotNull... elements) {
|
|
||||||
return new JsonArray(Arrays.stream(elements).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
//<editor-fold desc="valueOf(...)" defaultstate="collapsed">
|
|
||||||
/**
|
|
||||||
* Creates a JSON array from the given list, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
|
||||||
* {@link JsonValue}s.
|
|
||||||
* @param list a list of objects
|
|
||||||
* @return a new JSON array
|
|
||||||
* @throws JsonConversionException if the given list or its entries cannot be converted to json
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(@Nullable List<?> list) {
|
|
||||||
if (list == null) return null;
|
|
||||||
if (list instanceof JsonArray json) return json;
|
|
||||||
return new JsonArray(list.stream().map(JsonValue::valueOf).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
|
||||||
* {@link JsonValue}s.
|
|
||||||
* @param array an array of objects
|
|
||||||
* @return a new JSON array
|
|
||||||
* @throws JsonConversionException if the given array or its entries cannot be converted to json
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(@Nullable Object @Nullable... array) {
|
|
||||||
return array == null ? null : new JsonArray(Arrays.stream(array).map(JsonValue::valueOf).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonBoolean#valueOf(boolean)} to convert the
|
|
||||||
* booleans to {@link JsonBoolean}s.
|
|
||||||
* @param array an array of booleans
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(boolean @Nullable... array) {
|
|
||||||
if (array == null) return null;
|
|
||||||
var list = new ArrayList<JsonValue>();
|
|
||||||
for (var b : array) {
|
|
||||||
list.add(JsonBoolean.valueOf(b));
|
|
||||||
}
|
|
||||||
return new JsonArray(Util.trusted(Collections.unmodifiableList(list)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(int)} to convert the
|
|
||||||
* bytes to {@link JsonNumber}s.
|
|
||||||
* @param array an array of bytes
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(byte @Nullable... array) {
|
|
||||||
if (array == null) return null;
|
|
||||||
var list = new ArrayList<JsonValue>();
|
|
||||||
for (var b : array) {
|
|
||||||
list.add(JsonNumber.valueOf(b));
|
|
||||||
}
|
|
||||||
return new JsonArray(Util.trusted(Collections.unmodifiableList(list)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(int)} to convert the
|
|
||||||
* shorts to {@link JsonNumber}s.
|
|
||||||
* @param array an array of shorts
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(short @Nullable... array) {
|
|
||||||
if (array == null) return null;
|
|
||||||
var list = new ArrayList<JsonValue>();
|
|
||||||
for (var s : array) {
|
|
||||||
list.add(JsonNumber.valueOf(s));
|
|
||||||
}
|
|
||||||
return new JsonArray(Util.trusted(Collections.unmodifiableList(list)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonString#valueOf(char)} to convert the
|
|
||||||
* chars to {@link JsonString}s.
|
|
||||||
* @param array an array of chars
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(char @Nullable... array) {
|
|
||||||
if (array == null) return null;
|
|
||||||
var list = new ArrayList<JsonValue>();
|
|
||||||
for (var c : array) {
|
|
||||||
list.add(JsonString.valueOf(c));
|
|
||||||
}
|
|
||||||
return new JsonArray(Util.trusted(Collections.unmodifiableList(list)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(int)} to convert the
|
|
||||||
* ints to {@link JsonNumber}s.
|
|
||||||
* @param array an array of ints
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(int @Nullable... array) {
|
|
||||||
return array == null ? null : new JsonArray(Arrays.stream(array).mapToObj(JsonNumber::valueOf).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(long)} to convert the
|
|
||||||
* longs to {@link JsonNumber}s.
|
|
||||||
* @param array an array of longs
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(long @Nullable... array) {
|
|
||||||
return array == null ? null : new JsonArray(Arrays.stream(array).mapToObj(JsonNumber::valueOf).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(double)} to convert the
|
|
||||||
* floats to {@link JsonNumber}s.
|
|
||||||
* @param array an array of floats
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(float @Nullable... array) {
|
|
||||||
if (array == null) return null;
|
|
||||||
var list = new ArrayList<JsonValue>();
|
|
||||||
for (var f : array) {
|
|
||||||
list.add(JsonNumber.valueOf(f));
|
|
||||||
}
|
|
||||||
return new JsonArray(Util.trusted(Collections.unmodifiableList(list)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new JSON array from the given array, using {@link JsonNumber#valueOf(double)} to convert the
|
|
||||||
* doubles to {@link JsonNumber}s.
|
|
||||||
* @param array an array of doubles
|
|
||||||
* @return a new JSON array
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonArray valueOf(double @Nullable... array) {
|
|
||||||
return array == null ? null : new JsonArray(Arrays.stream(array).mapToObj(JsonNumber::valueOf).toList());
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
//<editor-fold desc="List" defaultstate="collapsed">
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return elements.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return elements.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean contains(Object o) {
|
|
||||||
return elements.contains(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked") // elements is immutable
|
|
||||||
public @NotNull Iterator<@Nullable JsonValue> iterator() {
|
|
||||||
return (Iterator<JsonValue>) elements.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object @NotNull[] toArray() {
|
|
||||||
return elements.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T @NotNull[] toArray(T @NotNull[] a) {
|
|
||||||
return elements.toArray(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean add(@Nullable JsonValue value) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean remove(Object o) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("SlowListContainsAll")
|
|
||||||
public boolean containsAll(@NotNull Collection<?> c) {
|
|
||||||
return elements.containsAll(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean addAll(@NotNull Collection<? extends @Nullable JsonValue> c) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean addAll(int index, @NotNull Collection<? extends @Nullable JsonValue> c) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean removeAll(@NotNull Collection<?> c) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean retainAll(@NotNull Collection<?> c) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue get(int index) {
|
|
||||||
return elements.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue set(int index, @Nullable JsonValue element) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void add(int index, @Nullable JsonValue element) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue remove(int index) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int indexOf(Object o) {
|
|
||||||
return elements.indexOf(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int lastIndexOf(Object o) {
|
|
||||||
return elements.lastIndexOf(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked") // elements is immutable
|
|
||||||
public @NotNull ListIterator<@Nullable JsonValue> listIterator() {
|
|
||||||
return (ListIterator<JsonValue>) elements.listIterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked") // elements is immutable
|
|
||||||
public @NotNull ListIterator<@Nullable JsonValue> listIterator(int index) {
|
|
||||||
return (ListIterator<JsonValue>) elements.listIterator(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull JsonArray subList(int fromIndex, int toIndex) {
|
|
||||||
return new JsonArray(elements.subList(fromIndex, toIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull JsonArray reversed() {
|
|
||||||
return new JsonArray(Util.trusted(elements.reversed()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
//<editor-fold desc="get...(int index)" defaultstate="collapsed">
|
|
||||||
/**
|
|
||||||
* {@return the element at the specified index in this array}
|
|
||||||
* @param index index of the element to return
|
|
||||||
* @throws ClassCastException if the element at the specified index is not an instance of {@link JsonString}
|
|
||||||
* @throws NullPointerException if the element at the specified index is {@code null}
|
|
||||||
* @throws IndexOutOfBoundsException if the index is less than zero or greater than or equals to the size of the array
|
|
||||||
*/
|
|
||||||
public @NotNull JsonString getString(int index) {
|
|
||||||
return get(index, JsonString.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the element at the specified index in this array}
|
|
||||||
* @param index index of the element to return
|
|
||||||
* @throws ClassCastException if the element at the specified index is not an instance of {@link JsonNumber}
|
|
||||||
* @throws NullPointerException if the element at the specified index is {@code null}
|
|
||||||
* @throws IndexOutOfBoundsException if the index is less than zero or greater than or equals to the size of the array
|
|
||||||
*/
|
|
||||||
public @NotNull JsonNumber getNumber(int index) {
|
|
||||||
return get(index, JsonNumber.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the element at the specified index in this array}
|
|
||||||
* @param index index of the element to return
|
|
||||||
* @throws ClassCastException if the element at the specified index is not an instance of {@link JsonBoolean}
|
|
||||||
* @throws NullPointerException if the element at the specified index is {@code null}
|
|
||||||
* @throws IndexOutOfBoundsException if the index is less than zero or greater than or equals to the size of the array
|
|
||||||
*/
|
|
||||||
public @NotNull JsonBoolean getBoolean(int index) {
|
|
||||||
return get(index, JsonBoolean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the element at the specified index in this array}
|
|
||||||
* @param index index of the element to return
|
|
||||||
* @throws ClassCastException if the element at the specified index is not an instance of {@link JsonArray}
|
|
||||||
* @throws NullPointerException if the element at the specified index is {@code null}
|
|
||||||
* @throws IndexOutOfBoundsException if the index is less than zero or greater than or equals to the size of the array
|
|
||||||
*/
|
|
||||||
public @NotNull JsonArray getArray(int index) {
|
|
||||||
return get(index, JsonArray.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the element at the specified index in this array}
|
|
||||||
* @param index index of the element to return
|
|
||||||
* @throws ClassCastException if the element at the specified index is not an instance of {@link JsonObject}
|
|
||||||
* @throws NullPointerException if the element at the specified index is {@code null}
|
|
||||||
* @throws IndexOutOfBoundsException if the index is less than zero or greater than or equals to the size of the array
|
|
||||||
*/
|
|
||||||
public @NotNull JsonObject getObject(int index) {
|
|
||||||
return get(index, JsonObject.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T extends JsonValue> @NotNull T get(int index, @NotNull Class<T> type) {
|
|
||||||
var value = elements().get(index);
|
|
||||||
if (value == null) {
|
|
||||||
throw new NullPointerException("Value at index" + index + " is null.");
|
|
||||||
} else {
|
|
||||||
return type.cast(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toString() {
|
|
||||||
var joiner = new StringJoiner(",", "[", "]");
|
|
||||||
for (JsonValue element : elements) {
|
|
||||||
joiner.add(JsonValue.toJsonString(element));
|
|
||||||
}
|
|
||||||
return joiner.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toPrettyJsonString(int depth) {
|
|
||||||
if (size() < 2) return toJsonString();
|
|
||||||
|
|
||||||
var entries = new ArrayList<String>(elements.size());
|
|
||||||
var length = 0L;
|
|
||||||
var composite = false;
|
|
||||||
for (var element : elements) {
|
|
||||||
var string = JsonValue.toPrettyJsonString(element, depth + Util.PRETTY_PRINT_INDENT.length());
|
|
||||||
entries.add(string);
|
|
||||||
|
|
||||||
composite |= element instanceof JsonObject || element instanceof JsonArray;
|
|
||||||
length += string.length() + 2; // +2 to account for ", " and "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!composite && depth + length <= Util.PRETTY_PRINT_ARRAY_MULTILINE_THRESHOLD) {
|
|
||||||
return "[" + String.join(", ", entries) + "]";
|
|
||||||
} else {
|
|
||||||
var out = new StringJoiner(",\n", "[\n", "\n]");
|
|
||||||
entries.forEach(e -> out.add(indent(e)));
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull String indent(@NotNull String string) {
|
|
||||||
if (string.isEmpty()) return "";
|
|
||||||
|
|
||||||
StringBuilder out = new StringBuilder();
|
|
||||||
|
|
||||||
Iterator<String> it = string.lines().iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
out.append(Util.PRETTY_PRINT_INDENT).append(it.next());
|
|
||||||
if (it.hasNext()) out.append('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,382 +0,0 @@
|
|||||||
package eu.jonahbauer.json;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
|
||||||
import org.jetbrains.annotations.Contract;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Java representation of a JSON object.
|
|
||||||
* @param entries the object's entries
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public record JsonObject(@NotNull Map<@NotNull String, @Nullable JsonValue> entries) implements JsonValue, Map<@NotNull String, @Nullable JsonValue> {
|
|
||||||
public static final @NotNull JsonObject EMPTY = new JsonObject(Map.of());
|
|
||||||
|
|
||||||
public JsonObject {
|
|
||||||
Objects.requireNonNull(entries, "entries");
|
|
||||||
entries = Util.defensiveCopy(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a JSON object containing one mapping.
|
|
||||||
* @param k1 the key
|
|
||||||
* @param v1 the value
|
|
||||||
* @return a new JSON object containing the specified mapping
|
|
||||||
*/
|
|
||||||
public static @NotNull JsonObject of(
|
|
||||||
@NotNull String k1, @Nullable JsonValue v1
|
|
||||||
) {
|
|
||||||
var map = new LinkedHashMap<String, JsonValue>();
|
|
||||||
map.put(Objects.requireNonNull(k1), v1);
|
|
||||||
return new JsonObject(Util.trusted(Collections.unmodifiableSequencedMap(map)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a JSON object containing two mapping.
|
|
||||||
* @param k1 the first key
|
|
||||||
* @param v1 the first value
|
|
||||||
* @param k2 the second key
|
|
||||||
* @param v2 the second value
|
|
||||||
* @return a new JSON object containing the specified mappings
|
|
||||||
*/
|
|
||||||
public static @NotNull JsonObject of(
|
|
||||||
@NotNull String k1, @Nullable JsonValue v1,
|
|
||||||
@NotNull String k2, @Nullable JsonValue v2
|
|
||||||
) {
|
|
||||||
var map = new LinkedHashMap<String, JsonValue>();
|
|
||||||
map.put(Objects.requireNonNull(k1), v1);
|
|
||||||
map.put(Objects.requireNonNull(k2), v2);
|
|
||||||
return new JsonObject(Util.trusted(Collections.unmodifiableSequencedMap(map)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a JSON object containing three mapping.
|
|
||||||
* @param k1 the first key
|
|
||||||
* @param v1 the first value
|
|
||||||
* @param k2 the second key
|
|
||||||
* @param v2 the second value
|
|
||||||
* @param k3 the third key
|
|
||||||
* @param v3 the third value
|
|
||||||
* @return a new JSON object containing the specified mappings
|
|
||||||
*/
|
|
||||||
public static @NotNull JsonObject of(
|
|
||||||
@NotNull String k1, @Nullable JsonValue v1,
|
|
||||||
@NotNull String k2, @Nullable JsonValue v2,
|
|
||||||
@NotNull String k3, @Nullable JsonValue v3
|
|
||||||
) {
|
|
||||||
var map = new LinkedHashMap<String, JsonValue>();
|
|
||||||
map.put(Objects.requireNonNull(k1), v1);
|
|
||||||
map.put(Objects.requireNonNull(k2), v2);
|
|
||||||
map.put(Objects.requireNonNull(k3), v3);
|
|
||||||
return new JsonObject(Util.trusted(Collections.unmodifiableSequencedMap(map)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a JSON object containing four mapping.
|
|
||||||
* @param k1 the first key
|
|
||||||
* @param v1 the first value
|
|
||||||
* @param k2 the second key
|
|
||||||
* @param v2 the second value
|
|
||||||
* @param k3 the third key
|
|
||||||
* @param v3 the third value
|
|
||||||
* @param k4 the fourth key
|
|
||||||
* @param v4 the fourth value
|
|
||||||
* @return a new JSON object containing the specified mappings
|
|
||||||
*/
|
|
||||||
public static @NotNull JsonObject of(
|
|
||||||
@NotNull String k1, @Nullable JsonValue v1,
|
|
||||||
@NotNull String k2, @Nullable JsonValue v2,
|
|
||||||
@NotNull String k3, @Nullable JsonValue v3,
|
|
||||||
@NotNull String k4, @Nullable JsonValue v4
|
|
||||||
) {
|
|
||||||
var map = new LinkedHashMap<String, JsonValue>();
|
|
||||||
map.put(Objects.requireNonNull(k1), v1);
|
|
||||||
map.put(Objects.requireNonNull(k2), v2);
|
|
||||||
map.put(Objects.requireNonNull(k3), v3);
|
|
||||||
map.put(Objects.requireNonNull(k4), v4);
|
|
||||||
return new JsonObject(Util.trusted(Collections.unmodifiableSequencedMap(map)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON object from the given map, using {@link JsonValue#valueOf(Object)} to convert the values to
|
|
||||||
* {@link JsonValue}s. The keys are expected to be instances of {@link CharSequence}.
|
|
||||||
* @param map a map
|
|
||||||
* @return a new JSON object
|
|
||||||
* @throws JsonConversionException if the given map or its entries cannot be converted to json
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonObject valueOf(@Nullable Map<?, ?> map) {
|
|
||||||
if (map == null) return null;
|
|
||||||
if (map instanceof JsonObject json) return json;
|
|
||||||
|
|
||||||
var out = new LinkedHashMap<String, JsonValue>();
|
|
||||||
map.forEach((key, value) -> out.put(keyOf(key), JsonValue.valueOf(value)));
|
|
||||||
return new JsonObject(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull String keyOf(@Nullable Object object) {
|
|
||||||
return switch (object) {
|
|
||||||
case CharSequence chars -> chars.toString();
|
|
||||||
case null -> throw new JsonConversionException("cannot use null as json key");
|
|
||||||
default -> throw new JsonConversionException("cannot convert object of type " + object.getClass() + " to json key");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//<editor-fold desc="Map" defaultstate="collapsed">
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return entries.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return entries.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsKey(Object key) {
|
|
||||||
Objects.requireNonNull(key, "key");
|
|
||||||
return entries.containsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsValue(Object value) {
|
|
||||||
return entries.containsValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue get(Object key) {
|
|
||||||
Objects.requireNonNull(key, "key");
|
|
||||||
return entries.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue put(@NotNull String key, @Nullable JsonValue value) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable JsonValue remove(Object key) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putAll(@NotNull Map<? extends @NotNull String, ? extends @Nullable JsonValue> m) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<@NotNull String> keySet() {
|
|
||||||
return entries.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Collection<@Nullable JsonValue> values() {
|
|
||||||
return entries.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<Entry<@NotNull String, @Nullable JsonValue>> entrySet() {
|
|
||||||
return entries.entrySet();
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
//<editor-fold desc="get...(String key)" defaultstate="collapsed">
|
|
||||||
/**
|
|
||||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
|
||||||
* @throws ClassCastException if the value is not an instance of {@link JsonString}
|
|
||||||
* @throws NullPointerException if there is no mapping for the key or the key is mapped to {@code null}
|
|
||||||
*/
|
|
||||||
public @NotNull JsonString getString(@NotNull String key) {
|
|
||||||
return get(key, JsonString.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
|
||||||
* @throws ClassCastException if the value is not an instance of {@link JsonNumber}
|
|
||||||
* @throws NullPointerException if there is no mapping for the key or the key is mapped to {@code null}
|
|
||||||
*/
|
|
||||||
public @NotNull JsonNumber getNumber(@NotNull String key) {
|
|
||||||
return get(key, JsonNumber.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
|
||||||
* @throws ClassCastException if the value is not an instance of {@link JsonBoolean}
|
|
||||||
* @throws NullPointerException if there is no mapping for the key or the key is mapped to {@code null}
|
|
||||||
*/
|
|
||||||
public @NotNull JsonBoolean getBoolean(@NotNull String key) {
|
|
||||||
return get(key, JsonBoolean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
|
||||||
* @throws ClassCastException if the value is not an instance of {@link JsonArray}
|
|
||||||
* @throws NullPointerException if there is no mapping for the key or the key is mapped to {@code null}
|
|
||||||
*/
|
|
||||||
public @NotNull JsonArray getArray(@NotNull String key) {
|
|
||||||
return get(key, JsonArray.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
|
||||||
* @throws ClassCastException if the value is not an instance of {@link JsonObject}
|
|
||||||
* @throws NullPointerException if there is no mapping for the key or the key is mapped to {@code null}
|
|
||||||
*/
|
|
||||||
public @NotNull JsonObject getObject(@NotNull String key) {
|
|
||||||
return get(key, JsonObject.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T extends JsonValue> @NotNull T get(@NotNull String key, @NotNull Class<T> type) {
|
|
||||||
var value = entries().get(key);
|
|
||||||
if (value == null) {
|
|
||||||
if (entries().containsKey(key)) {
|
|
||||||
throw new NullPointerException("Key " + key + " is mapped to null.");
|
|
||||||
} else {
|
|
||||||
throw new NullPointerException("No mapping for key " + key);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return type.cast(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
//<editor-fold desc="get...OrDefault(String key, ...)" defaultstate="collapsed">
|
|
||||||
// getStringOrDefault(String, JsonString) omitted because JsonString implements CharSequence
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonString#valueOf(CharSequence) JsonString.valueOf(defaultValue)} if this object contains no mapping for
|
|
||||||
* the key or the key is mapped to a value that is not an instance of {@link JsonString}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonString getStringOrDefault(@NotNull String key, @Nullable CharSequence defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonString.class, JsonString::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
|
||||||
* for the key or the key is mapped to a value that is not an instance of {@link JsonNumber}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable JsonNumber defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonNumber.class, Function.identity());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonNumber#valueOf(Number) JsonNumber.valueOf(defaultValue)} if this object contains no mapping for the
|
|
||||||
* key or the key is mapped to a value that is not an instance of {@link JsonNumber}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable Number defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonNumber.class, JsonNumber::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
|
||||||
* for the key or the key is mapped to a value that is not an instance of {@link JsonBoolean}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable JsonBoolean defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonBoolean.class, Function.identity());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonBoolean#valueOf(Boolean) JsonBoolean.valueOf(defaultValue)} if this object contains no mapping
|
|
||||||
* for the key or the key is mapped to a value that is not an instance of {@link JsonBoolean}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable Boolean defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonBoolean.class, JsonBoolean::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// getArrayOrDefault(String, JsonArray) omitted because JsonArray implements List<?>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonArray#valueOf(List) JsonArray.valueOf(defaultValue)} if this object contains no mapping for
|
|
||||||
* the key or the key is mapped to a value that is not an instance of {@link JsonArray}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable List<?> defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonArray#valueOf(Object...) JsonArray.valueOf(defaultValue)} if this object contains no mapping for the
|
|
||||||
* key or the key is mapped to a value that is not an instance of {@link JsonArray}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable Object @Nullable... defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// getObjectOrDefault(String, JsonObject) omitted because JsonObject implements Map<?, ?>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value to which the specified key is mapped, or
|
|
||||||
* {@link JsonObject#valueOf(Map) JsonObject.valueOf(defaultValue)} if this object contains no mapping for the key
|
|
||||||
* or the key is mapped to a value that is not an instance of {@link JsonObject}.
|
|
||||||
* @return the value to which the specified key is mapped falling back to the default value
|
|
||||||
*/
|
|
||||||
public @Nullable JsonObject getObjectOrDefault(@NotNull String key, @Nullable Map<?, ?> defaultValue) {
|
|
||||||
return getOrDefault(key, defaultValue, JsonObject.class, JsonObject::valueOf);
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T extends JsonValue, S> @Nullable T getOrDefault(@NotNull String key, @Nullable S defaultValue, @NotNull Class<T> type, @NotNull Function<S, T> converter) {
|
|
||||||
var value = entries().get(key);
|
|
||||||
if (type.isInstance(value)) {
|
|
||||||
return type.cast(value);
|
|
||||||
} else {
|
|
||||||
return converter.apply(defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toString() {
|
|
||||||
StringJoiner out = new StringJoiner(",", "{", "}");
|
|
||||||
entries.forEach((key, value) -> out.add(JsonString.quote(key) + ":" + JsonValue.toJsonString(value)));
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toPrettyJsonString(int depth) {
|
|
||||||
StringJoiner out = new StringJoiner(",\n" + Util.PRETTY_PRINT_INDENT, "{\n" + Util.PRETTY_PRINT_INDENT, "\n}");
|
|
||||||
entries.forEach((key, value) -> {
|
|
||||||
var keyString = JsonString.quote(key);
|
|
||||||
var valueIdent = depth + Util.PRETTY_PRINT_INDENT.length() + keyString.length() + 3; // +3 for ": " and ","
|
|
||||||
var valueString = JsonValue.toPrettyJsonString(value, valueIdent);
|
|
||||||
out.add(keyString + ": " + indent(valueString));
|
|
||||||
});
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull String indent(@NotNull String string) {
|
|
||||||
if (string.isEmpty()) return "";
|
|
||||||
|
|
||||||
StringBuilder out = new StringBuilder();
|
|
||||||
|
|
||||||
Iterator<String> it = string.lines().iterator();
|
|
||||||
out.append(it.next());
|
|
||||||
|
|
||||||
while (it.hasNext()) {
|
|
||||||
String next = it.next();
|
|
||||||
out.append('\n').append(Util.PRETTY_PRINT_INDENT).append(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package eu.jonahbauer.json;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
|
||||||
public final class JsonTemplateProcessor<T extends JsonValue> implements StringTemplate.Processor<T, RuntimeException> {
|
|
||||||
/**
|
|
||||||
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}.
|
|
||||||
*/
|
|
||||||
public static final @NotNull JsonTemplateProcessor<JsonValue> JSON = new JsonTemplateProcessor<>(JsonValue.class, true);
|
|
||||||
/**
|
|
||||||
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}. Throws a
|
|
||||||
* {@link JsonConversionException} if the result is not a JSON object.
|
|
||||||
*/
|
|
||||||
public static final @NotNull JsonTemplateProcessor<@NotNull JsonObject> JSON_OBJECT = new JsonTemplateProcessor<>(JsonObject.class, false);
|
|
||||||
/**
|
|
||||||
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}. Throws a
|
|
||||||
* {@link JsonConversionException} if the result is not a JSON array.
|
|
||||||
*/
|
|
||||||
public static final @NotNull JsonTemplateProcessor<@NotNull JsonArray> JSON_ARRAY = new JsonTemplateProcessor<>(JsonArray.class, false);
|
|
||||||
|
|
||||||
private final @NotNull Class<T> clazz;
|
|
||||||
private final boolean allowNull;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T process(@NotNull StringTemplate template) throws RuntimeException {
|
|
||||||
var result = JsonValue.parse(template);
|
|
||||||
if (result == null && !allowNull || result != null && !clazz.isInstance(result)) {
|
|
||||||
throw new JsonConversionException("The template does not represent a " + clazz.getSimpleName());
|
|
||||||
}
|
|
||||||
return clazz.cast(result);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,146 +0,0 @@
|
|||||||
package eu.jonahbauer.json;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonParserException;
|
|
||||||
import eu.jonahbauer.json.parser.JsonParser;
|
|
||||||
import org.jetbrains.annotations.Contract;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public sealed interface JsonValue extends Serializable permits JsonObject, JsonArray, JsonBoolean, JsonNumber, JsonString {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the JSON representation of this value}
|
|
||||||
*/
|
|
||||||
default @NotNull String toJsonString() {
|
|
||||||
return toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return a prettified JSON representation of this value}
|
|
||||||
*/
|
|
||||||
default @NotNull String toPrettyJsonString() {
|
|
||||||
return toPrettyJsonString(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return a prettified JSON representation of this value}
|
|
||||||
* @param depth indicates the depth at which this value appears (no formal definition given)
|
|
||||||
*/
|
|
||||||
default @NotNull String toPrettyJsonString(int depth) {
|
|
||||||
return toJsonString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the JSON representation of the given value}
|
|
||||||
* @param value a {@link JsonValue} (possibly {@code null})
|
|
||||||
*/
|
|
||||||
static @NotNull String toJsonString(@Nullable JsonValue value) {
|
|
||||||
return value == null ? "null" : value.toJsonString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the prettified JSON representation of the given value}
|
|
||||||
* @param value a {@link JsonValue} (possibly {@code null})
|
|
||||||
*/
|
|
||||||
static @NotNull String toPrettyJsonString(@Nullable JsonValue value) {
|
|
||||||
return toPrettyJsonString(value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the prettified JSON representation of the given value}
|
|
||||||
* @param value a {@link JsonValue} (possibly {@code null})
|
|
||||||
* @param depth indicates the depth at which the value appears (no formal definition given)
|
|
||||||
*/
|
|
||||||
static @NotNull String toPrettyJsonString(@Nullable JsonValue value, int depth) {
|
|
||||||
return value == null ? "null" : value.toPrettyJsonString(depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a string as a JSON. The string must adhere to the <a href="https://www.json.org">JSON grammar</a>.
|
|
||||||
* @param json a string
|
|
||||||
* @return the result of parsing the string as a JSON
|
|
||||||
* @throws JsonParserException if the string cannot be parsed as JSON
|
|
||||||
*/
|
|
||||||
static @Nullable JsonValue parse(@NotNull String json) {
|
|
||||||
var parser = new JsonParser(json);
|
|
||||||
return parser.parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a string template as a JSON. The string template must adhere to the
|
|
||||||
* <a href="https://www.json.org">standard JSON grammar</a> with the following additional rules:
|
|
||||||
* <pre>{@code
|
|
||||||
* value
|
|
||||||
* placeholder
|
|
||||||
*
|
|
||||||
* member
|
|
||||||
* ws placeholder ws ':' element
|
|
||||||
*
|
|
||||||
* characters
|
|
||||||
* placeholder characters
|
|
||||||
* }</pre>
|
|
||||||
* where <code>placeholder</code> represents an object embedded in the string template.
|
|
||||||
* <p>
|
|
||||||
* A placeholder will be converted to JSON depending on the context it appears in.
|
|
||||||
* When used as a value, {@link JsonValue#valueOf(Object)} will be used.
|
|
||||||
* When used as an object key, only instances or {@link CharSequence} are allowed, which will be converted using
|
|
||||||
* {@link CharSequence#toString()}.
|
|
||||||
* When used in a string, {@link Objects#toString(Object)} will be used.
|
|
||||||
*
|
|
||||||
* @param json a string template
|
|
||||||
* @return the result of parsing the string template as a JSON
|
|
||||||
* @throws JsonParserException if the string template cannot be parsed as JSON
|
|
||||||
* @throws JsonConversionException if a placeholder cannot be converted to JSON
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
static @Nullable JsonValue parse(@NotNull StringTemplate json) {
|
|
||||||
var parser = new JsonParser(json);
|
|
||||||
return parser.parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an object to a JSON value:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link JsonValue}s and {@code null} are returned as is</li>
|
|
||||||
* <li>{@link Boolean}s are converted with {@link JsonBoolean#valueOf(Boolean)}</li>
|
|
||||||
* <li>{@link Number}s are converted with {@link JsonNumber#valueOf(Number)}</li>
|
|
||||||
* <li>{@link Character}s are converted with {@link JsonString#valueOf(Character)}</li>
|
|
||||||
* <li>{@link CharSequence}s are converted with {@link JsonString#valueOf(CharSequence)}</li>
|
|
||||||
* <li>{@link List}s are converted with {@link JsonArray#valueOf(List)}</li>
|
|
||||||
* <li>arrays are converted with the respective overload of {@link JsonArray#valueOf(Object...)}</li>
|
|
||||||
* <li>{@link Map}s are converted with {@link JsonObject#valueOf(Map)}</li>
|
|
||||||
* </ul>
|
|
||||||
* @param object an object
|
|
||||||
* @return a {@link JsonValue} representing the object
|
|
||||||
* @throws JsonConversionException if the object cannot be converted to a {@link JsonValue}
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
static @Nullable JsonValue valueOf(@Nullable Object object) {
|
|
||||||
return switch (object) {
|
|
||||||
case JsonValue json -> json;
|
|
||||||
case Boolean bool -> JsonBoolean.valueOf(bool);
|
|
||||||
case Number number -> JsonNumber.valueOf(number);
|
|
||||||
case Character chr -> JsonString.valueOf(String.valueOf(chr));
|
|
||||||
case CharSequence chars -> JsonString.valueOf(chars);
|
|
||||||
case List<?> list -> JsonArray.valueOf(list);
|
|
||||||
case Object[] array -> JsonArray.valueOf(array);
|
|
||||||
case boolean[] array -> JsonArray.valueOf(array);
|
|
||||||
case byte[] array -> JsonArray.valueOf(array);
|
|
||||||
case short[] array -> JsonArray.valueOf(array);
|
|
||||||
case char[] array -> JsonArray.valueOf(array);
|
|
||||||
case int[] array -> JsonArray.valueOf(array);
|
|
||||||
case long[] array -> JsonArray.valueOf(array);
|
|
||||||
case float[] array -> JsonArray.valueOf(array);
|
|
||||||
case double[] array -> JsonArray.valueOf(array);
|
|
||||||
case Map<?, ?> map -> JsonObject.valueOf(map);
|
|
||||||
case null -> null;
|
|
||||||
default -> throw new JsonConversionException("cannot convert object of type " + object.getClass() + " to json value");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package eu.jonahbauer.json;
|
|
||||||
|
|
||||||
import lombok.experimental.Delegate;
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
class Util {
|
|
||||||
static final @NotNull String PRETTY_PRINT_INDENT = " ";
|
|
||||||
static final int PRETTY_PRINT_ARRAY_MULTILINE_THRESHOLD = 80;
|
|
||||||
|
|
||||||
private static final @NotNull Class<?> LIST_1_2 = List.of(1).getClass();
|
|
||||||
private static final @NotNull Class<?> LIST_N = List.of(1, 2, 3).getClass();
|
|
||||||
private static final @NotNull Class<?> SUB_LIST = List.of(1, 2, 3).subList(0, 2).getClass();
|
|
||||||
private static final @NotNull Class<?> MAP_1 = Map.of(1, 2).getClass();
|
|
||||||
private static final @NotNull Class<?> MAP_N = Map.of(1, 2, 3, 4).getClass();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a defensive copy of the given list unless it is known to be an immutable list.
|
|
||||||
*/
|
|
||||||
public static <T> @NotNull List<T> defensiveCopy(@NotNull List<T> list) {
|
|
||||||
if (LIST_1_2.isInstance(list) || LIST_N.isInstance(list) || SUB_LIST.isInstance(list)) {
|
|
||||||
return list;
|
|
||||||
} else if (list instanceof TrustedList<T>(var delegate)) {
|
|
||||||
return delegate;
|
|
||||||
} else {
|
|
||||||
return list.stream().toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a defensive copy of the given map unless it is known to be an immutable map.
|
|
||||||
* If the given map implements {@link SequencedMap} as will the returned one.
|
|
||||||
*/
|
|
||||||
public static <K, V> @NotNull Map<K, V> defensiveCopy(@NotNull Map<@NotNull K, V> map) {
|
|
||||||
if (MAP_1.isInstance(map) || MAP_N.isInstance(map)) {
|
|
||||||
return map;
|
|
||||||
} else if (map instanceof TrustedMap<K, V>(var delegate)) {
|
|
||||||
return delegate;
|
|
||||||
} else if (map instanceof SequencedMap<K,V> sequenced) {
|
|
||||||
var out = new LinkedHashMap<K, V>();
|
|
||||||
sequenced.forEach((key, value) -> out.put(Objects.requireNonNull(key, "key"), value));
|
|
||||||
return Collections.unmodifiableSequencedMap(out);
|
|
||||||
} else {
|
|
||||||
var out = new HashMap<K, V>();
|
|
||||||
map.forEach((key, value) -> out.put(Objects.requireNonNull(key, "key"), value));
|
|
||||||
return Collections.unmodifiableMap(out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list that will be assumed immutable when given to {@link #defensiveCopy(List)}.
|
|
||||||
* {@return a list that will be assumed immutable}
|
|
||||||
*/
|
|
||||||
public static <T> @NotNull List<T> trusted(@NotNull List<T> list) {
|
|
||||||
return new TrustedList<>(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a map that will be assumed immutable when given to {@link #defensiveCopy(Map)}.
|
|
||||||
* {@return a map that will be assumed immutable}
|
|
||||||
*/
|
|
||||||
public static <K, V> @NotNull Map<K, V> trusted(@NotNull Map<@NotNull K, V> map) {
|
|
||||||
return new TrustedMap<>(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record TrustedList<T>(@Delegate @NotNull List<T> delegate) implements List<T> {}
|
|
||||||
|
|
||||||
private record TrustedMap<K, V>(@Delegate @NotNull Map<K, V> delegate) implements Map<K, V> {}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package eu.jonahbauer.json.exceptions;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class JsonParserException extends JsonReaderException {
|
|
||||||
|
|
||||||
public JsonParserException(int fragment, int line, int column, @NotNull String message) {
|
|
||||||
super(fragment, line, column, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonParserException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
|
||||||
super(fragment, line, column, message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package eu.jonahbauer.json.exceptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parent class for exceptions related to json processing.
|
|
||||||
*/
|
|
||||||
public abstract class JsonProcessingException extends RuntimeException {
|
|
||||||
public JsonProcessingException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonProcessingException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package eu.jonahbauer.json.exceptions;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
abstract class JsonReaderException extends JsonProcessingException {
|
|
||||||
private final int fragment;
|
|
||||||
private final int lineNumber;
|
|
||||||
private final int columnNumber;
|
|
||||||
|
|
||||||
public JsonReaderException(int fragment, int line, int column, @NotNull String message) {
|
|
||||||
super(message);
|
|
||||||
this.fragment = fragment;
|
|
||||||
this.lineNumber = line;
|
|
||||||
this.columnNumber = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonReaderException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
this.fragment = fragment;
|
|
||||||
this.lineNumber = line;
|
|
||||||
this.columnNumber = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String getMessage() {
|
|
||||||
if (getFragment() == 0) {
|
|
||||||
return super.getMessage() + " at line " + getLineNumber() + ", column " + getColumnNumber();
|
|
||||||
} else {
|
|
||||||
return super.getMessage() + " at fragment " + getFragment() + ", line " + getLineNumber() + ", column " + getColumnNumber();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,273 +0,0 @@
|
|||||||
package eu.jonahbauer.json.parser;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.*;
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonParserException;
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
|
||||||
import eu.jonahbauer.json.tokenizer.JsonTokenizer;
|
|
||||||
import eu.jonahbauer.json.tokenizer.JsonTokenizerImpl;
|
|
||||||
import eu.jonahbauer.json.tokenizer.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 static final String TT_JSON_STRING = "JSON_STRING";
|
|
||||||
private static final String TT_JSON_NUMBER = "JSON_NUMBER";
|
|
||||||
private static final String TT_JSON_STRING_TEMPLATE = "JSON_STRING_TEMPLATE";
|
|
||||||
private static final String TT_JSON_PLACEHOLDER = "JSON_PLACEHOLDER";
|
|
||||||
|
|
||||||
private final @NotNull JsonTokenizer tokenizer;
|
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
public JsonParser(@NotNull StringTemplate json) {
|
|
||||||
this(new JsonTokenizerImpl(json));
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonParser(@NotNull String json) {
|
|
||||||
this(new JsonTokenizerImpl(json));
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonParser(@NotNull Reader reader) {
|
|
||||||
this(new JsonTokenizerImpl(reader));
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonParser(@NotNull JsonTokenizer tokenizer) {
|
|
||||||
this.tokenizer = Objects.requireNonNull(tokenizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the input as a JSON value.
|
|
||||||
* @return the JSON value
|
|
||||||
* @throws JsonParserException if the input could not be parsed as JSON
|
|
||||||
* @throws UncheckedIOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
public @Nullable JsonValue parse() throws JsonParserException {
|
|
||||||
try {
|
|
||||||
var out = parse0();
|
|
||||||
if (tokenizer.next() != null) {
|
|
||||||
throw new JsonParserException(tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected trailing entries");
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new UncheckedIOException(ex);
|
|
||||||
} catch (JsonTokenizerException ex) {
|
|
||||||
throw new JsonParserException(ex.getFragment(), ex.getLineNumber(), ex.getColumnNumber(), ex.getMessage(), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable JsonValue parse0() throws IOException {
|
|
||||||
// Use a stack of Context objects to store the objects and arrays which are currently being parsed.
|
|
||||||
// This avoids the risk of stack overflow which would exist when having one nested calls to parse0.
|
|
||||||
var stack = new ArrayDeque<@NotNull Context>();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
JsonValue value;
|
|
||||||
|
|
||||||
var token = tokenizer.next();
|
|
||||||
if (!stack.isEmpty() && token == stack.peek().end()) {
|
|
||||||
value = stack.pop().build();
|
|
||||||
} else {
|
|
||||||
// consume value separator
|
|
||||||
if (stack.peek() instanceof Context context && !context.isEmpty()) {
|
|
||||||
if (token != JsonPunctuation.VALUE_SEPARATOR) {
|
|
||||||
throw unexpectedToken(token, context.end(), JsonPunctuation.VALUE_SEPARATOR);
|
|
||||||
}
|
|
||||||
token = tokenizer.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// read object key and consume name separator
|
|
||||||
if (stack.peek() instanceof ObjectContext object) {
|
|
||||||
var key = parseJsonObjectKey(object, token);
|
|
||||||
if ((token = tokenizer.next()) != JsonPunctuation.NAME_SEPARATOR) {
|
|
||||||
throw unexpectedToken(token, JsonPunctuation.NAME_SEPARATOR);
|
|
||||||
}
|
|
||||||
token = tokenizer.next();
|
|
||||||
object.setKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token == JsonPunctuation.BEGIN_OBJECT) {
|
|
||||||
stack.push(new ObjectContext());
|
|
||||||
continue;
|
|
||||||
} else if (token == JsonPunctuation.BEGIN_ARRAY) {
|
|
||||||
stack.push(new ArrayContext());
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// read json value
|
|
||||||
value = switch (token) {
|
|
||||||
case JsonPunctuation.BEGIN_ARRAY, JsonPunctuation.BEGIN_OBJECT -> 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 (stack.peek() instanceof Context context) {
|
|
||||||
context.add(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull String parseJsonObjectKey(@NotNull 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 use null as json object key");
|
|
||||||
default -> throw new JsonConversionException("cannot convert object of type " + object.getClass() + " to json object key");
|
|
||||||
};
|
|
||||||
case null -> throw unexpectedEndOfFile();
|
|
||||||
default -> throw unexpectedStartOfKey(context, token);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedStartOfKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
|
|
||||||
var expected = new ArrayList<>(4);
|
|
||||||
expected.add(TT_JSON_STRING);
|
|
||||||
|
|
||||||
if (context.isEmpty()) {
|
|
||||||
// if the context is not empty, we have already consumed a VALUE_SEPARATOR(,) and standard JSON does not
|
|
||||||
// allow trailing commas
|
|
||||||
expected.add(JsonPunctuation.END_OBJECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenizer.getFragment() != 0) {
|
|
||||||
// when the input is a StringTemplate, additional tokens are allowed
|
|
||||||
expected.add(TT_JSON_STRING_TEMPLATE);
|
|
||||||
expected.add(TT_JSON_PLACEHOLDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unexpectedToken(token, expected.toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedStartOfValue(@Nullable JsonToken token) {
|
|
||||||
var expected = new ArrayList<>(9);
|
|
||||||
expected.addAll(Arrays.asList(
|
|
||||||
JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY, JsonBoolean.TRUE, JsonBoolean.FALSE,
|
|
||||||
JsonNull.NULL, TT_JSON_STRING, TT_JSON_NUMBER
|
|
||||||
));
|
|
||||||
if (tokenizer.getFragment() != 0) {
|
|
||||||
expected.add(TT_JSON_STRING_TEMPLATE);
|
|
||||||
expected.add(TT_JSON_PLACEHOLDER);
|
|
||||||
}
|
|
||||||
throw unexpectedToken(token, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedEndOfFile() {
|
|
||||||
return new JsonParserException(tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected end of file");
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedToken(@Nullable JsonToken token, @NotNull Object @NotNull... expected) {
|
|
||||||
if (expected.length == 1) {
|
|
||||||
throw new JsonParserException(
|
|
||||||
tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
|
||||||
"unexpected token: " + token + " (expected " + expected[0] + ")"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new JsonParserException(
|
|
||||||
tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
|
||||||
"unexpected token: " + token + " (expected one of " + Arrays.toString(expected) + ")"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an JSON object or array that is currently being parsed.
|
|
||||||
*/
|
|
||||||
private sealed interface Context {
|
|
||||||
boolean isEmpty();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a nested JSON value to this context
|
|
||||||
* @param value the value
|
|
||||||
*/
|
|
||||||
void add(@Nullable JsonValue value);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds and returns the JSON value represented by this context.
|
|
||||||
* @return a JSON value
|
|
||||||
*/
|
|
||||||
@NotNull JsonValue build();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the token that indicates the end of this context}
|
|
||||||
*/
|
|
||||||
@NotNull JsonPunctuation end();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a JSON object that is currently being parsed.
|
|
||||||
*/
|
|
||||||
private static final class ObjectContext implements Context {
|
|
||||||
private final @NotNull SequencedMap<@NotNull String, @Nullable JsonValue> map = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
private @Nullable String key;
|
|
||||||
|
|
||||||
public void setKey(@Nullable 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 JsonValue build() {
|
|
||||||
if (key != null) throw new IllegalStateException();
|
|
||||||
return new JsonObject(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull JsonPunctuation end() {
|
|
||||||
return JsonPunctuation.END_OBJECT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a JSON array that is currently being parsed.
|
|
||||||
*/
|
|
||||||
private static final class ArrayContext implements Context {
|
|
||||||
private final @NotNull List<@Nullable 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Range;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
public interface JsonTokenizer extends Iterable<JsonToken> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the next token or <code>null</code> if the end of the input has been reached}
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Nullable JsonToken next() throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the fragment at which the previously read token appeared}
|
|
||||||
*/
|
|
||||||
@Range(from = 0, to = Integer.MAX_VALUE) int getFragment();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the line at which the previously read token appeared}
|
|
||||||
*/
|
|
||||||
@Range(from = 1, to = Integer.MAX_VALUE) int getLineNumber();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the column at which the previously read token appeared}
|
|
||||||
*/
|
|
||||||
@Range(from = 1, to = Integer.MAX_VALUE) int getColumnNumber();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return an iterator over the tokens} The {@link Iterator#next()} and {@link Iterator#hasNext()} methods
|
|
||||||
* may throw an {@link UncheckedIOException} if an I/O error occurs.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
default @NotNull Iterator<@NotNull JsonToken> iterator() {
|
|
||||||
class JsonTokenizerIterator implements Iterator<@NotNull JsonToken> {
|
|
||||||
private @Nullable JsonToken next;
|
|
||||||
private boolean isValid = false;
|
|
||||||
|
|
||||||
private void ensureValid() {
|
|
||||||
if (isValid) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
next = JsonTokenizer.this.next();
|
|
||||||
isValid = true;
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new UncheckedIOException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNext() {
|
|
||||||
ensureValid();
|
|
||||||
return next != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull JsonToken next() {
|
|
||||||
ensureValid();
|
|
||||||
if (next == null) {
|
|
||||||
throw new NoSuchElementException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = next;
|
|
||||||
next = null;
|
|
||||||
isValid = false;
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonTokenizerIterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return a stream of tokens} When an I/O error occurs, the corresponding {@link IOException} is wrapped in an
|
|
||||||
* {@link UncheckedIOException} exception.
|
|
||||||
*/
|
|
||||||
default @NotNull Stream<@NotNull JsonToken> stream() {
|
|
||||||
return StreamSupport.stream(this.spliterator(), false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.reader;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.jetbrains.annotations.Range;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public interface PushbackReader extends AutoCloseable {
|
|
||||||
int EOF = -1;
|
|
||||||
int PLACEHOLDER = -2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the next character.
|
|
||||||
* @return the next character or {@link #EOF} if the end of the stream has been reached
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Range(from = -2, to = Character.MAX_VALUE) int read() throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pushes the reader back, making {@link #read()} return the same character as before.
|
|
||||||
* @throws IllegalStateException when called before the first read or more than once after each read
|
|
||||||
*/
|
|
||||||
void pushback();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the index of the fragment the current character belongs to} Returns 0 unless a {@code StringTemplate} is
|
|
||||||
* being read.
|
|
||||||
* @throws IllegalStateException when called before the first read
|
|
||||||
*/
|
|
||||||
@Range(from = 0, to = Integer.MAX_VALUE) int getFragment();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the line number of the current character}
|
|
||||||
* @throws IllegalStateException when called before the first read
|
|
||||||
*/
|
|
||||||
@Range(from = 1, to = Integer.MAX_VALUE) int getLineNumber();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the column number of the current character}
|
|
||||||
* @throws IllegalStateException when called before the first read
|
|
||||||
*/
|
|
||||||
@Range(from = 1, to = Integer.MAX_VALUE) int getColumnNumber();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the object that has been read after {@link #read()} returned {@link #PLACEHOLDER}.
|
|
||||||
* @throws IllegalStateException the previous call to read did not return {@link #PLACEHOLDER}
|
|
||||||
* @return the object that has been read
|
|
||||||
*/
|
|
||||||
@Nullable Object getObject();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void close() throws IOException;
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.reader;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class PushbackReaderImpl implements PushbackReader {
|
|
||||||
private @Nullable Input input;
|
|
||||||
private @Nullable Reader reader;
|
|
||||||
|
|
||||||
private int current = Integer.MIN_VALUE;
|
|
||||||
private Object object;
|
|
||||||
|
|
||||||
private int fragment;
|
|
||||||
private int lineNumber;
|
|
||||||
private int columnNumber;
|
|
||||||
|
|
||||||
private boolean pushback;
|
|
||||||
|
|
||||||
public PushbackReaderImpl(@NotNull Reader reader) {
|
|
||||||
this.input = new Input(List.of(reader).iterator(), Collections.emptyIterator());
|
|
||||||
this.fragment = -1; // fragment will be incremented to 0 when the first character is read
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
public PushbackReaderImpl(@NotNull StringTemplate template) {
|
|
||||||
this.input = new Input(
|
|
||||||
template.fragments().stream().<Reader>map(StringReader::new).iterator(),
|
|
||||||
template.values().iterator()
|
|
||||||
);
|
|
||||||
this.fragment = 0; // fragment will be incremented to 1 when the first character is read
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getFragment() {
|
|
||||||
if (current == Integer.MIN_VALUE) {
|
|
||||||
throw new IllegalStateException("No character has been read so far.");
|
|
||||||
}
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getLineNumber() {
|
|
||||||
if (current == Integer.MIN_VALUE) {
|
|
||||||
throw new IllegalStateException("No character has been read so far.");
|
|
||||||
}
|
|
||||||
return lineNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getColumnNumber() {
|
|
||||||
if (current == Integer.MIN_VALUE) {
|
|
||||||
throw new IllegalStateException("No character has been read so far.");
|
|
||||||
}
|
|
||||||
return columnNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pushback() {
|
|
||||||
if (current == Integer.MIN_VALUE) {
|
|
||||||
throw new IllegalStateException("No character has been read so far.");
|
|
||||||
} else if (pushback) {
|
|
||||||
throw new IllegalStateException("Cannot push back more than one character at a time.");
|
|
||||||
}
|
|
||||||
pushback = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (pushback) {
|
|
||||||
pushback = false;
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
var input = this.input;
|
|
||||||
if (input == null) {
|
|
||||||
throw new IOException("closed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var reader = this.reader;
|
|
||||||
if (reader == null && input.readers().hasNext()) {
|
|
||||||
this.reader = reader = input.readers().next();
|
|
||||||
fragment++;
|
|
||||||
lineNumber = 1;
|
|
||||||
columnNumber = 0;
|
|
||||||
} else if (reader == null) {
|
|
||||||
return EOF;
|
|
||||||
}
|
|
||||||
|
|
||||||
int result = reader.read();
|
|
||||||
|
|
||||||
if (current == '\n' && result != '\n' || current == '\r' && result != '\n' && result != '\r') {
|
|
||||||
lineNumber++;
|
|
||||||
columnNumber = 1;
|
|
||||||
} else {
|
|
||||||
columnNumber++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == EOF) {
|
|
||||||
reader.close();
|
|
||||||
this.reader = null;
|
|
||||||
|
|
||||||
if (input.values().hasNext()) {
|
|
||||||
this.object = input.values().next();
|
|
||||||
return this.current = PLACEHOLDER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.object = null;
|
|
||||||
return this.current = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable Object getObject() {
|
|
||||||
if (current != PLACEHOLDER) {
|
|
||||||
throw new IllegalStateException("Currently not at a placeholder.");
|
|
||||||
}
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
var input = this.input;
|
|
||||||
var reader = this.reader;
|
|
||||||
this.input = null;
|
|
||||||
this.reader = null;
|
|
||||||
close(Arrays.asList(input, reader).iterator());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void close(@NotNull Iterator<? extends @Nullable AutoCloseable> closeables) throws ClosingException {
|
|
||||||
ClosingException exception = null;
|
|
||||||
while (closeables.hasNext()) {
|
|
||||||
var closeable = closeables.next();
|
|
||||||
if (closeable == null) continue;
|
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
if (exception == null) exception = new ClosingException();
|
|
||||||
exception.addSuppressed(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exception != null) throw exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
private record Input(@NotNull Iterator<@NotNull Reader> readers, @NotNull Iterator<Object> values) implements AutoCloseable {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws ClosingException {
|
|
||||||
PushbackReaderImpl.close(readers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class ClosingException extends IOException { }
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.token;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The JSON token {@code null}.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("java:S6548")
|
|
||||||
public enum JsonNull implements JsonToken {
|
|
||||||
NULL;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toString() {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.token;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public record JsonPlaceholder(@Nullable Object object) implements JsonToken {
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.token;
|
|
||||||
|
|
||||||
import eu.jonahbauer.json.JsonBoolean;
|
|
||||||
import eu.jonahbauer.json.JsonNumber;
|
|
||||||
import eu.jonahbauer.json.JsonString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a JSON token.
|
|
||||||
*/
|
|
||||||
public sealed interface JsonToken permits JsonBoolean, JsonNumber, JsonString, JsonNull, JsonPlaceholder, JsonPunctuation, JsonStringTemplate { }
|
|
@ -1 +0,0 @@
|
|||||||
["é"]
|
|
@ -1 +0,0 @@
|
|||||||
["日ш<E697A5>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22><><EFBFBD><EFBFBD>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22><>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]
|
|
@ -1 +0,0 @@
|
|||||||
["<22><>"]
|
|
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1 +0,0 @@
|
|||||||
[a<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
[<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
[123<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
[1e1<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
[0<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
[1e<EFBFBD>]
|
|
@ -1 +0,0 @@
|
|||||||
{"<22>":"0",}
|
|
@ -1 +0,0 @@
|
|||||||
["\u<>"]
|
|
@ -1 +0,0 @@
|
|||||||
["\<5C>"]
|
|
@ -1 +0,0 @@
|
|||||||
<EFBFBD>{}
|
|
@ -1 +0,0 @@
|
|||||||
<EFBFBD>
|
|
@ -1 +0,0 @@
|
|||||||
<EFBFBD>
|
|
@ -1,11 +1,14 @@
|
|||||||
[versions]
|
[versions]
|
||||||
annotations = "24.1.0"
|
annotations = "24.1.0"
|
||||||
junit = "5.12.1"
|
junit = "5.10.1"
|
||||||
junit-launcher = "1.12.1"
|
lombok = "1.18.30"
|
||||||
lombok = "1.18.32"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-launcher" }
|
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||||
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
|
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||||
|
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
|
||||||
|
|
||||||
|
[bundles]
|
||||||
|
junit = ["junit-jupiter", "junit-jupiter-api", "junit-jupiter-params"]
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
|
/*
|
||||||
@Suppress("UnstableApiUsage")
|
* This file was generated by the Gradle 'init' task.
|
||||||
dependencyResolutionManagement {
|
*/
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "json"
|
rootProject.name = "json"
|
||||||
include("core")
|
|
||||||
|
274
src/main/java/eu/jonahbauer/json/JsonArray.java
Normal file
274
src/main/java/eu/jonahbauer/json/JsonArray.java
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java representation of a JSON array.
|
||||||
|
* @param elements the array's elements
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public record JsonArray(
|
||||||
|
@NotNull List<@Nullable JsonValue> elements
|
||||||
|
) implements JsonValue, List<@Nullable JsonValue> {
|
||||||
|
|
||||||
|
public JsonArray {
|
||||||
|
Objects.requireNonNull(elements, "elements");
|
||||||
|
if (!Util.isImmutable(elements)) {
|
||||||
|
elements = elements.stream().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link JsonArray} from the given list, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
||||||
|
* {@link JsonValue}s.
|
||||||
|
* @param list a list of objects
|
||||||
|
* @return a {@link JsonArray}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
public static @Nullable JsonArray valueOf(@Nullable List<?> list) {
|
||||||
|
if (list == null) return null;
|
||||||
|
if (list instanceof JsonArray json) return json;
|
||||||
|
return new JsonArray(list.stream().map(JsonValue::valueOf).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link JsonArray} from the given array, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
||||||
|
* {@link JsonValue}s.
|
||||||
|
* @param array an array of objects
|
||||||
|
* @return a {@link JsonArray}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
public static @Nullable JsonArray valueOf(@Nullable Object @Nullable... array) {
|
||||||
|
return array == null ? null : new JsonArray(Arrays.stream(array).map(JsonValue::valueOf).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link JsonArray} from the given array, using {@link JsonNumber#valueOf(int)} to convert the
|
||||||
|
* {@code int}s to {@link JsonNumber}s.
|
||||||
|
* @param array an array of {@code int}s
|
||||||
|
* @return a {@link JsonArray}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
public static @Nullable JsonArray valueOf(int @Nullable... array) {
|
||||||
|
return array == null ? null : new JsonArray(Arrays.stream(array).<JsonValue>mapToObj(JsonNumber::valueOf).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link JsonArray} from the given array, using {@link JsonNumber#valueOf(double)} to convert the
|
||||||
|
* {@code double}s to {@link JsonNumber}s.
|
||||||
|
* @param array an array of {@code double}s
|
||||||
|
* @return a {@link JsonArray}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
public static @Nullable JsonArray valueOf(double @Nullable... array) {
|
||||||
|
return array == null ? null : new JsonArray(Arrays.stream(array).<JsonValue>mapToObj(JsonNumber::valueOf).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold desc="List" defaultstate="collapsed">
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return elements.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return elements.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Object o) {
|
||||||
|
return elements.contains(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Iterator<@Nullable JsonValue> iterator() {
|
||||||
|
return elements.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object @NotNull[] toArray() {
|
||||||
|
return elements.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T @NotNull[] toArray(T @NotNull[] a) {
|
||||||
|
return elements.toArray(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(@Nullable JsonValue value) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean remove(Object o) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("SlowListContainsAll")
|
||||||
|
public boolean containsAll(@NotNull Collection<?> c) {
|
||||||
|
return elements.containsAll(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addAll(@NotNull Collection<? extends @Nullable JsonValue> c) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addAll(int index, @NotNull Collection<? extends @Nullable JsonValue> c) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAll(@NotNull Collection<?> c) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean retainAll(@NotNull Collection<?> c) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue get(int index) {
|
||||||
|
return elements.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue set(int index, @Nullable JsonValue element) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(int index, @Nullable JsonValue element) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue remove(int index) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int indexOf(Object o) {
|
||||||
|
return elements.indexOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int lastIndexOf(Object o) {
|
||||||
|
return elements.lastIndexOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull ListIterator<@Nullable JsonValue> listIterator() {
|
||||||
|
return elements.listIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull ListIterator<@Nullable JsonValue> listIterator(int index) {
|
||||||
|
return elements.listIterator(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<@Nullable JsonValue> subList(int fromIndex, int toIndex) {
|
||||||
|
return elements.subList(fromIndex, toIndex);
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the element at the specified position in this array}
|
||||||
|
* @param index index of the element to return
|
||||||
|
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonString}.
|
||||||
|
*/
|
||||||
|
public @Nullable JsonString getString(int index) {
|
||||||
|
return get(index, JsonString.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the element at the specified position in this array}
|
||||||
|
* @param index index of the element to return
|
||||||
|
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonNumber}.
|
||||||
|
*/
|
||||||
|
public @Nullable JsonNumber getNumber(int index) {
|
||||||
|
return get(index, JsonNumber.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the element at the specified position in this array}
|
||||||
|
* @param index index of the element to return
|
||||||
|
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonBoolean}.
|
||||||
|
*/
|
||||||
|
public @Nullable JsonBoolean getBoolean(int index) {
|
||||||
|
return get(index, JsonBoolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the element at the specified position in this array}
|
||||||
|
* @param index index of the element to return
|
||||||
|
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonArray}.
|
||||||
|
*/
|
||||||
|
public @Nullable JsonArray getArray(int index) {
|
||||||
|
return get(index, JsonArray.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the element at the specified position in this array}
|
||||||
|
* @param index index of the element to return
|
||||||
|
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonObject}.
|
||||||
|
*/
|
||||||
|
public @Nullable JsonObject getObject(int index) {
|
||||||
|
return get(index, JsonObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends JsonValue> @Nullable T get(int index, @NotNull Class<T> type) {
|
||||||
|
return type.cast(elements().get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return elements.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toJsonString() {
|
||||||
|
var joiner = new StringJoiner(", ", "[", "]");
|
||||||
|
for (JsonValue element : elements) {
|
||||||
|
joiner.add(JsonValue.toJsonString(element));
|
||||||
|
}
|
||||||
|
return joiner.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toPrettyJsonString() {
|
||||||
|
if (size() < 2) return toJsonString();
|
||||||
|
|
||||||
|
var out = new StringJoiner(",\n", "[\n", "\n]");
|
||||||
|
elements().forEach(e -> out.add(indent(JsonValue.toPrettyJsonString(e))));
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String indent(@NotNull String string) {
|
||||||
|
if (string.isEmpty()) return "";
|
||||||
|
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
|
||||||
|
Iterator<String> it = string.lines().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
out.append(" ").append(it.next());
|
||||||
|
if (it.hasNext()) out.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package eu.jonahbauer.json;
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
import eu.jonahbauer.json.token.JsonToken;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@ -14,24 +15,24 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
@Getter
|
@Getter
|
||||||
@Accessors(fluent = true)
|
@Accessors(fluent = true)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum JsonBoolean implements JsonValue, JsonToken {
|
public enum JsonBoolean implements JsonValue, JsonToken, JsonTokenKind {
|
||||||
TRUE(true),
|
TRUE(true),
|
||||||
FALSE(false),
|
FALSE(false),
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given boolean to a JSON boolean.
|
* Creates a {@link JsonBoolean} wrapping the given {@code boolean} value.
|
||||||
* @param bool a boolean
|
* @param bool a {@code boolean} value
|
||||||
* @return a JSON boolean
|
* @return a {@link JsonBoolean}
|
||||||
*/
|
*/
|
||||||
public static @NotNull JsonBoolean valueOf(boolean bool) {
|
public static @NotNull JsonBoolean valueOf(boolean bool) {
|
||||||
return bool ? TRUE : FALSE;
|
return bool ? TRUE : FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given boolean to a JSON boolean.
|
* Creates a {@link JsonBoolean} wrapping the given {@link Boolean} value.
|
||||||
* @param bool a boolean
|
* @param bool a {@link Boolean} value
|
||||||
* @return a JSON boolean
|
* @return a {@link JsonBoolean}
|
||||||
*/
|
*/
|
||||||
@Contract("null -> null; !null -> !null")
|
@Contract("null -> null; !null -> !null")
|
||||||
public static @Nullable JsonBoolean valueOf(@Nullable Boolean bool) {
|
public static @Nullable JsonBoolean valueOf(@Nullable Boolean bool) {
|
||||||
@ -44,4 +45,9 @@ public enum JsonBoolean implements JsonValue, JsonToken {
|
|||||||
public @NotNull String toString() {
|
public @NotNull String toString() {
|
||||||
return String.valueOf(value);
|
return String.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonBoolean getKind() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package eu.jonahbauer.json;
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||||
|
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||||
|
import eu.jonahbauer.json.token.JsonToken;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@ -10,36 +12,36 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* numbers and therefore all numbers are stored as {@code double}.
|
* numbers and therefore all numbers are stored as {@code double}.
|
||||||
*/
|
*/
|
||||||
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
||||||
|
public static final JsonTokenKind KIND = ComplexTokenKind.NUMBER;
|
||||||
|
private static final long LONG_BIT_MASK = (~0L << Double.PRECISION);
|
||||||
|
|
||||||
public JsonNumber {
|
public JsonNumber {
|
||||||
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given int to a JSON number.
|
* Creates a {@link JsonNumber} wrapping the given {@code int} value.
|
||||||
* @param i an int
|
* @param i an {@code int} value
|
||||||
* @return a new JSON number
|
* @return a {@link JsonNumber}
|
||||||
*/
|
*/
|
||||||
public static @NotNull JsonNumber valueOf(int i) {
|
public static @NotNull JsonNumber valueOf(int i) {
|
||||||
return new JsonNumber(i);
|
return new JsonNumber(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given long to a JSON number.
|
* Creates a {@link JsonNumber} wrapping the given {@code long} value.
|
||||||
* @param l a long
|
* @param l a {@code long} value
|
||||||
* @return a new JSON number
|
* @return a {@link JsonNumber}
|
||||||
* @throws IllegalArgumentException if conversion from {@code long} to {@code double} is lossy
|
* @throws IllegalArgumentException if conversion from {@code long} to {@code double} is lossy
|
||||||
*/
|
*/
|
||||||
public static @NotNull JsonNumber valueOf(long l) {
|
public static @NotNull JsonNumber valueOf(long l) {
|
||||||
if ((long) (double) l != l) {
|
return new JsonNumber(checkLongRange(l));
|
||||||
throw new IllegalArgumentException("lossy conversion from long to double for value " + l);
|
|
||||||
}
|
|
||||||
return new JsonNumber(l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given double to a JSON number.
|
* Creates a {@link JsonNumber} wrapping the given {@code double} value.
|
||||||
* @param d a double
|
* @param d an {@code double} value
|
||||||
* @return a new JSON number
|
* @return a {@link JsonNumber}
|
||||||
* @throws IllegalArgumentException if {@code d} is not {@linkplain Double#isFinite(double) finite}
|
* @throws IllegalArgumentException if {@code d} is not {@linkplain Double#isFinite(double) finite}
|
||||||
*/
|
*/
|
||||||
public static @NotNull JsonNumber valueOf(double d) {
|
public static @NotNull JsonNumber valueOf(double d) {
|
||||||
@ -47,18 +49,30 @@ public record JsonNumber(double value) implements JsonValue, JsonToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given number to a JSON number using {@link Number#doubleValue()}. The conversion to a JSON number
|
* Creates a {@link JsonNumber} wrapping {@code number}. {@code number} is converted to a {@code double} using
|
||||||
* might be lossy.
|
* {@link Number#doubleValue()}.
|
||||||
* @param number a number
|
* @param number a number
|
||||||
* @return a new JSON number
|
* @return a {@link JsonNumber}
|
||||||
*/
|
*/
|
||||||
@Contract("null -> null; !null -> !null")
|
@Contract("null -> null; !null -> !null")
|
||||||
public static @Nullable JsonNumber valueOf(@Nullable Number number) {
|
public static @Nullable JsonNumber valueOf(@Nullable Number number) {
|
||||||
return number == null ? null : new JsonNumber(number.doubleValue());
|
return number == null ? null : new JsonNumber(number.doubleValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long checkLongRange(long value) {
|
||||||
|
if (((value >= 0 ? value : -value) & LONG_BIT_MASK) != 0) {
|
||||||
|
throw new IllegalArgumentException("lossy conversion from long to double for value " + value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String toString() {
|
public @NotNull String toString() {
|
||||||
return (long) value == value ? Long.toString((long) value) : Double.toString(value);
|
return Double.toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonTokenKind getKind() {
|
||||||
|
return KIND;
|
||||||
}
|
}
|
||||||
}
|
}
|
320
src/main/java/eu/jonahbauer/json/JsonObject.java
Normal file
320
src/main/java/eu/jonahbauer/json/JsonObject.java
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java representation of a JSON object.
|
||||||
|
* @param entries the object's entries
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public record JsonObject(@NotNull Map<@NotNull String, @Nullable JsonValue> entries) implements JsonValue, Map<@NotNull String, @Nullable JsonValue> {
|
||||||
|
|
||||||
|
public JsonObject {
|
||||||
|
Objects.requireNonNull(entries, "entries");
|
||||||
|
if (!Util.isImmutable(entries)) {
|
||||||
|
if (entries instanceof SequencedMap<?, ?>) {
|
||||||
|
var map = new LinkedHashMap<String, JsonValue>();
|
||||||
|
entries.forEach((key, value) -> map.put(Objects.requireNonNull(key, "key"), value));
|
||||||
|
entries = Collections.unmodifiableSequencedMap(map);
|
||||||
|
} else {
|
||||||
|
var map = new HashMap<String, JsonValue>();
|
||||||
|
entries.forEach((key, value) -> map.put(Objects.requireNonNull(key, "key"), value));
|
||||||
|
entries = Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link JsonObject} from the given map, using {@link JsonValue#valueOf(Object)} to convert the values to
|
||||||
|
* {@link JsonValue}s. The keys are expected to be instances of {@link CharSequence}.
|
||||||
|
* @param map a map
|
||||||
|
* @return a {@link JsonObject}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
public static @Nullable JsonObject valueOf(@Nullable Map<?, ?> map) {
|
||||||
|
if (map == null) return null;
|
||||||
|
if (map instanceof JsonObject json) return json;
|
||||||
|
|
||||||
|
var out = new LinkedHashMap<String, JsonValue>();
|
||||||
|
map.forEach((key, value) -> out.put(keyOf(key), JsonValue.valueOf(value)));
|
||||||
|
return new JsonObject(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String keyOf(@Nullable Object object) {
|
||||||
|
return switch (object) {
|
||||||
|
case CharSequence chars -> chars.toString();
|
||||||
|
case null -> throw new JsonProcessingException("cannot convert null to json key");
|
||||||
|
default -> throw new JsonProcessingException(STR."cannot convert object of type \{object.getClass()} to json key");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold desc="Map" defaultstate="collapsed">
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return entries.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsKey(Object key) {
|
||||||
|
Objects.requireNonNull(key, "key");
|
||||||
|
return entries.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsValue(Object value) {
|
||||||
|
return entries.containsValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue get(Object key) {
|
||||||
|
Objects.requireNonNull(key, "key");
|
||||||
|
return entries.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue put(@NotNull String key, @Nullable JsonValue value) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue remove(Object key) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(@NotNull Map<? extends @NotNull String, ? extends @Nullable JsonValue> m) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Set<@NotNull String> keySet() {
|
||||||
|
return entries.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Collection<@Nullable JsonValue> values() {
|
||||||
|
return entries.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Set<Entry<@NotNull String, @Nullable JsonValue>> entrySet() {
|
||||||
|
return entries.entrySet();
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold desc="typed get(...)" defaultstate="collapsed">
|
||||||
|
/**
|
||||||
|
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||||
|
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonString}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonString getString(@NotNull String key) {
|
||||||
|
return get(key, JsonString.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||||
|
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonNumber}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonNumber getNumber(@NotNull String key) {
|
||||||
|
return get(key, JsonNumber.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||||
|
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonBoolean}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonBoolean getBoolean(@NotNull String key) {
|
||||||
|
return get(key, JsonBoolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||||
|
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonArray}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonArray getArray(@NotNull String key) {
|
||||||
|
return get(key, JsonArray.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||||
|
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonObject}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonObject getObject(@NotNull String key) {
|
||||||
|
return get(key, JsonObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends JsonValue> @Nullable T get(@NotNull String key, @NotNull Class<T> type) {
|
||||||
|
return type.cast(entries().get(key));
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
//<editor-fold desc="typed getOrDefault(...)" defaultstate="collapsed">
|
||||||
|
// getStringOrDefault(String, JsonString) omitted because JsonString implements CharSequence
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonString#valueOf(CharSequence) JsonString.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonString}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonString#valueOf(CharSequence) JsonString.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonString}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonString getStringOrDefault(@NotNull String key, @Nullable CharSequence defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonString.class, JsonString::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||||
|
* for the key or key is mapped to an object that is neither {@code null} nor an instance of {@link JsonNumber}.
|
||||||
|
* @return the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||||
|
* for the key or key is mapped to an object that is neither {@code null} nor an instance of
|
||||||
|
* {@link JsonNumber}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable JsonNumber defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonNumber.class, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonNumber#valueOf(Number) JsonNumber.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonNumber}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonNumber#valueOf(Number) JsonNumber.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonNumber}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable Number defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonNumber.class, JsonNumber::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||||
|
* for the key or key is mapped to an object that is neither {@code null} nor an instance of {@link JsonBoolean}.
|
||||||
|
* @return the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||||
|
* for the key or key is mapped to an object that is neither {@code null} nor an instance of
|
||||||
|
* {@link JsonBoolean}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable JsonBoolean defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonBoolean.class, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonBoolean#valueOf(Boolean) JsonBoolean.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonBoolean}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonBoolean#valueOf(Boolean) JsonBoolean.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonBoolean}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable Boolean defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonBoolean.class, JsonBoolean::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getArrayOrDefault(String, JsonArray) omitted because JsonArray implements List<?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonArray#valueOf(List) JsonArray.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonArray}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonArray#valueOf(List) JsonArray.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonArray}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable List<?> defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonArray#valueOf(Object...) JsonArray.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonArray}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonArray#valueOf(Object...) JsonArray.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonArray}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable Object @Nullable... defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getObjectOrDefault(String, JsonObject) omitted because JsonObject implements Map<?, ?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonObject#valueOf(Map) JsonObject.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||||
|
* instance of {@link JsonObject}.
|
||||||
|
* @return the value to which the specified key is mapped, or
|
||||||
|
* {@link JsonObject#valueOf(Map) JsonObject.valueOf}{@code (defaultValue)}
|
||||||
|
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||||
|
* nor an instance of {@link JsonObject}
|
||||||
|
*/
|
||||||
|
public @Nullable JsonObject getObjectOrDefault(@NotNull String key, @Nullable Map<?, ?> defaultValue) {
|
||||||
|
return getOrDefault(key, defaultValue, JsonObject.class, JsonObject::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends JsonValue, S> @Nullable T getOrDefault(@NotNull String key, @Nullable S defaultValue, @NotNull Class<T> type, @NotNull Function<S, T> converter) {
|
||||||
|
JsonValue value = entries.get(key);
|
||||||
|
if (type.isInstance(value)) {
|
||||||
|
return type.cast(value);
|
||||||
|
} else if (value == null && containsKey(key)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return converter.apply(defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
StringJoiner out = new StringJoiner(",", "{", "}");
|
||||||
|
entries.forEach((key, value) -> out.add(JsonString.quote(key) + ":" + value));
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toPrettyJsonString() {
|
||||||
|
StringJoiner out = new StringJoiner(",\n ", "{\n ", "\n}");
|
||||||
|
entries.forEach((key, value) -> out.add(JsonString.quote(key) + ": " + indent(JsonValue.toPrettyJsonString(value))));
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String indent(@NotNull String string) {
|
||||||
|
if (string.isEmpty()) return "";
|
||||||
|
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
|
||||||
|
Iterator<String> it = string.lines().iterator();
|
||||||
|
out.append(it.next());
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String next = it.next();
|
||||||
|
out.append('\n').append(it.hasNext() ? " " : " ").append(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
249
src/main/java/eu/jonahbauer/json/JsonPath.java
Normal file
249
src/main/java/eu/jonahbauer/json/JsonPath.java
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public final class JsonPath {
|
||||||
|
public static final JsonPath EMPTY = new JsonPath(Collections.emptyList());
|
||||||
|
|
||||||
|
private final @NotNull List<@NotNull Component> components;
|
||||||
|
|
||||||
|
public static @NotNull JsonPath parse(@NotNull String path) {
|
||||||
|
var components = new ArrayList<Component>();
|
||||||
|
|
||||||
|
var reader = new StringReader(path);
|
||||||
|
|
||||||
|
if (reader.hasNext()) {
|
||||||
|
if (reader.next() == '.' && !reader.hasNext()) return JsonPath.EMPTY;
|
||||||
|
reader.pushback();
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (reader.next() != '.') throw unexpectedCharacter(reader.current(), "'.'");
|
||||||
|
|
||||||
|
components.add(switch (reader.next()) {
|
||||||
|
// object access with string
|
||||||
|
case '"' -> {
|
||||||
|
reader.pushback();
|
||||||
|
yield new Component.ObjectAccess(readString(reader));
|
||||||
|
}
|
||||||
|
case '[' -> {
|
||||||
|
var out = switch (reader.next()) {
|
||||||
|
// object access with string
|
||||||
|
case '"' -> {
|
||||||
|
reader.pushback();
|
||||||
|
yield new Component.ObjectAccess(readString(reader));
|
||||||
|
}
|
||||||
|
// array access with index
|
||||||
|
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
|
||||||
|
var negative = reader.current() == '-';
|
||||||
|
if (negative) reader.pushback();
|
||||||
|
var index = readInteger(reader);
|
||||||
|
yield new Component.ArrayAccess(index, negative && index != 0);
|
||||||
|
}
|
||||||
|
default -> throw unexpectedCharacter(reader.current(), "'\"' or '1'-'9'");
|
||||||
|
};
|
||||||
|
if (reader.next() != ']') throw unexpectedCharacter(reader.current(), "']'");
|
||||||
|
yield out;
|
||||||
|
}
|
||||||
|
// object access with identifier
|
||||||
|
default -> {
|
||||||
|
var chr = reader.current();
|
||||||
|
if (chr == '_' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z') {
|
||||||
|
yield new Component.ObjectAccess(readIdentifier(reader));
|
||||||
|
} else {
|
||||||
|
throw unexpectedCharacter(chr, "one of '\"', '[', '_', 'a'-'z' or 'A'-'Z'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} while (reader.hasNext());
|
||||||
|
|
||||||
|
return new JsonPath(Collections.unmodifiableList(components));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonPath(@NotNull List<@NotNull Component> components) {
|
||||||
|
this.components = components;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable JsonValue select(@Nullable JsonValue value) {
|
||||||
|
for (Component component : components) {
|
||||||
|
value = component.select(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull JsonPath resolve(@NotNull String key) {
|
||||||
|
var components = new ArrayList<Component>(this.components.size() + 1);
|
||||||
|
components.addAll(this.components);
|
||||||
|
components.add(new Component.ObjectAccess(key));
|
||||||
|
return new JsonPath(Collections.unmodifiableList(components));
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull JsonPath resolve(int index) {
|
||||||
|
var components = new ArrayList<Component>(this.components.size() + 1);
|
||||||
|
components.addAll(this.components);
|
||||||
|
components.add(new Component.ArrayAccess(Math.abs(index), index < 0));
|
||||||
|
return new JsonPath(Collections.unmodifiableList(components));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface Component {
|
||||||
|
@Nullable JsonValue select(@Nullable JsonValue value);
|
||||||
|
|
||||||
|
record ArrayAccess(int index, boolean end) implements Component {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue select(@Nullable JsonValue value) {
|
||||||
|
if (value instanceof JsonArray array) {
|
||||||
|
if (end) return array.get(array.size() - index);
|
||||||
|
return array.get(index);
|
||||||
|
} else {
|
||||||
|
throw new JsonProcessingException("unexpected type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + index + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ObjectAccess(@NotNull String key) implements Component {
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue select(@Nullable JsonValue value) {
|
||||||
|
if (value instanceof JsonObject object) {
|
||||||
|
return object.get(key);
|
||||||
|
} else {
|
||||||
|
throw new JsonProcessingException("unexpected type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ".[\"" + JsonString.quote(key) + "\"]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readInteger(@NotNull StringReader reader) {
|
||||||
|
var i = reader.next() - '0';
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
var chr = reader.next();
|
||||||
|
if (chr < '0' || chr > '9') {
|
||||||
|
reader.pushback();
|
||||||
|
break;
|
||||||
|
} else if (i == 0) {
|
||||||
|
throw unexpectedCharacter(chr, "']'");
|
||||||
|
} else {
|
||||||
|
i = Math.multiplyExact(i, 10);
|
||||||
|
i = Math.addExact(i, chr - '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String readString(@NotNull StringReader reader) {
|
||||||
|
var out = new StringBuilder();
|
||||||
|
if (reader.next() != '"') throw new IllegalStateException();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
char chr = reader.next();
|
||||||
|
if (chr == '"') {
|
||||||
|
return out.toString();
|
||||||
|
} else if (chr == '\\') {
|
||||||
|
out.append(switch (reader.next()) {
|
||||||
|
case '"' -> '"';
|
||||||
|
case '\\' -> '\\';
|
||||||
|
case '/' -> '/';
|
||||||
|
case 'b' -> '\b';
|
||||||
|
case 'f' -> '\f';
|
||||||
|
case 'n' -> '\n';
|
||||||
|
case 'r' -> '\r';
|
||||||
|
case 't' -> '\t';
|
||||||
|
case 'u' -> {
|
||||||
|
var u0 = toHex(reader.next());
|
||||||
|
var u1 = toHex(reader.next());
|
||||||
|
var u2 = toHex(reader.next());
|
||||||
|
var u3 = toHex(reader.next());
|
||||||
|
|
||||||
|
yield (char) ((u0 << 12) + (u1 << 8) + (u2 << 4) + u3);
|
||||||
|
}
|
||||||
|
default -> throw unexpectedCharacter(reader.current(), "one of '\"', '\\', '/', 'b', 'f', 'n', 'r', 't' or 'u'");
|
||||||
|
});
|
||||||
|
} else if (chr < 32) {
|
||||||
|
throw new JsonProcessingException("unescaped control character in string literal");
|
||||||
|
} else {
|
||||||
|
out.append(chr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int toHex(char chr) {
|
||||||
|
if ('0' <= chr && chr <= '9') {
|
||||||
|
return chr - '0';
|
||||||
|
} else if ('a' <= chr && chr <= 'f') {
|
||||||
|
return 11 + chr - 'a';
|
||||||
|
} else if ('A' <= chr && chr <= 'F') {
|
||||||
|
return 11 + chr - 'A';
|
||||||
|
} else {
|
||||||
|
throw unexpectedCharacter(chr, "one of '0'-'9', 'a'-'f' or 'A'-'F'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String readIdentifier(@NotNull StringReader reader) {
|
||||||
|
var out = new StringBuilder();
|
||||||
|
out.append(reader.next());
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
var chr = reader.next();
|
||||||
|
if (chr == '_' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || '0' <= chr && chr <= '9') {
|
||||||
|
out.append(chr);
|
||||||
|
} else {
|
||||||
|
throw unexpectedCharacter(chr, "one of '.', '_', 'a'-'z', 'A'-'Z' or '0'-'9'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonProcessingException unexpectedCharacter(char character, String expected) {
|
||||||
|
return new JsonProcessingException("unexpected character '" + character + "' (expected " + expected + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StringReader {
|
||||||
|
private final String string;
|
||||||
|
private final int length;
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public StringReader(String string) {
|
||||||
|
this.string = string;
|
||||||
|
this.length = string.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushback() {
|
||||||
|
if (index == 0) throw new IllegalStateException();
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char next() {
|
||||||
|
if (index >= length) {
|
||||||
|
throw new JsonProcessingException("unexpected end of string");
|
||||||
|
}
|
||||||
|
return string.charAt(index++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char current() {
|
||||||
|
return string.charAt(index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return index < length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class JsonProcessingException extends RuntimeException {
|
||||||
|
private final @Nullable Position position;
|
||||||
|
|
||||||
|
public JsonProcessingException(int fragment, int line, int column, @NotNull String message) {
|
||||||
|
super(message);
|
||||||
|
this.position = new Position(fragment, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonProcessingException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.position = new Position(fragment, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonProcessingException(@NotNull String message) {
|
||||||
|
super(message);
|
||||||
|
this.position = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonProcessingException(@NotNull String message, @NotNull Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.position = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
if (position != null) {
|
||||||
|
return super.getMessage() + " at " + position;
|
||||||
|
} else {
|
||||||
|
return super.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Position(int fragment, int line, int column) {
|
||||||
|
public Position {
|
||||||
|
if (fragment < -1) throw new IllegalArgumentException();
|
||||||
|
if (line < 0) throw new IllegalArgumentException();
|
||||||
|
if (column < 0) throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (fragment == -1) {
|
||||||
|
return STR."line \{line}, column \{column}";
|
||||||
|
} else {
|
||||||
|
return STR."fragment \{fragment}, line \{line}, column \{column}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package eu.jonahbauer.json;
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||||
|
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||||
|
import eu.jonahbauer.json.token.JsonToken;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@ -8,38 +10,13 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public record JsonString(@NotNull String value) implements JsonValue, JsonToken, CharSequence {
|
public record JsonString(@NotNull String value) implements JsonValue, JsonToken, CharSequence {
|
||||||
public static final @NotNull JsonString EMPTY = new JsonString("");
|
public static final JsonTokenKind KIND = ComplexTokenKind.STRING;
|
||||||
|
|
||||||
public JsonString {
|
public JsonString {
|
||||||
Objects.requireNonNull(value, "value");
|
Objects.requireNonNull(value, "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given character to a JSON string.
|
|
||||||
* @param chr a character
|
|
||||||
* @return a new JSON string
|
|
||||||
*/
|
|
||||||
public static @NotNull JsonString valueOf(char chr) {
|
|
||||||
return new JsonString(String.valueOf(chr));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given character to a JSON string.
|
|
||||||
* @param chr a character
|
|
||||||
* @return a new JSON string
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
|
||||||
public static @Nullable JsonString valueOf(@Nullable Character chr) {
|
|
||||||
return chr == null ? null : new JsonString(String.valueOf(chr));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given string to a JSON string.
|
|
||||||
* @param chars a string
|
|
||||||
* @return a new JSON string
|
|
||||||
*/
|
|
||||||
@Contract("null -> null; !null -> !null")
|
@Contract("null -> null; !null -> !null")
|
||||||
public static @Nullable JsonString valueOf(@Nullable CharSequence chars) {
|
public static @Nullable JsonString valueOf(@Nullable CharSequence chars) {
|
||||||
return switch (chars) {
|
return switch (chars) {
|
||||||
@ -125,4 +102,14 @@ public record JsonString(@NotNull String value) implements JsonValue, JsonToken,
|
|||||||
public @NotNull String toJsonString() {
|
public @NotNull String toJsonString() {
|
||||||
return quote(value());
|
return quote(value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toPrettyJsonString() {
|
||||||
|
return toJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonTokenKind getKind() {
|
||||||
|
return KIND;
|
||||||
|
}
|
||||||
}
|
}
|
87
src/main/java/eu/jonahbauer/json/JsonValue.java
Normal file
87
src/main/java/eu/jonahbauer/json/JsonValue.java
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.parser.JsonConversionException;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public sealed interface JsonValue extends Serializable permits JsonObject, JsonArray, JsonBoolean, JsonNumber, JsonString {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the JSON representation of this value}
|
||||||
|
*/
|
||||||
|
default @NotNull String toJsonString() {
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return a prettified JSON representation of this value}
|
||||||
|
*/
|
||||||
|
default @NotNull String toPrettyJsonString() {
|
||||||
|
return toJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
default @Nullable JsonValue select(@NotNull JsonPath path) {
|
||||||
|
return path.select(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
default @Nullable JsonValue select(@NotNull String path) {
|
||||||
|
return JsonPath.parse(path).select(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the JSON representation of the given value}
|
||||||
|
* @param value a {@link JsonValue} (possibly {@code null})
|
||||||
|
*/
|
||||||
|
static @NotNull String toJsonString(@Nullable JsonValue value) {
|
||||||
|
return value == null ? "null" : value.toJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the prettified JSON representation of the given value}
|
||||||
|
* @param value a {@link JsonValue} (possibly {@code null})
|
||||||
|
*/
|
||||||
|
static @NotNull String toPrettyJsonString(@Nullable JsonValue value) {
|
||||||
|
return value == null ? "null" : value.toPrettyJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to convert {@code object} to a {@link JsonValue}:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link JsonValue}s and {@code null} are simple returned</li>
|
||||||
|
* <li>{@link Number}s are converted with {@link JsonNumber#valueOf(Number)}</li>
|
||||||
|
* <li>{@link CharSequence}s are converted with {@link JsonString#valueOf(CharSequence)}</li>
|
||||||
|
* <li>{@link Boolean}s are converted with {@link JsonBoolean#valueOf(Boolean)}</li>
|
||||||
|
* <li>{@link List}s are converted with {@link JsonArray#valueOf(List)}</li>
|
||||||
|
* <li>
|
||||||
|
* {@code Object[]}, {@code int[]} and {@code double[]} are converted with
|
||||||
|
* {@link JsonArray#valueOf(Object...)}, {@link JsonArray#valueOf(int...)} and
|
||||||
|
* {@link JsonArray#valueOf(double...)} resp.
|
||||||
|
* </li>
|
||||||
|
* <li>{@link Map}s are converted with {@link JsonObject#valueOf(Map)}</li>
|
||||||
|
* </ul>
|
||||||
|
* @param object an object
|
||||||
|
* @return a {@link JsonValue} representing the object
|
||||||
|
* @throws JsonConversionException if the object cannot be converted to a {@link JsonValue}
|
||||||
|
*/
|
||||||
|
@Contract("null -> null; !null -> !null")
|
||||||
|
static @Nullable JsonValue valueOf(@Nullable Object object) {
|
||||||
|
return switch (object) {
|
||||||
|
case JsonValue json -> json;
|
||||||
|
case Number number -> JsonNumber.valueOf(number);
|
||||||
|
case CharSequence chars -> JsonString.valueOf(chars);
|
||||||
|
case Boolean bool -> JsonBoolean.valueOf(bool);
|
||||||
|
case List<?> list -> JsonArray.valueOf(list);
|
||||||
|
case Object[] array -> JsonArray.valueOf(array);
|
||||||
|
case int[] array -> JsonArray.valueOf(array);
|
||||||
|
case double[] array -> JsonArray.valueOf(array);
|
||||||
|
case Map<?, ?> map -> JsonObject.valueOf(map);
|
||||||
|
case null -> null;
|
||||||
|
default -> throw new JsonConversionException(STR."cannot convert object of type \{object.getClass()} to json value");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
21
src/main/java/eu/jonahbauer/json/Util.java
Normal file
21
src/main/java/eu/jonahbauer/json/Util.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class Util {
|
||||||
|
private static final @NotNull Class<?> LIST_1_2 = List.of(1).getClass();
|
||||||
|
private static final @NotNull Class<?> LIST_N = List.of(1, 2, 3).getClass();
|
||||||
|
private static final @NotNull Class<?> MAP_1 = Map.of(1, 2).getClass();
|
||||||
|
private static final @NotNull Class<?> MAP_N = Map.of(1, 2, 3, 4).getClass();
|
||||||
|
|
||||||
|
public static boolean isImmutable(@NotNull List<?> list) {
|
||||||
|
return LIST_1_2.isInstance(list) || LIST_N.isInstance(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isImmutable(@NotNull Map<?, ?> map) {
|
||||||
|
return MAP_1.isInstance(map) || MAP_N.isInstance(map);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
package eu.jonahbauer.json.exceptions;
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonProcessingException;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
|
||||||
* An exception that indicates problems with conversion between java objects and json.
|
|
||||||
*/
|
|
||||||
public class JsonConversionException extends JsonProcessingException {
|
public class JsonConversionException extends JsonProcessingException {
|
||||||
public JsonConversionException(@NotNull String message) {
|
public JsonConversionException(@NotNull String message) {
|
||||||
super(message);
|
super(message);
|
231
src/main/java/eu/jonahbauer/json/parser/JsonParser.java
Normal file
231
src/main/java/eu/jonahbauer/json/parser/JsonParser.java
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/main/java/eu/jonahbauer/json/parser/JsonProcessor.java
Normal file
15
src/main/java/eu/jonahbauer/json/parser/JsonProcessor.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
@SuppressWarnings("java:S6548")
|
||||||
|
public enum JsonProcessor implements StringTemplate.Processor<@Nullable JsonValue, RuntimeException> {
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue process(@NotNull StringTemplate template) {
|
||||||
|
return new JsonParser(template).parse();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package eu.jonahbauer.json.exceptions;
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonProcessingException;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class JsonTokenizerException extends JsonReaderException {
|
public class JsonTokenizerException extends JsonProcessingException {
|
||||||
|
|
||||||
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message) {
|
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message) {
|
||||||
super(fragment, line, column, message);
|
super(fragment, line, column, message);
|
||||||
}
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.jonahbauer.json.parser.tokenizer;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.token.JsonToken;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
public interface JsonTokenizer extends Iterable<JsonToken> {
|
||||||
|
|
||||||
|
@Nullable JsonToken next() throws IOException;
|
||||||
|
|
||||||
|
int getFragment();
|
||||||
|
int getLine();
|
||||||
|
int getColumn();
|
||||||
|
|
||||||
|
boolean hasTemplateSupport();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default @NotNull Iterator<@NotNull JsonToken> iterator() {
|
||||||
|
class JsonTokenizerIterator implements Iterator<@NotNull JsonToken> {
|
||||||
|
private JsonToken next;
|
||||||
|
private boolean valid = false;
|
||||||
|
|
||||||
|
|
||||||
|
private void ensureValid() {
|
||||||
|
try {
|
||||||
|
if (!valid) {
|
||||||
|
next = JsonTokenizer.this.next();
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
ensureValid();
|
||||||
|
return next != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonToken next() {
|
||||||
|
ensureValid();
|
||||||
|
if (next == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
} else {
|
||||||
|
valid = false;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonTokenizerIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
default @NotNull Stream<@NotNull JsonToken> stream() {
|
||||||
|
return StreamSupport.stream(this.spliterator(), false);
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +1,40 @@
|
|||||||
package eu.jonahbauer.json.tokenizer;
|
package eu.jonahbauer.json.parser.tokenizer;
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
import eu.jonahbauer.json.token.*;
|
||||||
import eu.jonahbauer.json.tokenizer.reader.PushbackReader;
|
|
||||||
import eu.jonahbauer.json.tokenizer.reader.PushbackReaderImpl;
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.*;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import eu.jonahbauer.json.JsonBoolean;
|
import eu.jonahbauer.json.JsonBoolean;
|
||||||
import eu.jonahbauer.json.JsonNumber;
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
import eu.jonahbauer.json.JsonString;
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import eu.jonahbauer.json.parser.JsonTokenizerException;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class JsonTokenizerImpl implements JsonTokenizer {
|
public class JsonTokenizerImpl implements JsonTokenizer {
|
||||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?([eE][+-]?\\d+)?");
|
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?([eE][+-]?\\d+)?");
|
||||||
private static final JsonPunctuation[] PUNCTUATION;
|
private final @NotNull TemplateReader reader;
|
||||||
|
|
||||||
static {
|
|
||||||
PUNCTUATION = new JsonPunctuation[128];
|
|
||||||
for (var punctuation : JsonPunctuation.values()) {
|
|
||||||
PUNCTUATION[punctuation.value()] = punctuation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final @NotNull PushbackReader reader;
|
|
||||||
private final int[] buffer = new int[4];
|
private final int[] buffer = new int[4];
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private int fragment;
|
private int fragment;
|
||||||
@Getter
|
@Getter
|
||||||
private int lineNumber;
|
private int line;
|
||||||
@Getter
|
@Getter
|
||||||
private int columnNumber;
|
private int column;
|
||||||
|
|
||||||
public JsonTokenizerImpl(@NotNull String json) {
|
public JsonTokenizerImpl(@NotNull String json) {
|
||||||
this(new StringReader(json));
|
this.reader = new TemplateReader(json);
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
public JsonTokenizerImpl(@NotNull StringTemplate template) {
|
|
||||||
this.reader = new PushbackReaderImpl(template);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonTokenizerImpl(@NotNull Reader reader) {
|
public JsonTokenizerImpl(@NotNull Reader reader) {
|
||||||
this.reader = new PushbackReaderImpl(reader);
|
this.reader = new TemplateReader(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonTokenizerImpl(@NotNull StringTemplate json) {
|
||||||
|
this.reader = new TemplateReader(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -58,40 +46,62 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
} while (isWhitespace(chr));
|
} while (isWhitespace(chr));
|
||||||
|
|
||||||
fragment = reader.getFragment();
|
fragment = reader.getFragment();
|
||||||
lineNumber = reader.getLineNumber();
|
line = reader.getLine();
|
||||||
columnNumber = reader.getColumnNumber();
|
column = reader.getColumn();
|
||||||
|
|
||||||
if (chr == PushbackReader.EOF) return null;
|
return switch (chr) {
|
||||||
if (chr == PushbackReader.PLACEHOLDER) return new JsonPlaceholder(reader.getObject());
|
case TemplateReader.EOF -> null;
|
||||||
if (chr < 128 && PUNCTUATION[chr] != null) return PUNCTUATION[chr];
|
case TemplateReader.PLACEHOLDER -> new JsonPlaceholder(reader.getObject());
|
||||||
if (chr == '"') return nextString();
|
case '{' -> JsonPunctuation.BEGIN_OBJECT;
|
||||||
|
case '}' -> JsonPunctuation.END_OBJECT;
|
||||||
reader.pushback();
|
case '[' -> JsonPunctuation.BEGIN_ARRAY;
|
||||||
String token = nextToken();
|
case ']' -> JsonPunctuation.END_ARRAY;
|
||||||
return switch (token) {
|
case ',' -> JsonPunctuation.VALUE_SEPARATOR;
|
||||||
case "true" -> JsonBoolean.TRUE;
|
case ':' -> JsonPunctuation.NAME_SEPARATOR;
|
||||||
case "false" -> JsonBoolean.FALSE;
|
case '"' -> {
|
||||||
case "null" -> JsonNull.NULL;
|
StringTemplate string = readString();
|
||||||
case String number when isNumberLiteral(number) -> {
|
if (string.fragments().size() == 1) {
|
||||||
try {
|
yield new JsonString(string.fragments().getFirst());
|
||||||
yield new JsonNumber(Double.parseDouble(number));
|
} else {
|
||||||
} catch (IllegalArgumentException ex) {
|
yield new JsonStringTemplate(string);
|
||||||
throw error("invalid number literal: " + number, ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default -> throw error("invalid token: " + token);
|
default -> {
|
||||||
|
if (chr < 0) throw new AssertionError();
|
||||||
|
|
||||||
|
reader.pushback();
|
||||||
|
String token = nextToken();
|
||||||
|
yield switch (token) {
|
||||||
|
case "true" -> JsonBoolean.TRUE;
|
||||||
|
case "false" -> JsonBoolean.FALSE;
|
||||||
|
case "null" -> JsonNull.NULL;
|
||||||
|
case String number when isNumberLiteral(number) -> {
|
||||||
|
try {
|
||||||
|
yield new JsonNumber(Double.parseDouble(number));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw error("invalid number literal: " + number, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw error("invalid token: " + token);
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWhitespace(int chr) {
|
@Override
|
||||||
|
public boolean hasTemplateSupport() {
|
||||||
|
return reader.isHasTemplateSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isWhitespace(int chr) {
|
||||||
return chr == ' ' || chr == '\t' || chr == '\r' || chr == '\n';
|
return chr == ' ' || chr == '\t' || chr == '\r' || chr == '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPunctuation(int chr) {
|
protected boolean isPunctuation(int chr) {
|
||||||
return chr == '{' || chr == '}' || chr == '[' || chr == ']' || chr == ',' || chr == ':' || chr == '"';
|
return chr == '{' || chr == '}' || chr == '[' || chr == ']' || chr == ',' || chr == ':' || chr == '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isNumberLiteral(@NotNull String token) {
|
protected boolean isNumberLiteral(@NotNull String token) {
|
||||||
return NUMBER_PATTERN.matcher(token).matches();
|
return NUMBER_PATTERN.matcher(token).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,26 +120,20 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonToken nextString() throws IOException {
|
private @NotNull StringTemplate readString() throws IOException {
|
||||||
var fragments = new ArrayList<String>();
|
List<String> fragments = new ArrayList<>();
|
||||||
var values = new ArrayList<>();
|
List<Object> values = new ArrayList<>();
|
||||||
|
|
||||||
var current = new StringBuilder();
|
StringBuilder current = new StringBuilder();
|
||||||
while (true) {
|
while (true) {
|
||||||
int chr = reader.read();
|
int chr = reader.read();
|
||||||
if (chr == '"') {
|
if (chr == '"') {
|
||||||
if (fragments.isEmpty()) {
|
fragments.add(current.toString());
|
||||||
return new JsonString(current.toString());
|
return StringTemplate.of(fragments, values);
|
||||||
} else {
|
|
||||||
fragments.add(current.toString());
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
var result = new JsonStringTemplate(StringTemplate.of(fragments, values));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else if (chr == '\\') {
|
} else if (chr == '\\') {
|
||||||
chr = reader.read();
|
chr = reader.read();
|
||||||
switch (chr) {
|
switch (chr) {
|
||||||
case PushbackReader.EOF -> throw error("incomplete escape sequence in string literal");
|
case TemplateReader.EOF -> throw error("incomplete escape sequence in string literal");
|
||||||
case '"' -> current.append('"');
|
case '"' -> current.append('"');
|
||||||
case '\\' -> current.append('\\');
|
case '\\' -> current.append('\\');
|
||||||
case '/' -> current.append('/');
|
case '/' -> current.append('/');
|
||||||
@ -159,17 +163,17 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
} else if ('A' <= esc && esc <= 'F') {
|
} else if ('A' <= esc && esc <= 'F') {
|
||||||
code += 11 + (esc - 'A');
|
code += 11 + (esc - 'A');
|
||||||
} else {
|
} else {
|
||||||
throw error("invalid character " + toString(buffer[i]) + " in escape sequence");
|
throw error(STR."invalid character \{toString(buffer[i])} in escape sequence");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current.append((char) code);
|
current.append((char) code);
|
||||||
}
|
}
|
||||||
default -> throw error("invalid character " + toString(chr) + " in escape sequence");
|
default -> throw error(STR."invalid character \{toString(chr)} in escape sequence");
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (chr == PushbackReader.EOF) {
|
} else if (chr == TemplateReader.EOF) {
|
||||||
throw error("unclosed string literal");
|
throw error("unclosed string literal");
|
||||||
} else if (chr == PushbackReader.PLACEHOLDER) {
|
} else if (chr == TemplateReader.PLACEHOLDER) {
|
||||||
fragments.add(current.toString());
|
fragments.add(current.toString());
|
||||||
current.setLength(0);
|
current.setLength(0);
|
||||||
values.add(reader.getObject());
|
values.add(reader.getObject());
|
||||||
@ -182,16 +186,17 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonTokenizerException error(@NotNull String message) {
|
private @NotNull JsonTokenizerException error(@NotNull String message) {
|
||||||
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message);
|
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
|
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
|
||||||
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message, cause);
|
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull String toString(int chr) {
|
private static @NotNull String toString(int chr) {
|
||||||
return switch (chr) {
|
return switch (chr) {
|
||||||
case PushbackReader.EOF -> "EOF";
|
case TemplateReader.EOF -> "EOF";
|
||||||
|
case TemplateReader.PLACEHOLDER -> "PLACEHOLDER";
|
||||||
case '\t' -> "'\\t'";
|
case '\t' -> "'\\t'";
|
||||||
case '\r' -> "'\\r'";
|
case '\r' -> "'\\r'";
|
||||||
case '\n' -> "'\\n'";
|
case '\n' -> "'\\n'";
|
||||||
@ -199,9 +204,9 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
case '\f' -> "'\\f'";
|
case '\f' -> "'\\f'";
|
||||||
default -> {
|
default -> {
|
||||||
if (Character.isISOControl((char) chr)) {
|
if (Character.isISOControl((char) chr)) {
|
||||||
yield "0x" + Integer.toHexString(chr);
|
yield STR."0x\{Integer.toHexString(chr)}";
|
||||||
} else {
|
} else {
|
||||||
yield "'" + (char) chr + "'";
|
yield STR."'\{(char) chr}'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -0,0 +1,125 @@
|
|||||||
|
package eu.jonahbauer.json.parser.tokenizer;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
class TemplateReader {
|
||||||
|
public static final int EOF = -1;
|
||||||
|
public static final int PLACEHOLDER = -2;
|
||||||
|
|
||||||
|
|
||||||
|
private final IntFunction<Reader> readers;
|
||||||
|
private final List<Object> values;
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final boolean hasTemplateSupport;
|
||||||
|
|
||||||
|
private Reader reader;
|
||||||
|
|
||||||
|
private int fragment;
|
||||||
|
@Getter
|
||||||
|
private int line = 1;
|
||||||
|
@Getter
|
||||||
|
private int column;
|
||||||
|
|
||||||
|
|
||||||
|
private int current;
|
||||||
|
private Object currentObject;
|
||||||
|
|
||||||
|
private boolean pushback;
|
||||||
|
|
||||||
|
|
||||||
|
public TemplateReader(@NotNull String string) {
|
||||||
|
this(new StringReader(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateReader(@NotNull Reader reader) {
|
||||||
|
Objects.requireNonNull(reader, "reader");
|
||||||
|
this.readers = _ -> reader;
|
||||||
|
this.values = List.of();
|
||||||
|
this.length = 1;
|
||||||
|
this.hasTemplateSupport = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateReader(@NotNull StringTemplate template) {
|
||||||
|
Objects.requireNonNull(template, "template");
|
||||||
|
this.readers = i -> new StringReader(template.fragments().get(i));
|
||||||
|
this.values = template.values();
|
||||||
|
this.length = template.fragments().size();
|
||||||
|
this.hasTemplateSupport = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (pushback) {
|
||||||
|
pushback = false;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureReader()) return EOF;
|
||||||
|
|
||||||
|
int result = reader.read();
|
||||||
|
|
||||||
|
// track position
|
||||||
|
if (current != '\r' && result == '\n' || result == '\r') {
|
||||||
|
line++;
|
||||||
|
column = 0;
|
||||||
|
} else if (result != '\n') {
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == EOF) {
|
||||||
|
reader = null;
|
||||||
|
if (fragment < length) {
|
||||||
|
currentObject = values.get(fragment - 1);
|
||||||
|
current = PLACEHOLDER;
|
||||||
|
} else {
|
||||||
|
currentObject = null;
|
||||||
|
current = result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentObject = null;
|
||||||
|
current = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean ensureReader() {
|
||||||
|
if (reader != null) {
|
||||||
|
return true;
|
||||||
|
} else if (fragment < length) {
|
||||||
|
reader = readers.apply(fragment++);
|
||||||
|
line = 1;
|
||||||
|
column = 0;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getObject() {
|
||||||
|
if (current != PLACEHOLDER) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return currentObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushback() {
|
||||||
|
if (pushback) throw new IllegalStateException();
|
||||||
|
pushback = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFragment() {
|
||||||
|
return hasTemplateSupport ? fragment : -1;
|
||||||
|
}
|
||||||
|
}
|
18
src/main/java/eu/jonahbauer/json/token/JsonNull.java
Normal file
18
src/main/java/eu/jonahbauer/json/token/JsonNull.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@SuppressWarnings("java:S6548")
|
||||||
|
public enum JsonNull implements JsonToken, JsonTokenKind {
|
||||||
|
NULL;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonNull getKind() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
13
src/main/java/eu/jonahbauer/json/token/JsonPlaceholder.java
Normal file
13
src/main/java/eu/jonahbauer/json/token/JsonPlaceholder.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public record JsonPlaceholder(Object value) implements JsonToken {
|
||||||
|
public static final JsonTokenKind KIND = ComplexTokenKind.PLACEHOLDER;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonTokenKind getKind() {
|
||||||
|
return KIND;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.token;
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -8,7 +8,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
@Getter
|
@Getter
|
||||||
@Accessors(fluent = true)
|
@Accessors(fluent = true)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum JsonPunctuation implements JsonToken {
|
public enum JsonPunctuation implements JsonToken, JsonTokenKind {
|
||||||
BEGIN_OBJECT('{'),
|
BEGIN_OBJECT('{'),
|
||||||
END_OBJECT('}'),
|
END_OBJECT('}'),
|
||||||
BEGIN_ARRAY('['),
|
BEGIN_ARRAY('['),
|
||||||
@ -19,6 +19,11 @@ public enum JsonPunctuation implements JsonToken {
|
|||||||
|
|
||||||
private final char value;
|
private final char value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonTokenKind getKind() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String toString() {
|
public @NotNull String toString() {
|
||||||
return name() + "(" + value + ")";
|
return name() + "(" + value + ")";
|
@ -1,17 +1,24 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.token;
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
import eu.jonahbauer.json.JsonString;
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
public record JsonStringTemplate(@NotNull StringTemplate template) implements JsonToken {
|
public record JsonStringTemplate(@NotNull StringTemplate template) implements JsonToken {
|
||||||
|
public static final JsonTokenKind KIND = ComplexTokenKind.STRING_TEMPLATE;
|
||||||
|
|
||||||
public JsonStringTemplate {
|
public JsonStringTemplate {
|
||||||
Objects.requireNonNull(template);
|
Objects.requireNonNull(template, "template");
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull JsonString asString() {
|
public @NotNull JsonString asString() {
|
||||||
return new JsonString(STR.process(template));
|
return new JsonString(STR.process(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull JsonTokenKind getKind() {
|
||||||
|
return KIND;
|
||||||
|
}
|
||||||
}
|
}
|
12
src/main/java/eu/jonahbauer/json/token/JsonToken.java
Normal file
12
src/main/java/eu/jonahbauer/json/token/JsonToken.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonBoolean;
|
||||||
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public sealed interface JsonToken permits JsonBoolean, JsonNull, JsonNumber, JsonString, JsonPlaceholder, JsonPunctuation, JsonStringTemplate {
|
||||||
|
|
||||||
|
@NotNull JsonTokenKind getKind();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.jonahbauer.json.token;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||||
|
import eu.jonahbauer.json.JsonBoolean;
|
||||||
|
|
||||||
|
public sealed interface JsonTokenKind permits JsonBoolean, JsonNull, JsonPunctuation, ComplexTokenKind {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.jonahbauer.json.token.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||||
|
|
||||||
|
public enum ComplexTokenKind implements JsonTokenKind {
|
||||||
|
STRING, NUMBER, STRING_TEMPLATE, PLACEHOLDER
|
||||||
|
}
|
@ -3,8 +3,6 @@ module eu.jonahbauer.json {
|
|||||||
requires static org.jetbrains.annotations;
|
requires static org.jetbrains.annotations;
|
||||||
|
|
||||||
exports eu.jonahbauer.json;
|
exports eu.jonahbauer.json;
|
||||||
exports eu.jonahbauer.json.exceptions;
|
|
||||||
exports eu.jonahbauer.json.tokenizer;
|
|
||||||
exports eu.jonahbauer.json.tokenizer.token;
|
|
||||||
exports eu.jonahbauer.json.parser;
|
exports eu.jonahbauer.json.parser;
|
||||||
|
exports eu.jonahbauer.json.token;
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package eu.jonahbauer.json.parser;
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonParserException;
|
import eu.jonahbauer.json.JsonProcessingException;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
@ -11,6 +10,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@ -18,10 +18,9 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
class JsonParserTest {
|
class JsonParserTest {
|
||||||
|
|
||||||
// https://github.com/nst/JSONTestSuite
|
|
||||||
@ParameterizedTest(name = "{0}")
|
@ParameterizedTest(name = "{0}")
|
||||||
@MethodSource("parameters")
|
@MethodSource("parameters")
|
||||||
void suite(@NotNull String name) throws IOException {
|
void suite(String name) throws IOException {
|
||||||
var path = "/nst/JsonTestSuite/" + name;
|
var path = "/nst/JsonTestSuite/" + name;
|
||||||
Boolean expected = switch (name.charAt(0)) {
|
Boolean expected = switch (name.charAt(0)) {
|
||||||
case 'i' -> null;
|
case 'i' -> null;
|
||||||
@ -37,14 +36,14 @@ class JsonParserTest {
|
|||||||
var parser = new JsonParser(reader);
|
var parser = new JsonParser(reader);
|
||||||
|
|
||||||
if (expected == Boolean.FALSE) {
|
if (expected == Boolean.FALSE) {
|
||||||
assertThrows(JsonParserException.class, parser::parse).printStackTrace(System.out);
|
assertThrows(JsonProcessingException.class, parser::parse).printStackTrace(System.out);
|
||||||
} else if (expected == Boolean.TRUE) {
|
} else if (expected == Boolean.TRUE) {
|
||||||
assertDoesNotThrow(parser::parse);
|
assertDoesNotThrow(parser::parse);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
parser.parse();
|
parser.parse();
|
||||||
System.out.println("accepted");
|
System.out.println("accepted");
|
||||||
} catch (JsonParserException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
System.out.println("rejected");
|
System.out.println("rejected");
|
||||||
ex.printStackTrace(System.out);
|
ex.printStackTrace(System.out);
|
||||||
}
|
}
|
||||||
@ -52,8 +51,8 @@ class JsonParserTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static @NotNull Stream<@NotNull Arguments> parameters() throws IOException {
|
static Stream<Arguments> parameters() throws IOException {
|
||||||
var filenames = new ArrayList<Arguments>();
|
List<Arguments> filenames = new ArrayList<>();
|
||||||
|
|
||||||
try (
|
try (
|
||||||
InputStream in = Objects.requireNonNull(JsonParserTest.class.getResource("/nst/JsonTestSuite")).openStream();
|
InputStream in = Objects.requireNonNull(JsonParserTest.class.getResource("/nst/JsonTestSuite")).openStream();
|
75
src/test/java/eu/jonahbauer/json/parser/JsonPathTest.java
Normal file
75
src/test/java/eu/jonahbauer/json/parser/JsonPathTest.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class JsonPathTest {
|
||||||
|
private final JsonValue array = new JsonArray(List.of(
|
||||||
|
new JsonNumber(1),
|
||||||
|
new JsonNumber(2),
|
||||||
|
new JsonObject(Map.of(
|
||||||
|
"bar", JsonBoolean.FALSE
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
private final JsonValue object = new JsonObject(Map.of(
|
||||||
|
"foo", array,
|
||||||
|
"bar", new JsonString("baz"),
|
||||||
|
"quack quack", new JsonString("foobar")
|
||||||
|
));;
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseEmptyShouldFail() {
|
||||||
|
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseIdentity() {
|
||||||
|
var path = JsonPath.parse(".");
|
||||||
|
assertEquals(object, path.select(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseArrayAccess() {
|
||||||
|
var path = JsonPath.parse(".[0]");
|
||||||
|
assertEquals(new JsonNumber(1), path.select(array));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseArrayAccessWithLeadingZero() {
|
||||||
|
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(".[01]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseObjectAccessWithIdentifier() {
|
||||||
|
var path = JsonPath.parse(".bar");
|
||||||
|
assertEquals(new JsonString("baz"), path.select(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseObjectAccessWithString() {
|
||||||
|
var path = JsonPath.parse(".\"quack quack\"");
|
||||||
|
assertEquals(new JsonString("foobar"), path.select(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseObjectAccessWithArraySyntax() {
|
||||||
|
var path = JsonPath.parse(".[\"quack quack\"]");
|
||||||
|
assertEquals(new JsonString("foobar"), path.select(object));
|
||||||
|
|
||||||
|
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(".[quack quack]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseChainedAccess() {
|
||||||
|
var path = JsonPath.parse(".foo[2].bar");
|
||||||
|
assertEquals(JsonBoolean.FALSE, path.select(object));
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package eu.jonahbauer.json;
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
import eu.jonahbauer.json.exceptions.JsonParserException;
|
import eu.jonahbauer.json.JsonObject;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import eu.jonahbauer.json.JsonProcessingException;
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.example.json.*;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
@ -16,15 +18,14 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static eu.jonahbauer.json.JsonTemplateProcessor.*;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
class JsonProcessorTest {
|
||||||
class JsonTemplateProcessorTest {
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@SuppressWarnings("StringOperationCanBeSimplified")
|
||||||
void processWithoutPlaceholders() {
|
void processWithoutPlaceholders() {
|
||||||
JsonValue value = JSON."""
|
JsonValue value = JsonProcessor.JSON."""
|
||||||
{
|
{
|
||||||
"string": "string",
|
"string": "string",
|
||||||
"number": 1337,
|
"number": 1337,
|
||||||
@ -47,8 +48,9 @@ class JsonTemplateProcessorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@SuppressWarnings("TrailingWhitespacesInTextBlock")
|
||||||
void processWithValuePlaceholder() {
|
void processWithValuePlaceholder() {
|
||||||
JsonValue value = JSON."""
|
JsonValue value = JsonProcessor.JSON."""
|
||||||
{
|
{
|
||||||
"string": \{"string"},
|
"string": \{"string"},
|
||||||
"number": \{1337},
|
"number": \{1337},
|
||||||
@ -73,7 +75,7 @@ class JsonTemplateProcessorTest {
|
|||||||
@Test
|
@Test
|
||||||
void processWithKeyPlaceholder() {
|
void processWithKeyPlaceholder() {
|
||||||
String key = "foo";
|
String key = "foo";
|
||||||
JsonValue value = JSON."""
|
JsonValue value = JsonProcessor.JSON."""
|
||||||
{
|
{
|
||||||
\{key}: "value"
|
\{key}: "value"
|
||||||
}
|
}
|
||||||
@ -87,15 +89,15 @@ class JsonTemplateProcessorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void processWithPlaceholderInInvalidPosition() {
|
void processWithPlaceholderInInvalidPosition() {
|
||||||
assertThrows(JsonParserException.class, () -> {
|
assertThrows(JsonProcessingException.class, () -> {
|
||||||
var _ = JSON."{\"key\" \{"value"}}";
|
var _ = JsonProcessor.JSON."{\"key\" \{"value"}}";
|
||||||
}).printStackTrace(System.out);
|
}).printStackTrace(System.out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void processWithPlaceholderInStringLiteral() {
|
void processWithPlaceholderInStringLiteral() {
|
||||||
Object object = new Object();
|
Object object = new Object();
|
||||||
JsonValue value = JSON."{\"key\": \"Hello \{object}!\"}";
|
JsonValue value = JsonProcessor.JSON."{\"key\": \"Hello \{object}!\"}";
|
||||||
assertEquals(JsonValue.valueOf(Map.of(
|
assertEquals(JsonValue.valueOf(Map.of(
|
||||||
"key", "Hello " + object + "!"
|
"key", "Hello " + object + "!"
|
||||||
)), value);
|
)), value);
|
||||||
@ -104,7 +106,7 @@ class JsonTemplateProcessorTest {
|
|||||||
@Test
|
@Test
|
||||||
void processWithPlaceholderInKeyStringLiteral() {
|
void processWithPlaceholderInKeyStringLiteral() {
|
||||||
Object object = new Object();
|
Object object = new Object();
|
||||||
JsonValue value = JSON."{\"key-\{object}\": \"value\"}";
|
JsonValue value = JsonProcessor.JSON."{\"key-\{object}\": \"value\"}";
|
||||||
assertEquals(JsonValue.valueOf(Map.of(
|
assertEquals(JsonValue.valueOf(Map.of(
|
||||||
"key-" + object, "value"
|
"key-" + object, "value"
|
||||||
)), value);
|
)), value);
|
||||||
@ -122,18 +124,18 @@ class JsonTemplateProcessorTest {
|
|||||||
default -> throw new IllegalArgumentException();
|
default -> throw new IllegalArgumentException();
|
||||||
};
|
};
|
||||||
|
|
||||||
try (var in = Objects.requireNonNull(JsonTemplateProcessor.class.getResourceAsStream(path))) {
|
try (var in = Objects.requireNonNull(JsonProcessorTest.class.getResourceAsStream(path))) {
|
||||||
String input = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
String input = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (expected == Boolean.FALSE) {
|
if (expected == Boolean.FALSE) {
|
||||||
assertThrows(JsonParserException.class, () -> JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
|
assertThrows(JsonProcessingException.class, () -> JsonProcessor.JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
|
||||||
} else if (expected == Boolean.TRUE) {
|
} else if (expected == Boolean.TRUE) {
|
||||||
Assertions.assertDoesNotThrow(() -> JSON.process(StringTemplate.of(input)));
|
Assertions.assertDoesNotThrow(() -> JsonProcessor.JSON.process(StringTemplate.of(input)));
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
JSON.process(StringTemplate.of(input));
|
JsonProcessor.JSON.process(StringTemplate.of(input));
|
||||||
System.out.println("accepted");
|
System.out.println("accepted");
|
||||||
} catch (JsonParserException ex) {
|
} catch (JsonProcessingException ex) {
|
||||||
System.out.println("rejected");
|
System.out.println("rejected");
|
||||||
ex.printStackTrace(System.out);
|
ex.printStackTrace(System.out);
|
||||||
}
|
}
|
||||||
@ -141,11 +143,11 @@ class JsonTemplateProcessorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static @NotNull Stream<@NotNull Arguments> parameters() throws IOException {
|
static Stream<Arguments> parameters() throws IOException {
|
||||||
var filenames = new ArrayList<Arguments>();
|
List<Arguments> filenames = new ArrayList<>();
|
||||||
|
|
||||||
try (
|
try (
|
||||||
InputStream in = Objects.requireNonNull(JsonTemplateProcessorTest.class.getResource("/nst/JsonTestSuite")).openStream();
|
InputStream in = Objects.requireNonNull(JsonProcessorTest.class.getResource("/nst/JsonTestSuite")).openStream();
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(in))
|
BufferedReader br = new BufferedReader(new InputStreamReader(in))
|
||||||
) {
|
) {
|
||||||
String resource;
|
String resource;
|
@ -0,0 +1,95 @@
|
|||||||
|
package eu.jonahbauer.json.parser.tokenizer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static java.lang.StringTemplate.RAW;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class JsonTokenizerImpl$TemplateReaderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readString() throws IOException {
|
||||||
|
var tokenizer = new TemplateReader("Hello World!");
|
||||||
|
assertEquals('H', tokenizer.read());
|
||||||
|
assertEquals('e', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals(' ', tokenizer.read());
|
||||||
|
assertEquals('W', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals('r', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('d', tokenizer.read());
|
||||||
|
assertEquals('!', tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readStringTemplate() throws IOException {
|
||||||
|
String name = "World";
|
||||||
|
var tokenizer = new TemplateReader(RAW."Hello \{name}!");
|
||||||
|
assertEquals('H', tokenizer.read());
|
||||||
|
assertEquals('e', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals(' ', tokenizer.read());
|
||||||
|
assertEquals(-2, tokenizer.read());
|
||||||
|
assertEquals(name, tokenizer.getObject());
|
||||||
|
assertEquals('!', tokenizer.read());
|
||||||
|
assertThrows(IllegalStateException.class, tokenizer::getObject);
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushbackString() throws IOException {
|
||||||
|
var tokenizer = new TemplateReader("Hello World!");
|
||||||
|
assertEquals('H', tokenizer.read());
|
||||||
|
assertEquals('e', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals(' ', tokenizer.read());
|
||||||
|
tokenizer.pushback();
|
||||||
|
assertEquals(' ', tokenizer.read());
|
||||||
|
assertEquals('W', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals('r', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
tokenizer.pushback();
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('d', tokenizer.read());
|
||||||
|
assertEquals('!', tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
tokenizer.pushback();
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushbackPlaceholder() throws IOException {
|
||||||
|
String name = "World";
|
||||||
|
var tokenizer = new TemplateReader(RAW."Hello \{name}!");
|
||||||
|
assertEquals('H', tokenizer.read());
|
||||||
|
assertEquals('e', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('l', tokenizer.read());
|
||||||
|
assertEquals('o', tokenizer.read());
|
||||||
|
assertEquals(' ', tokenizer.read());
|
||||||
|
assertEquals(-2, tokenizer.read());
|
||||||
|
assertEquals(name, tokenizer.getObject());
|
||||||
|
tokenizer.pushback();
|
||||||
|
assertEquals(-2, tokenizer.read());
|
||||||
|
assertEquals(name, tokenizer.getObject());
|
||||||
|
assertEquals('!', tokenizer.read());
|
||||||
|
assertThrows(IllegalStateException.class, tokenizer::getObject);
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
assertEquals(-1, tokenizer.read());
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
package eu.jonahbauer.json.tokenizer;
|
package eu.jonahbauer.json.parser.tokenizer;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.token.JsonPunctuation;
|
||||||
|
import eu.jonahbauer.json.token.JsonStringTemplate;
|
||||||
import eu.jonahbauer.json.JsonBoolean;
|
import eu.jonahbauer.json.JsonBoolean;
|
||||||
|
import eu.jonahbauer.json.token.JsonNull;
|
||||||
import eu.jonahbauer.json.JsonNumber;
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
import eu.jonahbauer.json.JsonString;
|
import eu.jonahbauer.json.JsonString;
|
||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
import eu.jonahbauer.json.parser.JsonTokenizerException;
|
||||||
import eu.jonahbauer.json.tokenizer.token.*;
|
import eu.jonahbauer.json.token.JsonToken;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@ -115,26 +118,17 @@ class JsonTokenizerImplTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("preview")
|
|
||||||
void stringTemplate() {
|
void stringTemplate() {
|
||||||
var name = "World";
|
var name = "World";
|
||||||
test(RAW."\"Hello, \{name}!\"", new JsonStringTemplate(RAW."Hello, \{name}!"));
|
test(RAW."\"Hello, \{name}!\"", new JsonStringTemplate(RAW."Hello, \{name}!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
void placeholder() {
|
|
||||||
var name = "World";
|
|
||||||
test(RAW."\{name}", new JsonPlaceholder(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void test(@NotNull String json, @NotNull JsonToken @NotNull... expected) {
|
private void test(@NotNull String json, @NotNull JsonToken @NotNull... expected) {
|
||||||
var tokenizer = new JsonTokenizerImpl(json);
|
var tokenizer = new JsonTokenizerImpl(json);
|
||||||
var actual = tokenizer.stream().toList();
|
var actual = tokenizer.stream().toList();
|
||||||
assertEquals(List.of(expected), actual);
|
assertEquals(List.of(expected), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("preview")
|
|
||||||
private void test(@NotNull StringTemplate json, @NotNull JsonToken @NotNull... expected) {
|
private void test(@NotNull StringTemplate json, @NotNull JsonToken @NotNull... expected) {
|
||||||
var tokenizer = new JsonTokenizerImpl(json);
|
var tokenizer = new JsonTokenizerImpl(json);
|
||||||
var actual = tokenizer.stream().toList();
|
var actual = tokenizer.stream().toList();
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
["譌・ム淫"]
|
@ -0,0 +1 @@
|
|||||||
|
["<22><><EFBFBD>"]
|
@ -0,0 +1 @@
|
|||||||
|
["<22>"]
|
@ -0,0 +1 @@
|
|||||||
|
["И"]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user