Compare commits

..

No commits in common. "5a5d1492a57199474a143eeb4d28f9061465afd7" and "4d9a3ef4abd27d67a1fc8f2b9b636e6f8e449ae3" have entirely different histories.

394 changed files with 1961 additions and 1909 deletions

View File

@ -1,2 +1,27 @@
# 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!"
// }
```

View File

@ -8,20 +8,18 @@ description = "json"
java {
toolchain {
version = 21
languageVersion = JavaLanguageVersion.of(22)
}
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
api(libs.annotations)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
testImplementation(libs.bundles.junit)
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)
}
tasks {

View File

@ -0,0 +1,409 @@
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();
}
}

View File

@ -1,10 +1,9 @@
package eu.jonahbauer.json;
import eu.jonahbauer.json.token.JsonTokenKind;
import eu.jonahbauer.json.tokenizer.token.JsonToken;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import eu.jonahbauer.json.token.JsonToken;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -15,24 +14,24 @@ import org.jetbrains.annotations.Nullable;
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public enum JsonBoolean implements JsonValue, JsonToken, JsonTokenKind {
public enum JsonBoolean implements JsonValue, JsonToken {
TRUE(true),
FALSE(false),
;
/**
* Creates a {@link JsonBoolean} wrapping the given {@code boolean} value.
* @param bool a {@code boolean} value
* @return a {@link JsonBoolean}
* Converts the given boolean to a JSON boolean.
* @param bool a boolean
* @return a JSON boolean
*/
public static @NotNull JsonBoolean valueOf(boolean bool) {
return bool ? TRUE : FALSE;
}
/**
* Creates a {@link JsonBoolean} wrapping the given {@link Boolean} value.
* @param bool a {@link Boolean} value
* @return a {@link JsonBoolean}
* Converts the given boolean to a JSON boolean.
* @param bool a boolean
* @return a JSON boolean
*/
@Contract("null -> null; !null -> !null")
public static @Nullable JsonBoolean valueOf(@Nullable Boolean bool) {
@ -45,9 +44,4 @@ public enum JsonBoolean implements JsonValue, JsonToken, JsonTokenKind {
public @NotNull String toString() {
return String.valueOf(value);
}
@Override
public @NotNull JsonBoolean getKind() {
return this;
}
}

View File

@ -1,8 +1,6 @@
package eu.jonahbauer.json;
import eu.jonahbauer.json.token.JsonTokenKind;
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
import eu.jonahbauer.json.token.JsonToken;
import eu.jonahbauer.json.tokenizer.token.JsonToken;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -12,36 +10,36 @@ import org.jetbrains.annotations.Nullable;
* numbers and therefore all numbers are stored as {@code double}.
*/
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 {
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
}
/**
* Creates a {@link JsonNumber} wrapping the given {@code int} value.
* @param i an {@code int} value
* @return a {@link JsonNumber}
* Converts the given int to a JSON number.
* @param i an int
* @return a new JSON number
*/
public static @NotNull JsonNumber valueOf(int i) {
return new JsonNumber(i);
}
/**
* Creates a {@link JsonNumber} wrapping the given {@code long} value.
* @param l a {@code long} value
* @return a {@link JsonNumber}
* Converts the given long to a JSON number.
* @param l a long
* @return a new JSON number
* @throws IllegalArgumentException if conversion from {@code long} to {@code double} is lossy
*/
public static @NotNull JsonNumber valueOf(long l) {
return new JsonNumber(checkLongRange(l));
if ((long) (double) l != l) {
throw new IllegalArgumentException("lossy conversion from long to double for value " + l);
}
return new JsonNumber(l);
}
/**
* Creates a {@link JsonNumber} wrapping the given {@code double} value.
* @param d an {@code double} value
* @return a {@link JsonNumber}
* Converts the given double to a JSON number.
* @param d a double
* @return a new JSON number
* @throws IllegalArgumentException if {@code d} is not {@linkplain Double#isFinite(double) finite}
*/
public static @NotNull JsonNumber valueOf(double d) {
@ -49,30 +47,18 @@ public record JsonNumber(double value) implements JsonValue, JsonToken {
}
/**
* Creates a {@link JsonNumber} wrapping {@code number}. {@code number} is converted to a {@code double} using
* {@link Number#doubleValue()}.
* Converts the given number to a JSON number using {@link Number#doubleValue()}. The conversion to a JSON number
* might be lossy.
* @param number a number
* @return a {@link JsonNumber}
* @return a new JSON number
*/
@Contract("null -> null; !null -> !null")
public static @Nullable JsonNumber valueOf(@Nullable Number number) {
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
public @NotNull String toString() {
return Double.toString(value);
}
@Override
public @NotNull JsonTokenKind getKind() {
return KIND;
return (long) value == value ? Long.toString((long) value) : Double.toString(value);
}
}

View File

@ -0,0 +1,382 @@
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();
}
}

View File

@ -1,8 +1,6 @@
package eu.jonahbauer.json;
import eu.jonahbauer.json.token.JsonTokenKind;
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
import eu.jonahbauer.json.token.JsonToken;
import eu.jonahbauer.json.tokenizer.token.JsonToken;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -10,13 +8,38 @@ import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.stream.IntStream;
@SuppressWarnings("unused")
public record JsonString(@NotNull String value) implements JsonValue, JsonToken, CharSequence {
public static final JsonTokenKind KIND = ComplexTokenKind.STRING;
public static final @NotNull JsonString EMPTY = new JsonString("");
public JsonString {
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")
public static @Nullable JsonString valueOf(@Nullable CharSequence chars) {
return switch (chars) {
@ -102,14 +125,4 @@ public record JsonString(@NotNull String value) implements JsonValue, JsonToken,
public @NotNull String toJsonString() {
return quote(value());
}
@Override
public @NotNull String toPrettyJsonString() {
return toJsonString();
}
@Override
public @NotNull JsonTokenKind getKind() {
return KIND;
}
}

View File

@ -0,0 +1,37 @@
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);
}
}

View File

@ -0,0 +1,146 @@
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");
};
}
}

View File

@ -0,0 +1,72 @@
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> {}
}

View File

@ -1,8 +1,10 @@
package eu.jonahbauer.json.parser;
package eu.jonahbauer.json.exceptions;
import eu.jonahbauer.json.JsonProcessingException;
import org.jetbrains.annotations.NotNull;
/**
* An exception that indicates problems with conversion between java objects and json.
*/
public class JsonConversionException extends JsonProcessingException {
public JsonConversionException(@NotNull String message) {
super(message);

View File

@ -0,0 +1,14 @@
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);
}
}

View File

@ -0,0 +1,14 @@
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);
}
}

View File

@ -0,0 +1,34 @@
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();
}
}
}

View File

@ -1,9 +1,9 @@
package eu.jonahbauer.json.parser;
package eu.jonahbauer.json.exceptions;
import eu.jonahbauer.json.JsonProcessingException;
import org.jetbrains.annotations.NotNull;
public class JsonTokenizerException extends JsonProcessingException {
public class JsonTokenizerException extends JsonReaderException {
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message) {
super(fragment, line, column, message);
}

View File

@ -0,0 +1,273 @@
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;
}
}
}

View File

@ -0,0 +1,89 @@
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);
}
}

View File

@ -1,40 +1,52 @@
package eu.jonahbauer.json.parser.tokenizer;
package eu.jonahbauer.json.tokenizer;
import eu.jonahbauer.json.token.*;
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
import eu.jonahbauer.json.tokenizer.reader.PushbackReader;
import eu.jonahbauer.json.tokenizer.reader.PushbackReaderImpl;
import eu.jonahbauer.json.tokenizer.token.*;
import lombok.Getter;
import eu.jonahbauer.json.JsonBoolean;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.parser.JsonTokenizerException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.*;
import java.util.ArrayList;
import java.util.regex.Pattern;
public class JsonTokenizerImpl implements JsonTokenizer {
public final class JsonTokenizerImpl implements JsonTokenizer {
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?([eE][+-]?\\d+)?");
private final @NotNull TemplateReader reader;
private static final JsonPunctuation[] PUNCTUATION;
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];
@Getter
private int fragment;
@Getter
private int line;
private int lineNumber;
@Getter
private int column;
private int columnNumber;
public JsonTokenizerImpl(@NotNull String json) {
this.reader = new TemplateReader(json);
this(new StringReader(json));
}
@SuppressWarnings("preview")
public JsonTokenizerImpl(@NotNull StringTemplate template) {
this.reader = new PushbackReaderImpl(template);
}
public JsonTokenizerImpl(@NotNull Reader reader) {
this.reader = new TemplateReader(reader);
}
public JsonTokenizerImpl(@NotNull StringTemplate json) {
this.reader = new TemplateReader(json);
this.reader = new PushbackReaderImpl(reader);
}
@Override
@ -46,62 +58,40 @@ public class JsonTokenizerImpl implements JsonTokenizer {
} while (isWhitespace(chr));
fragment = reader.getFragment();
line = reader.getLine();
column = reader.getColumn();
lineNumber = reader.getLineNumber();
columnNumber = reader.getColumnNumber();
return switch (chr) {
case TemplateReader.EOF -> null;
case TemplateReader.PLACEHOLDER -> new JsonPlaceholder(reader.getObject());
case '{' -> JsonPunctuation.BEGIN_OBJECT;
case '}' -> JsonPunctuation.END_OBJECT;
case '[' -> JsonPunctuation.BEGIN_ARRAY;
case ']' -> JsonPunctuation.END_ARRAY;
case ',' -> JsonPunctuation.VALUE_SEPARATOR;
case ':' -> JsonPunctuation.NAME_SEPARATOR;
case '"' -> {
StringTemplate string = readString();
if (string.fragments().size() == 1) {
yield new JsonString(string.fragments().getFirst());
} else {
yield new JsonStringTemplate(string);
if (chr == PushbackReader.EOF) return null;
if (chr == PushbackReader.PLACEHOLDER) return new JsonPlaceholder(reader.getObject());
if (chr < 128 && PUNCTUATION[chr] != null) return PUNCTUATION[chr];
if (chr == '"') return nextString();
reader.pushback();
String token = nextToken();
return 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 -> {
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);
};
}
default -> throw error("invalid token: " + token);
};
}
@Override
public boolean hasTemplateSupport() {
return reader.isHasTemplateSupport();
}
protected boolean isWhitespace(int chr) {
private boolean isWhitespace(int chr) {
return chr == ' ' || chr == '\t' || chr == '\r' || chr == '\n';
}
protected boolean isPunctuation(int chr) {
private boolean isPunctuation(int chr) {
return chr == '{' || chr == '}' || chr == '[' || chr == ']' || chr == ',' || chr == ':' || chr == '"';
}
protected boolean isNumberLiteral(@NotNull String token) {
private boolean isNumberLiteral(@NotNull String token) {
return NUMBER_PATTERN.matcher(token).matches();
}
@ -120,20 +110,26 @@ public class JsonTokenizerImpl implements JsonTokenizer {
}
}
private @NotNull StringTemplate readString() throws IOException {
List<String> fragments = new ArrayList<>();
List<Object> values = new ArrayList<>();
private @NotNull JsonToken nextString() throws IOException {
var fragments = new ArrayList<String>();
var values = new ArrayList<>();
StringBuilder current = new StringBuilder();
var current = new StringBuilder();
while (true) {
int chr = reader.read();
if (chr == '"') {
fragments.add(current.toString());
return StringTemplate.of(fragments, values);
if (fragments.isEmpty()) {
return new JsonString(current.toString());
} else {
fragments.add(current.toString());
@SuppressWarnings("preview")
var result = new JsonStringTemplate(StringTemplate.of(fragments, values));
return result;
}
} else if (chr == '\\') {
chr = reader.read();
switch (chr) {
case TemplateReader.EOF -> throw error("incomplete escape sequence in string literal");
case PushbackReader.EOF -> throw error("incomplete escape sequence in string literal");
case '"' -> current.append('"');
case '\\' -> current.append('\\');
case '/' -> current.append('/');
@ -163,17 +159,17 @@ public class JsonTokenizerImpl implements JsonTokenizer {
} else if ('A' <= esc && esc <= 'F') {
code += 11 + (esc - 'A');
} else {
throw error(STR."invalid character \{toString(buffer[i])} in escape sequence");
throw error("invalid character " + toString(buffer[i]) + " in escape sequence");
}
}
current.append((char) code);
}
default -> throw error(STR."invalid character \{toString(chr)} in escape sequence");
default -> throw error("invalid character " + toString(chr) + " in escape sequence");
}
} else if (chr == TemplateReader.EOF) {
} else if (chr == PushbackReader.EOF) {
throw error("unclosed string literal");
} else if (chr == TemplateReader.PLACEHOLDER) {
} else if (chr == PushbackReader.PLACEHOLDER) {
fragments.add(current.toString());
current.setLength(0);
values.add(reader.getObject());
@ -186,17 +182,16 @@ public class JsonTokenizerImpl implements JsonTokenizer {
}
private @NotNull JsonTokenizerException error(@NotNull String message) {
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message);
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message);
}
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message, cause);
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message, cause);
}
private static @NotNull String toString(int chr) {
return switch (chr) {
case TemplateReader.EOF -> "EOF";
case TemplateReader.PLACEHOLDER -> "PLACEHOLDER";
case PushbackReader.EOF -> "EOF";
case '\t' -> "'\\t'";
case '\r' -> "'\\r'";
case '\n' -> "'\\n'";
@ -204,9 +199,9 @@ public class JsonTokenizerImpl implements JsonTokenizer {
case '\f' -> "'\\f'";
default -> {
if (Character.isISOControl((char) chr)) {
yield STR."0x\{Integer.toHexString(chr)}";
yield "0x" + Integer.toHexString(chr);
} else {
yield STR."'\{(char) chr}'";
yield "'" + (char) chr + "'";
}
}
};

View File

@ -0,0 +1,53 @@
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;
}

View File

@ -0,0 +1,158 @@
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 { }
}

View File

@ -0,0 +1,16 @@
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";
}
}

View File

@ -0,0 +1,6 @@
package eu.jonahbauer.json.tokenizer.token;
import org.jetbrains.annotations.Nullable;
public record JsonPlaceholder(@Nullable Object object) implements JsonToken {
}

View File

@ -1,4 +1,4 @@
package eu.jonahbauer.json.token;
package eu.jonahbauer.json.tokenizer.token;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@ -8,7 +8,7 @@ import org.jetbrains.annotations.NotNull;
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public enum JsonPunctuation implements JsonToken, JsonTokenKind {
public enum JsonPunctuation implements JsonToken {
BEGIN_OBJECT('{'),
END_OBJECT('}'),
BEGIN_ARRAY('['),
@ -19,11 +19,6 @@ public enum JsonPunctuation implements JsonToken, JsonTokenKind {
private final char value;
@Override
public @NotNull JsonTokenKind getKind() {
return this;
}
@Override
public @NotNull String toString() {
return name() + "(" + value + ")";

View File

@ -1,24 +1,17 @@
package eu.jonahbauer.json.token;
package eu.jonahbauer.json.tokenizer.token;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@SuppressWarnings("preview")
public record JsonStringTemplate(@NotNull StringTemplate template) implements JsonToken {
public static final JsonTokenKind KIND = ComplexTokenKind.STRING_TEMPLATE;
public JsonStringTemplate {
Objects.requireNonNull(template, "template");
Objects.requireNonNull(template);
}
public @NotNull JsonString asString() {
return new JsonString(STR.process(template));
}
@Override
public @NotNull JsonTokenKind getKind() {
return KIND;
}
}

View File

@ -0,0 +1,10 @@
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 { }

View File

@ -3,6 +3,8 @@ module eu.jonahbauer.json {
requires static org.jetbrains.annotations;
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.token;
}

View File

@ -1,9 +1,7 @@
package eu.jonahbauer.json.parser;
package eu.jonahbauer.json;
import eu.jonahbauer.json.JsonObject;
import eu.jonahbauer.json.JsonProcessingException;
import eu.jonahbauer.json.JsonValue;
import org.example.json.*;
import eu.jonahbauer.json.exceptions.JsonParserException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@ -18,14 +16,15 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;
import static eu.jonahbauer.json.JsonTemplateProcessor.*;
import static org.junit.jupiter.api.Assertions.*;
class JsonProcessorTest {
@SuppressWarnings("preview")
class JsonTemplateProcessorTest {
@Test
@SuppressWarnings("StringOperationCanBeSimplified")
void processWithoutPlaceholders() {
JsonValue value = JsonProcessor.JSON."""
JsonValue value = JSON."""
{
"string": "string",
"number": 1337,
@ -48,9 +47,8 @@ class JsonProcessorTest {
}
@Test
@SuppressWarnings("TrailingWhitespacesInTextBlock")
void processWithValuePlaceholder() {
JsonValue value = JsonProcessor.JSON."""
JsonValue value = JSON."""
{
"string": \{"string"},
"number": \{1337},
@ -75,7 +73,7 @@ class JsonProcessorTest {
@Test
void processWithKeyPlaceholder() {
String key = "foo";
JsonValue value = JsonProcessor.JSON."""
JsonValue value = JSON."""
{
\{key}: "value"
}
@ -89,15 +87,15 @@ class JsonProcessorTest {
@Test
void processWithPlaceholderInInvalidPosition() {
assertThrows(JsonProcessingException.class, () -> {
var _ = JsonProcessor.JSON."{\"key\" \{"value"}}";
assertThrows(JsonParserException.class, () -> {
var _ = JSON."{\"key\" \{"value"}}";
}).printStackTrace(System.out);
}
@Test
void processWithPlaceholderInStringLiteral() {
Object object = new Object();
JsonValue value = JsonProcessor.JSON."{\"key\": \"Hello \{object}!\"}";
JsonValue value = JSON."{\"key\": \"Hello \{object}!\"}";
assertEquals(JsonValue.valueOf(Map.of(
"key", "Hello " + object + "!"
)), value);
@ -106,7 +104,7 @@ class JsonProcessorTest {
@Test
void processWithPlaceholderInKeyStringLiteral() {
Object object = new Object();
JsonValue value = JsonProcessor.JSON."{\"key-\{object}\": \"value\"}";
JsonValue value = JSON."{\"key-\{object}\": \"value\"}";
assertEquals(JsonValue.valueOf(Map.of(
"key-" + object, "value"
)), value);
@ -124,18 +122,18 @@ class JsonProcessorTest {
default -> throw new IllegalArgumentException();
};
try (var in = Objects.requireNonNull(JsonProcessorTest.class.getResourceAsStream(path))) {
try (var in = Objects.requireNonNull(JsonTemplateProcessor.class.getResourceAsStream(path))) {
String input = new String(in.readAllBytes(), StandardCharsets.UTF_8);
if (expected == Boolean.FALSE) {
assertThrows(JsonProcessingException.class, () -> JsonProcessor.JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
assertThrows(JsonParserException.class, () -> JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
} else if (expected == Boolean.TRUE) {
Assertions.assertDoesNotThrow(() -> JsonProcessor.JSON.process(StringTemplate.of(input)));
Assertions.assertDoesNotThrow(() -> JSON.process(StringTemplate.of(input)));
} else {
try {
JsonProcessor.JSON.process(StringTemplate.of(input));
JSON.process(StringTemplate.of(input));
System.out.println("accepted");
} catch (JsonProcessingException ex) {
} catch (JsonParserException ex) {
System.out.println("rejected");
ex.printStackTrace(System.out);
}
@ -143,11 +141,11 @@ class JsonProcessorTest {
}
}
static Stream<Arguments> parameters() throws IOException {
List<Arguments> filenames = new ArrayList<>();
static @NotNull Stream<@NotNull Arguments> parameters() throws IOException {
var filenames = new ArrayList<Arguments>();
try (
InputStream in = Objects.requireNonNull(JsonProcessorTest.class.getResource("/nst/JsonTestSuite")).openStream();
InputStream in = Objects.requireNonNull(JsonTemplateProcessorTest.class.getResource("/nst/JsonTestSuite")).openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in))
) {
String resource;

View File

@ -1,6 +1,7 @@
package eu.jonahbauer.json.parser;
import eu.jonahbauer.json.JsonProcessingException;
import eu.jonahbauer.json.exceptions.JsonParserException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -10,7 +11,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@ -18,9 +18,10 @@ import static org.junit.jupiter.api.Assertions.*;
class JsonParserTest {
// https://github.com/nst/JSONTestSuite
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void suite(String name) throws IOException {
void suite(@NotNull String name) throws IOException {
var path = "/nst/JsonTestSuite/" + name;
Boolean expected = switch (name.charAt(0)) {
case 'i' -> null;
@ -36,14 +37,14 @@ class JsonParserTest {
var parser = new JsonParser(reader);
if (expected == Boolean.FALSE) {
assertThrows(JsonProcessingException.class, parser::parse).printStackTrace(System.out);
assertThrows(JsonParserException.class, parser::parse).printStackTrace(System.out);
} else if (expected == Boolean.TRUE) {
assertDoesNotThrow(parser::parse);
} else {
try {
parser.parse();
System.out.println("accepted");
} catch (JsonProcessingException ex) {
} catch (JsonParserException ex) {
System.out.println("rejected");
ex.printStackTrace(System.out);
}
@ -51,8 +52,8 @@ class JsonParserTest {
}
}
static Stream<Arguments> parameters() throws IOException {
List<Arguments> filenames = new ArrayList<>();
static @NotNull Stream<@NotNull Arguments> parameters() throws IOException {
var filenames = new ArrayList<Arguments>();
try (
InputStream in = Objects.requireNonNull(JsonParserTest.class.getResource("/nst/JsonTestSuite")).openStream();

View File

@ -1,13 +1,10 @@
package eu.jonahbauer.json.parser.tokenizer;
package eu.jonahbauer.json.tokenizer;
import eu.jonahbauer.json.token.JsonPunctuation;
import eu.jonahbauer.json.token.JsonStringTemplate;
import eu.jonahbauer.json.JsonBoolean;
import eu.jonahbauer.json.token.JsonNull;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.parser.JsonTokenizerException;
import eu.jonahbauer.json.token.JsonToken;
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
import eu.jonahbauer.json.tokenizer.token.*;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
@ -118,17 +115,26 @@ class JsonTokenizerImplTest {
}
@Test
@SuppressWarnings("preview")
void stringTemplate() {
var name = "World";
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) {
var tokenizer = new JsonTokenizerImpl(json);
var actual = tokenizer.stream().toList();
assertEquals(List.of(expected), actual);
}
@SuppressWarnings("preview")
private void test(@NotNull StringTemplate json, @NotNull JsonToken @NotNull... expected) {
var tokenizer = new JsonTokenizerImpl(json);
var actual = tokenizer.stream().toList();

View File

@ -0,0 +1 @@
["日ш<E697A5>"]

View File

@ -0,0 +1 @@
["<22>"]

View File

@ -0,0 +1 @@
["<22>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><>"]

View File

@ -0,0 +1 @@
[a<EFBFBD>]

View File

@ -0,0 +1 @@
[<EFBFBD>]

Some files were not shown because too many files have changed in this diff Show More