From 7b9aebf6b6ba34a4821b553d202b25f5ec646013 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Tue, 30 Jan 2024 15:35:59 +0100 Subject: [PATCH] Added records to the JSONCodec class --- Signed-off-by: Peter Kriens Signed-off-by: Peter Kriens --- aQute.libg/src/aQute/lib/json/JSONCodec.java | 2 + .../src/aQute/lib/json/RecordHandler.java | 163 ++++++++++++++++++ aQute.libg/src/aQute/lib/json/packageinfo | 2 +- aQute.libg/test/aQute/lib/json/JSONTest.java | 27 ++- 4 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 aQute.libg/src/aQute/lib/json/RecordHandler.java diff --git a/aQute.libg/src/aQute/lib/json/JSONCodec.java b/aQute.libg/src/aQute/lib/json/JSONCodec.java index 0a85007e83..7aed0863d5 100644 --- a/aQute.libg/src/aQute/lib/json/JSONCodec.java +++ b/aQute.libg/src/aQute/lib/json/JSONCodec.java @@ -180,6 +180,8 @@ else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map h = new MapHandler(clazz, Object.class, Object.class); else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive()) h = new NumberHandler(clazz); + else if (Record.class.isAssignableFrom(clazz)) + h = new RecordHandler(this, clazz); else { Method valueOf = null; Constructor constructor = null; diff --git a/aQute.libg/src/aQute/lib/json/RecordHandler.java b/aQute.libg/src/aQute/lib/json/RecordHandler.java new file mode 100644 index 0000000000..a4ed11939b --- /dev/null +++ b/aQute.libg/src/aQute/lib/json/RecordHandler.java @@ -0,0 +1,163 @@ +package aQute.lib.json; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +import aQute.bnd.exceptions.Exceptions; + +public class RecordHandler extends Handler { + final static Lookup lookup = MethodHandles.lookup(); + + final Map accessors = new TreeMap<>(); + final JSONCodec codec; + final MethodHandle constructor; + + class Accessor { + + final MethodHandle getter; + final String name; + final Type type; + final int index; + + public Accessor(Method m, int index) throws IllegalAccessException { + getter = lookup.unreflect(m); + this.name = m.getName(); + this.type = m.getGenericReturnType(); + this.index = index; + } + + public Object get(Object object) { + try { + return getter.invoke(object); + } catch (Throwable e) { + throw Exceptions.duck(e); + } + } + + } + + RecordHandler(JSONCodec codec, Class c) throws Exception { + this.codec = codec; + assert c.getSuperclass() == Record.class; + MethodType constructorType = MethodType.methodType(void.class); + int index = 0; + for (Field f : c.getDeclaredFields()) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers) || !Modifier.isPrivate(modifiers)) + continue; + try { + String name = f.getName(); + if (name.startsWith("__") && !name.equals("__extra")) { + continue; + } + Method method = c.getMethod(name); + if (method == null || method.getReturnType() != f.getType()) + continue; + + constructorType = constructorType.appendParameterTypes(f.getType()); + + Accessor accessor = new Accessor(method, index++); + accessors.put(name, accessor); + + } catch (NoSuchMethodException nsme) { + // ignore + } + } + this.constructor = lookup.findConstructor(c, constructorType); + } + + @Override + public void encode(Encoder enc, Object object, Map visited) throws Exception { + enc.append("{"); + enc.indent(); + String del = ""; + for (Accessor a : accessors.values()) { + Object value = a.get(object); + if (value == null && codec.ignorenull) + continue; + + enc.append(del); + if (!del.isEmpty()) + enc.linebreak(); + + StringHandler.string(enc, a.name); + enc.append(":"); + enc.encode(value, a.type, visited); + del = ","; + } + enc.undent(); + enc.append("}"); + } + + @SuppressWarnings("unchecked") + @Override + public Object decodeObject(Decoder r) throws Exception { + assert r.current() == '{'; + @SuppressWarnings("unchecked") + Object[] args = new Object[accessors.size()]; + int c = r.next(); + while (JSONCodec.START_CHARACTERS.indexOf(c) >= 0) { + + // Get key + String key = r.codec.parseString(r); + + // Get separator + c = r.skipWs(); + if (c != ':') + throw new IllegalArgumentException("Expected ':' but got " + (char) c); + + c = r.next(); + + Accessor a = accessors.get(key); + Object decoded = r.codec.decode(a == null ? Object.class : a.type, r); + + if (a != null) + args[a.index] = decoded; + else { + Accessor extra = accessors.get("__extra"); + if (extra != null) { + Map values; + if (args[extra.index] == null) { + args[extra.index] = values = new LinkedHashMap<>(); + } else + values = (Map) args[extra.index]; + values.put(key, decoded); + } + } + + c = r.skipWs(); + + if (c == '}') + break; + + if (c == ',') { + c = r.next(); + continue; + } + + throw new IllegalArgumentException( + "Invalid character in parsing object, expected } or , but found " + (char) c); + } + assert r.current() == '}'; + r.read(); // skip closing + return create(args); + } + + private Object create(Object[] args) { + try { + return constructor.invokeWithArguments(args); + } catch (Throwable e) { + throw Exceptions.duck(e); + } + } + +} diff --git a/aQute.libg/src/aQute/lib/json/packageinfo b/aQute.libg/src/aQute/lib/json/packageinfo index ac4dc0f2f0..d1852f81ea 100644 --- a/aQute.libg/src/aQute/lib/json/packageinfo +++ b/aQute.libg/src/aQute/lib/json/packageinfo @@ -1 +1 @@ -version 3.4.0 +version 3.5.0 diff --git a/aQute.libg/test/aQute/lib/json/JSONTest.java b/aQute.libg/test/aQute/lib/json/JSONTest.java index 3e2482babe..d19dbcb282 100644 --- a/aQute.libg/test/aQute/lib/json/JSONTest.java +++ b/aQute.libg/test/aQute/lib/json/JSONTest.java @@ -1,5 +1,6 @@ package aQute.lib.json; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -709,13 +710,13 @@ public void testDecodeBasic(@InjectTemporaryDirectory .get(Float.class)); assertEquals((Character) '0', dec.from("48") .get(Character.class)); - assertEquals((Boolean) true, dec.from("48") + assertEquals(true, dec.from("48") .get(Boolean.class)); - assertEquals((Boolean) false, dec.from("0") + assertEquals(false, dec.from("0") .get(Boolean.class)); - assertEquals((Boolean) true, dec.from("48") + assertEquals(true, dec.from("48") .get(boolean.class)); - assertEquals((Boolean) false, dec.from("0") + assertEquals(false, dec.from("0") .get(boolean.class)); // String based @@ -918,7 +919,7 @@ public void testDecodeTypeA() throws Exception { Data1A d = dec .from("{\"b\":false,\"by\":-1,\"ch\":49,\"d\":3.0,\"f\":3.0,\"i\":1,\"l\":2,\"s\":\"abc\",\"sh\":-10}") .get(Data1A.class); - assertEquals((Boolean) false, d.b); + assertEquals(false, d.b); assertEquals((Byte) (byte) (-1), d.by); assertEquals((Character) '1', d.ch); assertEquals(3.0d, d.d); @@ -1226,4 +1227,20 @@ public void testMapInheritance() throws Exception { .toString(); assertEquals("{'foo':'bar'}".replace('\'', '"'), s); } + + @Test + public void testRecord() throws Exception { + record ARecord(int a, String b, long c) {} + ARecord a = new ARecord(1, "1", 1L); + assertThat(new JSONCodec().enc() + .put(a) + .toString()).isEqualTo("{\"a\":1,\"b\":\"1\",\"c\":1}"); + + ARecord x = new JSONCodec().dec() + .from("{\"a\":1,\"b\":\"1\",\"c\":1}") + .get(ARecord.class); + assertThat(x.a).isEqualTo(1); + assertThat(x.b).isEqualTo("1"); + assertThat(x.c).isEqualTo(1L); + } }