diff --git a/build.gradle b/build.gradle index 1e4ad26..d944924 100644 --- a/build.gradle +++ b/build.gradle @@ -28,9 +28,9 @@ repositories { } dependencies { - implementation('dev.latvian.apps:ansi:1.0.0-build.1') - implementation('dev.latvian.apps:tiny-java-server:1.0.0-build.7') + compileOnly('dev.latvian.apps:tiny-java-server:1.0.0-build.7') compileOnly('org.jetbrains:annotations:24.0.1') + testImplementation('dev.latvian.apps:ansi:1.0.0-build.1') testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0') testImplementation('junit:junit:4.13.2') diff --git a/src/main/java/dev/latvian/apps/json/JSON.java b/src/main/java/dev/latvian/apps/json/JSON.java index 2648272..29380a0 100644 --- a/src/main/java/dev/latvian/apps/json/JSON.java +++ b/src/main/java/dev/latvian/apps/json/JSON.java @@ -1,6 +1,7 @@ package dev.latvian.apps.json; import dev.latvian.apps.json.adapter.JSONAdapter; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -15,14 +16,10 @@ import java.nio.file.Path; import java.time.Instant; import java.util.Arrays; -import java.util.Collection; import java.util.Date; -import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.function.Function; @@ -297,11 +294,12 @@ public JSON child() { return new JSON(this); } + @ApiStatus.Internal @SuppressWarnings("unchecked") public T adapt(Object value, Type genericType) { var t = genericType == null ? null : JSONAdapter.getRawType(genericType); - if (value == null) { + if (value == null || value == NULL) { return t == Optional.class ? (T) Optional.empty() : null; } else if (t == null || t == Object.class || t.isInstance(value)) { return (T) value; @@ -333,22 +331,8 @@ public T adapt(Object value, Type genericType) { return (T) Float.valueOf(((Number) value).floatValue()); } else if (t == Double.class || t == Double.TYPE) { return (T) Double.valueOf(((Number) value).doubleValue()); - } else if (t == Map.class || t == JSONObject.class) { + } else if (t == JSONObject.class || t == JSONArray.class) { return (T) value; - } else if (t == List.class || t == Collection.class || t == Iterable.class || t == JSONArray.class) { - return (T) value; - } else if (t == Set.class) { - return (T) new HashSet<>((Collection) value); - } else if (t.isEnum()) { - var str = String.valueOf(value); - - for (var e : t.getEnumConstants()) { - if (e.toString().equalsIgnoreCase(str)) { - return (T) e; - } - } - - throw new IllegalArgumentException("Unknown enum constant: " + str); } else { return (T) getAdapter(t).adapt(this, value, genericType); } diff --git a/src/main/java/dev/latvian/apps/json/adapter/ArrayJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/ArrayJSONAdapter.java index 7cc9ca4..9b8ad42 100644 --- a/src/main/java/dev/latvian/apps/json/adapter/ArrayJSONAdapter.java +++ b/src/main/java/dev/latvian/apps/json/adapter/ArrayJSONAdapter.java @@ -7,7 +7,6 @@ import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.Type; -import java.util.Collection; public class ArrayJSONAdapter implements JSONAdapter { private final Class component; @@ -20,20 +19,8 @@ public class ArrayJSONAdapter implements JSONAdapter { @Override public Object adapt(JSON json, Object jsonValue, Type genericType) { - if (jsonValue instanceof Iterable itr) { - int size; - - if (itr instanceof Collection c) { - size = c.size(); - } else { - size = 0; - - for (var ignored : itr) { - size++; - } - } - - if (size == 0) { + if (jsonValue instanceof JSONArray jsonArray) { + if (jsonArray.isEmpty()) { if (emptyArray == null) { emptyArray = Array.newInstance(component, 0); } @@ -41,19 +28,19 @@ public Object adapt(JSON json, Object jsonValue, Type genericType) { return emptyArray; } - var arr = Array.newInstance(component, size); + var arr = Array.newInstance(component, jsonArray.size()); int i = 0; - for (var value : itr) { + for (var value : jsonArray) { Array.set(arr, i, json.adapt(value, component)); i++; } return arr; - } else { - throw new IllegalArgumentException("Expected collection, got " + jsonValue.getClass().getName()); } + + throw new IllegalArgumentException("Expected JSON array for array of '" + component.getName() + "'"); } @Override diff --git a/src/main/java/dev/latvian/apps/json/adapter/CollectionJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/CollectionJSONAdapter.java new file mode 100644 index 0000000..b651cf8 --- /dev/null +++ b/src/main/java/dev/latvian/apps/json/adapter/CollectionJSONAdapter.java @@ -0,0 +1,45 @@ +package dev.latvian.apps.json.adapter; + +import dev.latvian.apps.json.JSON; +import dev.latvian.apps.json.JSONArray; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; + +public class CollectionJSONAdapter implements JSONAdapter> { + public interface Factory { + Factory LIST = ArrayList::new; + Factory LINKED_LIST = s -> new LinkedList<>(); + Factory LINKED_SET = LinkedHashSet::new; + + Collection create(int size); + } + + private final Class type; + private final Factory factory; + + CollectionJSONAdapter(Class type, Factory factory) { + this.type = type; + this.factory = factory; + } + + @Override + public Iterable adapt(JSON json, Object jsonValue, Type genericType) { + if (jsonValue instanceof JSONArray jsonArray) { + var collection = factory.create(jsonArray.size()); + var valueType = genericType instanceof ParameterizedType pt ? pt.getActualTypeArguments()[0] : null; + + for (var value : jsonArray) { + collection.add(json.adapt(value, valueType)); + } + + return collection; + } + + throw new IllegalArgumentException("Expected JSON array for type '" + type.getName() + "'"); + } +} diff --git a/src/main/java/dev/latvian/apps/json/adapter/EnumJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/EnumJSONAdapter.java index f99480e..e8b5601 100644 --- a/src/main/java/dev/latvian/apps/json/adapter/EnumJSONAdapter.java +++ b/src/main/java/dev/latvian/apps/json/adapter/EnumJSONAdapter.java @@ -8,6 +8,10 @@ import java.util.Locale; public class EnumJSONAdapter implements JSONAdapter { + public interface CustomName { + String getJSONName(); + } + private record EnumValue(int index, String name, Object value) { } @@ -24,7 +28,7 @@ private EnumValue[] values() { values = new EnumValue[c.length]; for (int i = 0; i < c.length; i++) { - values[i] = new EnumValue(i, ((Enum) c[i]).name().toLowerCase(Locale.ROOT), c[i]); + values[i] = new EnumValue(i, c[i] instanceof CustomName cn ? cn.getJSONName() : ((Enum) c[i]).name().toLowerCase(Locale.ROOT), c[i]); } } @@ -33,6 +37,16 @@ private EnumValue[] values() { @Override public Object adapt(JSON json, Object jsonValue, Type genericType) { + if (jsonValue instanceof Number n) { + int i = n.intValue(); + + if (i >= 0 && i < values().length) { + return values()[i].value; + } else { + throw new IndexOutOfBoundsException("Index out of bounds: " + i); + } + } + var str = String.valueOf(jsonValue); for (var val : values()) { @@ -41,18 +55,11 @@ public Object adapt(JSON json, Object jsonValue, Type genericType) { } } - throw new NullPointerException("Eunm value '" + str + "' not found"); + throw new NullPointerException("Unknown enum constant: " + str); } @Override public void write(JSON json, Writer writer, Object value, int depth, boolean pretty) throws IOException { - for (var val : values()) { - if (val.value == value) { - json.write(writer, val.name, depth, pretty); - return; - } - } - - json.write(writer, "", depth, pretty); + json.write(writer, value instanceof CustomName cn ? cn.getJSONName() : ((Enum) value).name().toLowerCase(Locale.ROOT), depth, pretty); } } diff --git a/src/main/java/dev/latvian/apps/json/adapter/JSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/JSONAdapter.java index 97c410f..934c859 100644 --- a/src/main/java/dev/latvian/apps/json/adapter/JSONAdapter.java +++ b/src/main/java/dev/latvian/apps/json/adapter/JSONAdapter.java @@ -8,11 +8,26 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SequencedCollection; +import java.util.SequencedSet; +import java.util.Set; public interface JSONAdapter { T adapt(JSON json, Object jsonValue, Type genericType); - void write(JSON json, Writer writer, T value, int depth, boolean pretty) throws IOException; + default void write(JSON json, Writer writer, T value, int depth, boolean pretty) throws IOException { + json.write(writer, value, depth, pretty); + } static Class getRawType(Type type) { return switch (type) { @@ -24,15 +39,25 @@ static Class getRawType(Type type) { }; } - static JSONAdapter create(Class type) { - if (type.isArray()) { - return new ArrayJSONAdapter(type.getComponentType()); - } else if (type.isRecord()) { - return new RecordJSONAdapter(type); - } else if (type.isEnum()) { - return new EnumJSONAdapter(type); + static JSONAdapter create(Class t) { + if (t == IdentityHashMap.class) { + return new MapJSONAdapter(t, MapJSONAdapter.Factory.IDENTITY_MAP); + } else if (t == Map.class || t == HashMap.class || t == LinkedHashMap.class) { + return new MapJSONAdapter(t, MapJSONAdapter.Factory.MAP); + } else if (t == Set.class || t == SequencedSet.class || t == HashSet.class || t == LinkedHashSet.class) { + return new CollectionJSONAdapter(t, CollectionJSONAdapter.Factory.LINKED_SET); + } else if (t == LinkedList.class) { + return new CollectionJSONAdapter(t, CollectionJSONAdapter.Factory.LINKED_LIST); + } else if (t == List.class || t == ArrayList.class || t == Collection.class || t == Iterable.class || t == SequencedCollection.class) { + return new CollectionJSONAdapter(t, CollectionJSONAdapter.Factory.LIST); + } else if (t.isArray()) { + return new ArrayJSONAdapter(t.getComponentType()); + } else if (t.isRecord()) { + return new RecordJSONAdapter(t); + } else if (t.isEnum()) { + return new EnumJSONAdapter(t); } else { - return new ReflectionJSONAdapter(type); + return new ReflectionJSONAdapter(t); } } } diff --git a/src/main/java/dev/latvian/apps/json/adapter/MapJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/MapJSONAdapter.java new file mode 100644 index 0000000..9fb3856 --- /dev/null +++ b/src/main/java/dev/latvian/apps/json/adapter/MapJSONAdapter.java @@ -0,0 +1,43 @@ +package dev.latvian.apps.json.adapter; + +import dev.latvian.apps.json.JSON; +import dev.latvian.apps.json.JSONObject; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class MapJSONAdapter implements JSONAdapter> { + public interface Factory { + Factory MAP = LinkedHashMap::new; + Factory IDENTITY_MAP = IdentityHashMap::new; + + Map create(int size); + } + + private final Class type; + private final Factory factory; + + MapJSONAdapter(Class type, Factory factory) { + this.type = type; + this.factory = factory; + } + + @Override + public Map adapt(JSON json, Object jsonValue, Type genericType) { + if (jsonValue instanceof JSONObject jsonObject) { + var map = factory.create(jsonObject.size()); + var valueType = genericType instanceof ParameterizedType pt ? pt.getActualTypeArguments()[1] : null; + + for (var entry : jsonObject.entrySet()) { + map.put(entry.getKey(), json.adapt(entry.getValue(), valueType)); + } + + return map; + } + + throw new IllegalArgumentException("Expected JSON object for type '" + type.getName() + "'"); + } +} diff --git a/src/main/java/dev/latvian/apps/json/adapter/RecordJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/RecordJSONAdapter.java index 25372ce..9c1cc13 100644 --- a/src/main/java/dev/latvian/apps/json/adapter/RecordJSONAdapter.java +++ b/src/main/java/dev/latvian/apps/json/adapter/RecordJSONAdapter.java @@ -10,7 +10,6 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; -import java.util.Map; import java.util.Optional; public class RecordJSONAdapter implements JSONAdapter { @@ -47,7 +46,7 @@ private JSONRecordComponent[] components() { @Override public Object adapt(JSON json, Object jsonValue, Type genericType) { - if (jsonValue instanceof Map object) { + if (jsonValue instanceof JSONObject jsonObject) { var components = components(); if (constructor == null) { @@ -62,7 +61,7 @@ public Object adapt(JSON json, Object jsonValue, Type genericType) { var args = new Object[components.length]; for (int i = 0; i < args.length; i++) { - var j = object.get(components[i].name); + var j = jsonObject.get(components[i].name); if (j != null && j != JSON.NULL) { if (components[i].optional) { @@ -97,7 +96,7 @@ public void write(JSON json, Writer writer, Object value, int depth, boolean pre if (op.isPresent()) { obj.put(rc.name, op.get()); } - } else { + } else if (o != null) { obj.put(rc.name, o); } } catch (Exception ex) { diff --git a/src/main/java/dev/latvian/apps/json/adapter/ReflectionJSONAdapter.java b/src/main/java/dev/latvian/apps/json/adapter/ReflectionJSONAdapter.java index ee20bc9..2146873 100644 --- a/src/main/java/dev/latvian/apps/json/adapter/ReflectionJSONAdapter.java +++ b/src/main/java/dev/latvian/apps/json/adapter/ReflectionJSONAdapter.java @@ -1,6 +1,7 @@ package dev.latvian.apps.json.adapter; import dev.latvian.apps.json.JSON; +import dev.latvian.apps.json.JSONObject; import java.io.IOException; import java.io.Writer; @@ -10,6 +11,7 @@ import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; public class ReflectionJSONAdapter implements JSONAdapter { private final Class type; @@ -40,7 +42,7 @@ private Map fields() { @Override public Object adapt(JSON json, Object jsonValue, Type genericType) { - if (jsonValue instanceof Map object) { + if (jsonValue instanceof JSONObject jsonObject) { if (constructor == null) { try { constructor = type.getDeclaredConstructor(); @@ -53,9 +55,9 @@ public Object adapt(JSON json, Object jsonValue, Type genericType) { try { var o = constructor.newInstance(); - for (var entry : object.entrySet()) { + for (var entry : jsonObject.entrySet()) { if (entry.getValue() != null && entry.getValue() != JSON.NULL) { - var f = fields().get(String.valueOf(entry.getKey())); + var f = fields().get(entry.getKey()); if (f != null) { f.set(o, json.adapt(entry.getValue(), f.getType())); @@ -74,6 +76,25 @@ public Object adapt(JSON json, Object jsonValue, Type genericType) { @Override public void write(JSON json, Writer writer, Object value, int depth, boolean pretty) throws IOException { - throw new UnsupportedOperationException("Reflection JSON for type '" + type.getName() + "' not supported yet"); + var fields = fields(); + var obj = JSONObject.of(fields.size()); + + for (var field : fields.entrySet()) { + try { + var o = field.getValue().get(value); + + if (o instanceof Optional op) { + if (op.isPresent()) { + obj.put(field.getKey(), op.get()); + } + } else if (o != null) { + obj.put(field.getKey(), o); + } + } catch (Exception ex) { + throw new RuntimeException("Failed to access field '" + field.getKey() + "'", ex); + } + } + + json.write(writer, obj, depth, pretty); } }