diff --git a/pom.xml b/pom.xml index a69f439..dea87ca 100644 --- a/pom.xml +++ b/pom.xml @@ -83,5 +83,11 @@ 2.10.3 test + + org.jetbrains + annotations + 16.0.2 + compile + diff --git a/src/main/java/nl/sander/jsontoy2/JsonReader.java b/src/main/java/nl/sander/jsontoy2/JsonReader.java index c34deb3..6179a0d 100644 --- a/src/main/java/nl/sander/jsontoy2/JsonReader.java +++ b/src/main/java/nl/sander/jsontoy2/JsonReader.java @@ -2,24 +2,37 @@ 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 facade + * 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 for a type that is not known beforehand + * reads a value from a stream for a type that is not known beforehand * - * @param stream the underlying stream to read + * @param inputStream the underlying stream to read * @return an Object with runtime type as follows: * "null" => null * "true"/"false" => Boolean @@ -29,61 +42,116 @@ public class JsonReader { * object => HashMap * array => List */ - public static Object read(InputStream stream) { - return read(new Parser(stream)); + public static Object read(InputStream inputStream) { + InputStream in = ensureBuffered(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 + * + * @param jsonString the Json String to read + * @return @see read(InputStream stream) + */ public static Object read(String jsonString) { - return read(new Parser(jsonString)); + 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(); } - public static T read(Class type, InputStream reader) { - return read(type, new Parser(reader)); + /** + * Reads a value from a stream for the given type + * + * @param type The class for the type that is needed + * @param inputStream The stream to read + * @param the type that is needed + * @return Object the specified type + */ + public static T read(Class type, InputStream inputStream) { + Parser parser = getParser(inputStream); + T value = read(type, parser); + parser.close(); + return value; } + /** + * Reads a value from a stream for the given type + * + * @param type The class for the type that is needed + * @param jsonString The String to read + * @param the type that is needed + * @return Object the specified type + */ public static T read(Class type, String jsonString) { - return read(type, new Parser(jsonString)); + return read(type, getParser(jsonString)); } @SuppressWarnings("unchecked") - static T read(Class type, Parser parser) { + private static T read(Class type, Parser parser) { return (T) getReader(type).read(parser); -// class.cast() does not work for primitives; +// class.cast() does not work well for primitives; } private static JsonValueReader getReader(Class type) { - return readers.get(type); + return readers.computeIfAbsent(type, k -> readSuppliers.get(k).get()); } - static void register(Class type, JsonValueReader objectReader) { - readers.put(type, objectReader); + private static void register(Class type, Supplier> objectReader) { + readSuppliers.put(type, objectReader); } - static { - register(Boolean.class, new BooleanReader()); - register(boolean.class, new BooleanReader()); - register(Integer.class, new IntegerReader()); - register(int.class, new IntegerReader()); - register(Long.class, new LongReader()); - register(long.class, new LongReader()); - register(Byte.class, new ByteReader()); - register(byte.class, new ByteReader()); - register(Short.class, new ShortReader()); - register(short.class, new ShortReader()); - register(Double.class, new DoubleReader()); - register(double.class, new DoubleReader()); - register(Float.class, new FloatReader()); - register(float.class, new FloatReader()); - register(Date.class, new DateReader()); - register(Character.class, new CharReader()); - register(char.class, new CharReader()); - register(String.class, new StringReader()); - register(LocalDateTime.class, new LocalDateTimeReader()); - register(List.class, new ListReader()); - register(Map.class, new MapReader()); + 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/Lexer.java b/src/main/java/nl/sander/jsontoy2/Lexer.java index a108e10..2985a16 100644 --- a/src/main/java/nl/sander/jsontoy2/Lexer.java +++ b/src/main/java/nl/sander/jsontoy2/Lexer.java @@ -2,21 +2,24 @@ package nl.sander.jsontoy2; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.function.Supplier; /** * implements lowest level operations on inputstream. */ -public class Lexer { - protected final InputStream inputStream; +public class Lexer implements AutoCloseable { + protected InputStream inputStream; protected final ByteBuf characterBuffer = new ByteBuf(); protected byte current; + protected int charCount = 0; public Lexer(InputStream inputStream) { this.inputStream = inputStream; } void advance() { + charCount++; try { current = (byte) inputStream.read(); } catch (IOException e) { @@ -29,21 +32,22 @@ public class Lexer { while (current > -1 && Character.isWhitespace(current)) { current = (byte) inputStream.read(); } - if (current == -1) { - throw new JsonParseException("end of source reached"); - } } catch (IOException e) { throw new IllegalStateException(e); } } - void eatUntil(char until) { - while (current > -1 && (current != until | Character.isWhitespace(current))) { + void eatUntil(char... until) { + while (current > -1 && (!contains(until, current) | Character.isWhitespace(current))) { advance(); } advance(); } + private boolean contains(char[] characters, byte c) { + return Arrays.binarySearch(characters, (char) c) > -1; + } + void expect(Supplier exceptionSupplier, char... word) { int increment = 0; @@ -55,4 +59,12 @@ public class Lexer { } } } + + public void close() { + try { + inputStream.close(); + } catch (IOException e) { + throw new JsonParseException(e); + } + } } diff --git a/src/main/java/nl/sander/jsontoy2/Parser.java b/src/main/java/nl/sander/jsontoy2/Parser.java index 09da074..a4871d2 100644 --- a/src/main/java/nl/sander/jsontoy2/Parser.java +++ b/src/main/java/nl/sander/jsontoy2/Parser.java @@ -1,6 +1,5 @@ package nl.sander.jsontoy2; -import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -26,8 +25,14 @@ public class Parser extends Lexer { advance(); } - protected Parser(String jsonString) { - super(new ByteArrayInputStream(jsonString.getBytes())); + public void init(InputStream inputStream) { + this.inputStream = inputStream; + linecount = 0; + charCount = 0; + escaping = false; + encoded = false; + encodedCodePointBuffer.clear(); + characterBuffer.clear(); advance(); } @@ -125,18 +130,23 @@ public class Parser extends Lexer { if (current != '{') { throw new JsonParseException("no map found"); } + advance(); while (current != -1 && current != '}') { skipWhitespace(); if (current == '"') { String key = parseString(); - eatUntil(':'); + skipWhitespace(); + if (current == ':') { + advance(); + } else { + throw new JsonParseException("expected colon"); + } skipWhitespace(); Maybe maybeValue = parseValue(); - maybeValue.ifPresent(o -> map.put(key, o)); - eatUntil(','); - } else { - advance(); + maybeValue.ifPresent(value -> map.put(key, value)); } + advance(); + skipWhitespace(); } return map; } @@ -275,4 +285,6 @@ public class Parser extends Lexer { return true; } } + + } diff --git a/src/test/java/nl/sander/jsontoy2/RandomObject.java b/src/test/java/nl/sander/jsontoy2/RandomObject.java new file mode 100644 index 0000000..1527ee1 --- /dev/null +++ b/src/test/java/nl/sander/jsontoy2/RandomObject.java @@ -0,0 +1,16 @@ +package nl.sander.jsontoy2; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RandomObject { + + @Test + public void testRandomJsonObject() { + Map map = JsonReader.read(Map.class, getClass().getResourceAsStream("/random_object.json")); + assertEquals(5, map.size()); + } +} diff --git a/src/test/resources/random_object.json b/src/test/resources/random_object.json new file mode 100644 index 0000000..91f5fc0 --- /dev/null +++ b/src/test/resources/random_object.json @@ -0,0 +1,21 @@ +{ + "firstName": "John", + "lastName": "Smith", + "age": 25, + "address": { + "streetAddress": "21 2nd Street", + "city": "New York", + "state": "NY", + "postalCode": 10021 + }, + "phoneNumbers": [ + { + "type": "home", + "number": "212 555-1234" + }, + { + "type": "fax", + "number": "646 555-4567" + } + ] +} \ No newline at end of file