diff --git a/pom.xml b/pom.xml
index dea87ca..307f89c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,9 +59,9 @@
- junit
- junit
- 4.13
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.6.2
test
diff --git a/src/main/java/nl/sander/jsontoy2/JavaObjectReaderFactory.java b/src/main/java/nl/sander/jsontoy2/JavaObjectReaderFactory.java
new file mode 100644
index 0000000..5559c64
--- /dev/null
+++ b/src/main/java/nl/sander/jsontoy2/JavaObjectReaderFactory.java
@@ -0,0 +1,153 @@
+package nl.sander.jsontoy2;
+
+import javassist.*;
+import nl.sander.jsontoy2.javassist.ClassCreationException;
+import nl.sander.jsontoy2.javassist.Javassist;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class JavaObjectReaderFactory {
+ public static final String ROOT_PACKAGE = "serializer.";
+ private final static CtClass DESERIALIZER_BASE = Javassist.getTypeDefinition(JsonValueReader.class);
+ private static final CtClass[] NO_EXCEPTIONS = {};
+ private static final CtClass[] PARSER_PARAM = {Javassist.getTypeDefinition(Parser.class)};
+ private static final Class>[] NO_PARAMS = {};
+ private static final CtClass[] CT_NO_PARAMS = {};
+ private final static CtClass OBJECT_CLASS = Javassist.getTypeDefinition(Object.class);
+ private final static CtClass BOOLEAN = Javassist.getTypeDefinition(boolean.class);
+ private final static CtClass INT = Javassist.getTypeDefinition(int.class);
+ private final static CtClass LONG = Javassist.getTypeDefinition(long.class);
+ private final static CtClass BYTE = Javassist.getTypeDefinition(byte.class);
+ private final static CtClass SHORT = Javassist.getTypeDefinition(short.class);
+ private final static CtClass FLOAT = Javassist.getTypeDefinition(float.class);
+ private final static CtClass DOUBLE = Javassist.getTypeDefinition(double.class);
+
+ @SuppressWarnings("unchecked")
+ static JsonValueReader createReaderInstance(Class type) {
+ try {
+ Class> jsonTypeReader = createReaderClass(type);
+ return (JsonValueReader) jsonTypeReader.getDeclaredConstructor(NO_PARAMS).newInstance(new Object[]{});
+
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ throw new JsonParseException(e);
+ }
+ }
+
+ private static Class> createReaderClass(Class> type) {
+ String name = createDeserializerName(type);
+ CtClass deserializerClass = Javassist.createClass(name, DESERIALIZER_BASE);
+
+ try {
+ deserializerClass.addConstructor(CtNewConstructor.make(CT_NO_PARAMS, NO_EXCEPTIONS, "{super();}", deserializerClass));
+ deserializerClass.addMethod(createReadJsonMethod(deserializerClass, type));
+ return deserializerClass.toClass();
+ } catch (CannotCompileException e) {
+ throw new ClassCreationException(e);
+ }
+ }
+
+ private static CtMethod createReadJsonMethod(CtClass serializerClass, Class> type) {
+ try {
+ String readMethodBodySource = createReadMethodBodySource(type);
+ System.out.println(readMethodBodySource);
+ return CtNewMethod.make(Modifier.PUBLIC, OBJECT_CLASS, "read", PARSER_PARAM, NO_EXCEPTIONS, readMethodBodySource, serializerClass);
+ } catch (CannotCompileException e) {
+ throw new ClassCreationException(e);
+ }
+ }
+
+
+ private static String createReadMethodBodySource(Class> type) {
+ String source = "{";
+ String typeName = type.getName();
+ if (ReaderFactory.readerSuppliers.containsKey(type)) {
+ source += "return " + JsonReader.class.getName() + ".read(" + typeName + ".class, $1);";
+ } else {
+ source += "java.util.Map object=" + JsonReader.class.getName() + ".readJavaObject(" + typeName + ".class,$1);\n";
+ source += typeName + " instance = new " + typeName + "();\n";
+
+ for (Field field : type.getDeclaredFields()) {
+ source += "instance.set" + capitalize(field.getName())
+ + "(" + getSourceForGetValueFromObject(field) + ");\n";
+ }
+
+ source += "return instance;";
+ }
+
+ source += "}\n";
+ return source;
+ }
+
+ @NotNull
+ private static String getSourceForGetValueFromObject(Field field) {
+ Class> fieldType = field.getType();
+ String fieldName = field.getName();
+ if (fieldType == boolean.class) {
+ return "getBoolean(\"" + fieldName + "\", object)";
+ } else if (fieldType == int.class) {
+ return "getInt(\"" + fieldName + "\", object)";
+ } else if (fieldType == long.class) {
+ return "getLong(\"" + fieldName + "\",object)";
+ } else if (fieldType == short.class) {
+ return "getShort(\"" + fieldName + "\",object)";
+ } else if (fieldType == byte.class) {
+ return "getByte(\"" + fieldName + "\",object)";
+ } else if (fieldType == float.class) {
+ return "getFloat(\"" + fieldName + "\",object)";
+ } else if (fieldType == double.class) {
+ return "getDouble(\"" + fieldName + "\",object)";
+ } else if (Set.class.isAssignableFrom(fieldType)) {
+ return "getSet(\"" + fieldName + "\",object)";
+ } else {
+ return "(" + fieldType.getName() + ")(object.get(\"" + fieldName + "\"))";
+ }
+ }
+
+
+ private static String genericType(CtField field) {
+ try {
+ if (!Javassist.isCollection(field.getType())) {
+ return "";
+ } else {
+
+ String genericSignature = field.getGenericSignature(); // java.util.List;
+ Pattern p = Pattern.compile("(.+?)<(.+?);>;");
+ Matcher matcher = p.matcher(genericSignature);
+ if (matcher.find()) {
+ String[] genericTypes = matcher.group(2).substring(1).replaceAll("/", ".").split(";L");
+ for (int i = 0; i < genericTypes.length; i++) {
+ genericTypes[i] += ".class";
+ }
+ return String.join(",", genericTypes);
+ }
+ throw new ClassCreationException("Generic type not ok");
+ }
+ } catch (NotFoundException e) {
+ throw new ClassCreationException(e);
+ }
+ }
+
+ private static String capitalize(String lowercase) {
+ return lowercase.substring(0, 1).toUpperCase() + lowercase.substring(1);
+ }
+
+ private static Set createTypeList(Class>... classes) {
+ return Arrays.stream(classes).map(Class::getName).collect(Collectors.toSet());
+ }
+
+ /*
+ * custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes
+ *
+ * Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
+ */
+ private static String createDeserializerName(Class> type) {
+ return ROOT_PACKAGE + type.getName().replaceAll("\\[]", "Array") + "Deserializer";
+ }
+}
diff --git a/src/main/java/nl/sander/jsontoy2/JsonReader.java b/src/main/java/nl/sander/jsontoy2/JsonReader.java
index 8dedf14..ccd6e11 100644
--- a/src/main/java/nl/sander/jsontoy2/JsonReader.java
+++ b/src/main/java/nl/sander/jsontoy2/JsonReader.java
@@ -1,33 +1,21 @@
package nl.sander.jsontoy2;
-import nl.sander.jsontoy2.readers.*;
-
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
-import java.time.LocalDateTime;
-import java.util.Date;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.function.Supplier;
/**
* public api
*/
public class JsonReader {
- private static final ConcurrentMap, Supplier>> readSuppliers = new ConcurrentHashMap<>();
- private static final ConcurrentMap, JsonValueReader>> readers = new ConcurrentHashMap<>();
+
private final static ThreadLocal> PARSERS = new ThreadLocal<>();
- static {
- registerPrimitiveTypeReaders();
- }
/**
* reads a value from a stream for a type that is not known beforehand
@@ -43,21 +31,12 @@ public class JsonReader {
* array => List
*/
public static Object read(InputStream inputStream) {
- final InputStream in = ensureBuffered(inputStream);
+ final InputStream in = ensureBufferedStream(inputStream);
try (Parser parser = getParser(in)) {
return read(parser);
}
}
- private static InputStream ensureBuffered(InputStream inputStream) {
- if (inputStream instanceof BufferedInputStream) {
- return inputStream;
- } else {
- return new BufferedInputStream(inputStream);
- }
- }
-
-
/**
* Reads a value from a string for a type that is not known beforehand
*
@@ -68,28 +47,6 @@ public class JsonReader {
return read(getParser(jsonString));
}
- private static Parser getParser(String jsonString) {
- return getParser(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)));
- }
-
- private static Parser getParser(InputStream inputStream) {
- Objects.requireNonNull(inputStream, "File not found");
- Parser parser;
- SoftReference parserReference = PARSERS.get();
- if (parserReference == null || (parser = parserReference.get()) == null) {
- parser = new Parser(inputStream);
- parserReference = new SoftReference<>(parser);
- PARSERS.set(parserReference);
- } else {
- parser.init(inputStream);
- }
- return parser;
- }
-
- static Object read(Parser parser) {
- return parser.parseAny();
- }
-
/**
* Reads a value from a stream for the given type
*
@@ -117,41 +74,47 @@ public class JsonReader {
return read(type, getParser(jsonString));
}
+ private static Parser getParser(String jsonString) {
+ return getParser(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private static Parser getParser(InputStream inputStream) {
+ Objects.requireNonNull(inputStream, "File not found");
+ Parser parser;
+ SoftReference parserReference = PARSERS.get();
+ if (parserReference == null || (parser = parserReference.get()) == null) {
+ parser = new Parser(inputStream);
+ parserReference = new SoftReference<>(parser);
+ PARSERS.set(parserReference);
+ } else {
+ parser.init(inputStream);
+ }
+ return parser;
+ }
+
+ static Object read(Parser parser) {
+ return parser.parseAny();
+ }
+
@SuppressWarnings("unchecked")
- private static T read(Class type, Parser parser) {
- return (T) getReader(type).read(parser);
+ public static T read(Class type, Parser parser) {
+ return (T) ReaderFactory.getReader(type).read(parser);
// class.cast() does not work well for primitives;
}
- private static JsonValueReader> getReader(Class type) {
- return readers.computeIfAbsent(type, k -> readSuppliers.get(k).get());
+ @SuppressWarnings("unused")
+ public static Map, ?> readJavaObject(Class> type, Parser parser) {
+ return parser.parseObject(type);
}
- private static void register(Class type, Supplier> objectReader) {
- readSuppliers.put(type, objectReader);
+
+ private static InputStream ensureBufferedStream(InputStream inputStream) {
+ if (inputStream instanceof BufferedInputStream) {
+ return inputStream;
+ } else {
+ return new BufferedInputStream(inputStream);
+ }
}
- private static void registerPrimitiveTypeReaders() {
- register(Boolean.class, BooleanReader::new);
- register(boolean.class, BooleanReader::new);
- register(Integer.class, IntegerReader::new);
- register(int.class, IntegerReader::new);
- register(Long.class, LongReader::new);
- register(long.class, LongReader::new);
- register(Byte.class, ByteReader::new);
- register(byte.class, ByteReader::new);
- register(Short.class, ShortReader::new);
- register(short.class, ShortReader::new);
- register(Double.class, DoubleReader::new);
- register(double.class, DoubleReader::new);
- register(Float.class, FloatReader::new);
- register(float.class, FloatReader::new);
- register(Date.class, DateReader::new);
- register(Character.class, CharReader::new);
- register(char.class, CharReader::new);
- register(String.class, StringReader::new);
- register(LocalDateTime.class, LocalDateTimeReader::new);
- register(List.class, ListReader::new);
- register(Map.class, MapReader::new);
- }
+
}
diff --git a/src/main/java/nl/sander/jsontoy2/JsonValueReader.java b/src/main/java/nl/sander/jsontoy2/JsonValueReader.java
index 7be16a5..e35eefd 100644
--- a/src/main/java/nl/sander/jsontoy2/JsonValueReader.java
+++ b/src/main/java/nl/sander/jsontoy2/JsonValueReader.java
@@ -1,5 +1,57 @@
package nl.sander.jsontoy2;
-public interface JsonValueReader {
- T read(Parser parser);
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Base class for generated readers
+ *
+ * @param the type that the reader produces
+ */
+@SuppressWarnings("unused") /* these methods will be called from generated code*/
+public abstract class JsonValueReader {
+ public abstract T read(Parser parser);
+
+ protected boolean getBoolean(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? false : (Boolean) value;
+ }
+
+ protected int getInt(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? 0 : (Integer) value;
+ }
+
+ protected long getLong(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? 0L : (Long) value;
+ }
+
+ protected short getShort(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? (short) 0 : (Short) value;
+ }
+
+ protected byte getByte(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? (byte) 0 : (Byte) value;
+ }
+
+ protected float getFloat(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? 0F : (Float) value;
+ }
+
+ protected double getDouble(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? 0D : (Double) value;
+ }
+
+ protected Set> getSet(String fieldName, Map values) {
+ Object value = values.get(fieldName);
+ return value == null ? null : new HashSet<>((List>) value);
+ }
+
}
diff --git a/src/main/java/nl/sander/jsontoy2/Lexer.java b/src/main/java/nl/sander/jsontoy2/Lexer.java
index 2985a16..0f0e3b7 100644
--- a/src/main/java/nl/sander/jsontoy2/Lexer.java
+++ b/src/main/java/nl/sander/jsontoy2/Lexer.java
@@ -37,6 +37,10 @@ public class Lexer implements AutoCloseable {
}
}
+ void eatUntil(char until) {
+ eatUntil(new char[]{until});
+ }
+
void eatUntil(char... until) {
while (current > -1 && (!contains(until, current) | Character.isWhitespace(current))) {
advance();
@@ -67,4 +71,9 @@ public class Lexer implements AutoCloseable {
throw new JsonParseException(e);
}
}
+
+ @SuppressWarnings("unused")
+ public byte current() {
+ return current;
+ }
}
diff --git a/src/main/java/nl/sander/jsontoy2/Parser.java b/src/main/java/nl/sander/jsontoy2/Parser.java
index de5f77b..f7d6517 100644
--- a/src/main/java/nl/sander/jsontoy2/Parser.java
+++ b/src/main/java/nl/sander/jsontoy2/Parser.java
@@ -38,7 +38,7 @@ public class Parser extends Lexer {
public Integer parseInteger() {
final String value = parseNumber();
- return Double.valueOf(value).intValue();
+ return Integer.valueOf(value);
}
public Long parseLong() {
@@ -125,6 +125,10 @@ public class Parser extends Lexer {
}
public Map, ?> parseObject() {
+ return parseObject(null);
+ }
+
+ public Map, ?> parseObject(Class> type) {
final HashMap