initial commit

This commit is contained in:
2025-04-12 16:27:57 +02:00
commit 3ee456bd8f
19 changed files with 1640 additions and 0 deletions

27
core/build.gradle.kts Normal file
View File

@@ -0,0 +1,27 @@
plugins {
`java-library`
}
group = "eu.jonahbauer"
version = "1.0-SNAPSHOT"
description = "json"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
}
}
dependencies {
api(libs.annotations)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
}
tasks {
withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("--enable-preview")
}
}

View File

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

View File

@@ -0,0 +1,46 @@
package eu.jonahbauer.json;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Java representation of a JSON boolean.
*/
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public enum JsonBoolean implements JsonValue {
TRUE(true),
FALSE(false),
;
/**
* 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;
}
/**
* 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) {
return bool == null ? null : valueOf((boolean) bool);
}
private final boolean value;
@Override
public @NotNull String toString() {
return String.valueOf(value);
}
}

View File

@@ -0,0 +1,63 @@
package eu.jonahbauer.json;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Java representation of a JSON boolean. Note, that JSON does not distinguish between integers and floating point
* numbers and therefore all numbers are stored as {@code double}.
*/
public record JsonNumber(double value) implements JsonValue {
public JsonNumber {
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
}
/**
* 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);
}
/**
* 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) {
if ((long) (double) l != 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.
* @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) {
return new JsonNumber(d);
}
/**
* 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 new JSON number
*/
@Contract("null -> null; !null -> !null")
public static @Nullable JsonNumber valueOf(@Nullable Number number) {
return number == null ? null : new JsonNumber(number.doubleValue());
}
@Override
public @NotNull String toString() {
return (long) value == value ? Long.toString((long) value) : Double.toString(value);
}
}

View File

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

View File

@@ -0,0 +1,127 @@
package eu.jonahbauer.json;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.stream.IntStream;
@SuppressWarnings("unused")
public record JsonString(@NotNull String value) implements JsonValue, CharSequence {
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) {
case JsonString json -> json;
case null -> null;
default -> {
@SuppressWarnings("java:S2259")
var result = new JsonString(chars.toString());
yield result;
}
};
}
/**
* Quotes the given {@link String} and encodes all invalid characters as an escape sequence.
* @param value a string value
* @return a JSON representation of the given {@code value}
*/
public static @NotNull String quote(@NotNull String value) {
StringBuilder out = new StringBuilder(value.length() + 2);
out.append('"');
for (int i = 0, length = value.length(); i < length; i++) {
char chr = value.charAt(i);
switch (chr) {
case '"' -> out.append("\\\"");
case '\\' -> out.append("\\\\");
case '\b' -> out.append("\\b");
case '\f' -> out.append("\\f");
case '\n' -> out.append("\\n");
case '\r' -> out.append("\\r");
case '\t' -> out.append("\\t");
default -> {
if (chr < 32) {
out.append("\\u%04x".formatted((int) chr));
} else {
out.append(chr);
}
}
}
}
out.append('"');
return out.toString();
}
//<editor-fold desc="CharSequence" defaultstate="collapsed">
@Override
public int length() {
return value.length();
}
@Override
public char charAt(int index) {
return value.charAt(index);
}
@Override
public @NotNull JsonString subSequence(int start, int end) {
return new JsonString(value.substring(start, end));
}
@Override
public boolean isEmpty() {
return value.isEmpty();
}
@Override
public @NotNull IntStream chars() {
return value.chars();
}
@Override
public @NotNull IntStream codePoints() {
return value.codePoints();
}
@Override
public @NotNull String toString() {
return value;
}
//</editor-fold>
@Override
public @NotNull String toJsonString() {
return quote(value());
}
}

View File

@@ -0,0 +1,83 @@
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.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();
}
/**
* {@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();
}
/**
* 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,69 @@
package eu.jonahbauer.json;
import lombok.experimental.Delegate;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@UtilityClass
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<?> 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

@@ -0,0 +1,16 @@
package eu.jonahbauer.json.exceptions;
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);
}
public JsonConversionException(@NotNull String message, @NotNull Throwable cause) {
super(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,7 @@
module eu.jonahbauer.json {
requires static lombok;
requires static org.jetbrains.annotations;
exports eu.jonahbauer.json;
exports eu.jonahbauer.json.exceptions;
}