Skip to content

Commit

Permalink
Added records to the JSONCodec class
Browse files Browse the repository at this point in the history
---
 Signed-off-by: Peter Kriens <[email protected]>

Signed-off-by: Peter Kriens <[email protected]>
  • Loading branch information
pkriens committed Jan 30, 2024
1 parent 193dfc8 commit 7b9aebf
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 6 deletions.
2 changes: 2 additions & 0 deletions aQute.libg/src/aQute/lib/json/JSONCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
163 changes: 163 additions & 0 deletions aQute.libg/src/aQute/lib/json/RecordHandler.java
Original file line number Diff line number Diff line change
@@ -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<String, Accessor> 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<Object, Type> 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<String, Object> values;
if (args[extra.index] == null) {
args[extra.index] = values = new LinkedHashMap<>();
} else
values = (Map<String, Object>) 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);
}
}

}
2 changes: 1 addition & 1 deletion aQute.libg/src/aQute/lib/json/packageinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version 3.4.0
version 3.5.0
27 changes: 22 additions & 5 deletions aQute.libg/test/aQute/lib/json/JSONTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 7b9aebf

Please sign in to comment.