Skip to content

Commit

Permalink
Merge pull request #6054 from pkriens/feature/records-in-json
Browse files Browse the repository at this point in the history
Adding records to JSONCodec
  • Loading branch information
pkriens authored Mar 18, 2024
2 parents 104ad99 + 46457f7 commit 77fb48e
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 52 deletions.
62 changes: 39 additions & 23 deletions aQute.libg/src/aQute/lib/converter/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
Expand Down Expand Up @@ -355,34 +356,49 @@ Object convertT(Type type, Object o) throws Exception {
}

if (o instanceof Map<?, ?> map) {

String key = null;
try {
MethodHandle mh = publicLookup().findConstructor(resultType, methodType(void.class));
Object instance = mh.invoke();
for (Map.Entry e : map.entrySet()) {
key = (String) e.getKey();
try {
Field f = resultType.getField(key);
Object value = convert(f.getGenericType(), e.getValue());
mh = publicLookup().unreflectSetter(f);
if (isStatic(f)) {
mh.invoke(value);
} else {
mh.invoke(instance, value);
}
} catch (Exception ee) {
// We cannot find the key, so try the __extra field
mh = publicLookup().findGetter(resultType, "__extra", Map.class);
Map<String, Object> extra = (Map<String, Object>) mh.invoke(instance);
if (extra == null) {
extra = new HashMap<>();
mh = publicLookup().findSetter(resultType, "__extra", Map.class);
mh.invoke(instance, extra);
if (resultType.isRecord()) {
int length = resultType.getRecordComponents().length;
Object[] arguments = new Object[length];
MethodType constructorType = methodType(void.class);
for (int i = 0; i < length; i++) {
RecordComponent c = resultType.getRecordComponents()[i];
Object value = map.get(c.getName());
arguments[i] = cnv(c.getGenericType(), value);
constructorType = constructorType.appendParameterTypes(c.getType());
}
MethodHandle mh = publicLookup().findConstructor(resultType, constructorType);
return mh.invokeWithArguments(arguments);
} else {
MethodHandle mh = publicLookup().findConstructor(resultType, methodType(void.class));
Object instance = mh.invoke();
for (Map.Entry e : map.entrySet()) {
key = (String) e.getKey();
try {
Field f = resultType.getField(key);
Object value = convert(f.getGenericType(), e.getValue());
mh = publicLookup().unreflectSetter(f);
if (isStatic(f)) {
mh.invoke(value);
} else {
mh.invoke(instance, value);
}
} catch (Exception ee) {
// We cannot find the key, so try the __extra field
mh = publicLookup().findGetter(resultType, "__extra", Map.class);
Map<String, Object> extra = (Map<String, Object>) mh.invoke(instance);
if (extra == null) {
extra = new HashMap<>();
mh = publicLookup().findSetter(resultType, "__extra", Map.class);
mh.invoke(instance, extra);
}
extra.put(key, convert(Object.class, e.getValue()));
}
extra.put(key, convert(Object.class, e.getValue()));
}
return instance;
}
return instance;
} catch (Error e) {
throw e;
} catch (Throwable e) {
Expand Down
5 changes: 5 additions & 0 deletions aQute.libg/src/aQute/lib/converter/SelectType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package aQute.lib.converter;

public interface SelectType {
Object select(Object o);
}
2 changes: 1 addition & 1 deletion aQute.libg/src/aQute/lib/converter/packageinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version 2.4.0
version 2.5.0
36 changes: 34 additions & 2 deletions aQute.libg/src/aQute/lib/json/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class Decoder implements Closeable {
MessageDigest digest;
Map<String, Object> extra;
Charset encoding = UTF_8;
int line = 0, position = 0;

boolean strict;
boolean inflate;
Expand Down Expand Up @@ -144,6 +145,12 @@ int read() throws Exception {
digest.update((byte) (current / 256));
digest.update((byte) (current % 256));
}
if (current == '\n') {
line++;
position = 0;
} else {
position++;
}
return current;
}

Expand Down Expand Up @@ -173,9 +180,22 @@ int next() throws Exception {
}

void expect(String s) throws Exception {
for (int i = 0; i < s.length(); i++)
if (!(s.charAt(i) == read()))
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int r= read();
if ( r < 0)
fatal("Expected " + s + " but got something different");

if (!(c == r)) {
if ( codec.promiscuous) {
char a = Character.toLowerCase(c);
char b = Character.toLowerCase((char) r);
if (a == b)
continue;
}
throw new IllegalArgumentException("Expected " + s + " but got something different");
}
}
read();
}

Expand All @@ -201,4 +221,16 @@ public Decoder inflate() {
inflate = true;
return this;
}

public void badJSON(String reason) {
if (codec.promiscuous) {
codec.fishy.incrementAndGet();
return;
}
throw new IllegalArgumentException(line + "," + position + ": " + reason);
}

public void fatal(String message) {
throw new IllegalArgumentException(line + "," + position + ": " + message);
}
}
83 changes: 63 additions & 20 deletions aQute.libg/src/aQute/lib/json/JSONCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

/**
Expand Down Expand Up @@ -52,33 +53,38 @@
* Will now use hex for encoding byte arrays
*/
public class JSONCodec {
final static Set<String> keywords = Set.of("abstract", "assert", "boolean",
final static Set<String> keywords = Set.of("abstract", "assert", "boolean",
"break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else",
"enum", "exports", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import",
"instanceof", "int", "interface", "long", "module", "native", "new", "package", "private", "protected",
"public", "requires", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
"throw", "throws", "transient", "try", "var", "void", "volatile", "while", "true", "false", "null", "_",
"record", "sealed", "non-sealed", "permits");
public static final String KEYWORD_SUFFIX = "__";
public static final String KEYWORD_SUFFIX = "__";

final static String START_CHARACTERS = "[{\"-0123456789tfn";
final static String START_CHARACTERSX = "[{\"-0123456789tfn";
final static String START_CHARACTERS_BAD = START_CHARACTERSX + "'TF";

// Handlers
private final static WeakHashMap<Type, Handler> handlers = new WeakHashMap<>();
private final static WeakHashMap<Type, Handler> handlers = new WeakHashMap<>();

private static StringHandler sh = new StringHandler();
private static BooleanHandler bh = new BooleanHandler();
private static CharacterHandler ch = new CharacterHandler();
private static CollectionHandler dch = new CollectionHandler(ArrayList.class,
private static StringHandler sh = new StringHandler();
private static BooleanHandler bh = new BooleanHandler();
private static CharacterHandler ch = new CharacterHandler();
private static CollectionHandler dch = new CollectionHandler(ArrayList.class,
Object.class);
private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null);
private static DateHandler sdh = new DateHandler();
private static FileHandler fh = new FileHandler();
private static ByteArrayHandler byteh = new ByteArrayHandler();
private static UUIDHandler uuidh = new UUIDHandler();

private static SpecialHandler sph = new SpecialHandler(Pattern.class, null,
null);
private static DateHandler sdh = new DateHandler();
private static FileHandler fh = new FileHandler();
private static ByteArrayHandler byteh = new ByteArrayHandler();
private static UUIDHandler uuidh = new UUIDHandler();

final AtomicInteger fishy = new AtomicInteger();
boolean ignorenull;
Map<Type, Handler> localHandlers = new ConcurrentHashMap<>();
Map<Type, Handler> localHandlers = new ConcurrentHashMap<>();
boolean promiscuous;
String startCharacters = START_CHARACTERSX;

/**
* Create a new Encoder with the state and appropriate API.
Expand Down Expand Up @@ -284,17 +290,27 @@ Object decode(Type type, Decoder isr) throws Exception {
type = ArrayList.class;
break;

case '\'' :
isr.badJSON("Got a single quote ' when a double quote \" should be used");
return parseString(isr);

case '"' :
return parseString(isr);

case 'N' :
isr.badJSON("null must not use upper case, got a N");
case 'n' :
isr.expect("ull");
return null;

case 'T' :
isr.badJSON("booleans must not use upper case, got a T");
case 't' :
isr.expect("rue");
return true;

case 'F' :
isr.badJSON("booleans must not use upper case, got a F");
case 'f' :
isr.expect("alse");
return false;
Expand Down Expand Up @@ -326,18 +342,28 @@ Object decode(Type type, Decoder isr) throws Exception {
case '[' :
return h.decodeArray(isr);

case '\'' :
isr.badJSON("single quote is not allowed");
case '"' :
String string = parseString(isr);
return h.decode(isr, string);

case 'N' :
isr.badJSON("do not use upper case for null");
case 'n' :
isr.expect("ull");
return h.decode(isr);

case 'T' :
isr.badJSON("do not use upper case for booleans");
// fall through
case 't' :
isr.expect("rue");
return h.decode(isr, Boolean.TRUE);

case 'F' :
isr.badJSON("do not use upper case for booleans");

case 'f' :
isr.expect("alse");
return h.decode(isr, Boolean.FALSE);
Expand All @@ -360,19 +386,23 @@ Object decode(Type type, Decoder isr) throws Exception {
}
}

String parseString(Decoder r) throws Exception {
assert r.current() == '"';
protected String parseString(Decoder r) throws Exception {
char quote = (char) r.current();
assert r.current() == '"' || (promiscuous && r.current == '\'');

int c = r.next(); // skip first "

StringBuilder sb = new StringBuilder();
while (c != '"') {
while (c != quote) {
if (c < 0 || Character.isISOControl(c))
throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current());

if (c == '\\') {
c = r.read();
switch (c) {
case '\'' :
r.badJSON("Do not escape single quotes");
// fall through
case '"' :
case '\\' :
case '/' :
Expand Down Expand Up @@ -404,6 +434,10 @@ String parseString(Decoder r) throws Exception {
sb.append((char) c);
break;

case '\n' :
r.badJSON("Do not escape a new line");
break;

default :
throw new IllegalArgumentException(
"The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c);
Expand All @@ -413,7 +447,7 @@ String parseString(Decoder r) throws Exception {

c = r.read();
}
assert c == '"';
assert c == quote;
r.read(); // skip quote
return sb.toString();
}
Expand Down Expand Up @@ -491,7 +525,7 @@ private Number parseNumber(Decoder r) throws Exception {
void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
assert r.current() == '[';
int c = r.next();
while (START_CHARACTERS.indexOf(c) >= 0) {
while (isStartCharacter(c)) {
Object o = decode(componentType, r);
list.add(o);

Expand Down Expand Up @@ -577,4 +611,13 @@ public static String name(String key) {
return key;
}

public JSONCodec promiscuous() {
this.startCharacters = START_CHARACTERS_BAD;
this.promiscuous = true;
return this;
}

public boolean isStartCharacter(int c) {
return startCharacters.indexOf(c) >= 0;
}
}
13 changes: 10 additions & 3 deletions aQute.libg/src/aQute/lib/json/MapHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public Object decodeObject(Decoder r) throws Exception {
Map<Object, Object> map = (Map<Object, Object>) factory.get();

int c = r.next();
while (JSONCodec.START_CHARACTERS.indexOf(c) >= 0) {
while (r.codec.isStartCharacter(c)) {
Object key = r.codec.parseString(r);
if (!(keyType == null || keyType == Object.class)) {
Handler h = r.codec.getHandler(keyType, null);
Expand All @@ -141,10 +141,17 @@ public Object decodeObject(Decoder r) throws Exception {
continue;
}

if (r.codec.promiscuous && r.isEof()) {
r.codec.fishy.incrementAndGet();
return map;
}

throw new IllegalArgumentException(
"Invalid character in parsing list, expected } or , but found " + (char) c);
"Invalid character in parsing map, expected } or , but found " + (char) c);
}
if (c != '}') {
r.fatal("Expected } but got " + (char) c);
}
assert r.current() == '}';
r.read(); // skip closing
return map;
}
Expand Down
7 changes: 6 additions & 1 deletion aQute.libg/src/aQute/lib/json/ObjectHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public Object decodeObject(Decoder r) throws Exception {
Object targetObject = factory.get();

int c = r.next();
while (JSONCodec.START_CHARACTERS.indexOf(c) >= 0) {
while (r.codec.isStartCharacter(c)) {

// Get key
String key = r.codec.parseString(r);
Expand Down Expand Up @@ -159,6 +159,11 @@ public Object decodeObject(Decoder r) throws Exception {
continue;
}

if (r.codec.promiscuous && r.isEof()) {
r.codec.fishy.incrementAndGet();
return targetObject;
}

throw new IllegalArgumentException(
"Invalid character in parsing object, expected } or , but found " + (char) c);
}
Expand Down
Loading

0 comments on commit 77fb48e

Please sign in to comment.