made parser threadlocal
This commit is contained in:
parent
48b4745210
commit
fd62bc9167
6 changed files with 186 additions and 51 deletions
6
pom.xml
6
pom.xml
|
|
@ -83,5 +83,11 @@
|
||||||
<version>2.10.3</version>
|
<version>2.10.3</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains</groupId>
|
||||||
|
<artifactId>annotations</artifactId>
|
||||||
|
<version>16.0.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,37 @@ package nl.sander.jsontoy2;
|
||||||
|
|
||||||
import nl.sander.jsontoy2.readers.*;
|
import nl.sander.jsontoy2.readers.*;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* public facade
|
* public api
|
||||||
*/
|
*/
|
||||||
public class JsonReader {
|
public class JsonReader {
|
||||||
|
private static final ConcurrentMap<Class<?>, Supplier<JsonValueReader<?>>> readSuppliers = new ConcurrentHashMap<>();
|
||||||
private static final ConcurrentMap<Class<?>, JsonValueReader<?>> readers = new ConcurrentHashMap<>();
|
private static final ConcurrentMap<Class<?>, JsonValueReader<?>> readers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final static ThreadLocal<SoftReference<Parser>> 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:
|
* @return an Object with runtime type as follows:
|
||||||
* "null" => null
|
* "null" => null
|
||||||
* "true"/"false" => Boolean
|
* "true"/"false" => Boolean
|
||||||
|
|
@ -29,61 +42,116 @@ public class JsonReader {
|
||||||
* object => HashMap
|
* object => HashMap
|
||||||
* array => List
|
* array => List
|
||||||
*/
|
*/
|
||||||
public static Object read(InputStream stream) {
|
public static Object read(InputStream inputStream) {
|
||||||
return read(new Parser(stream));
|
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) {
|
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<Parser> 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) {
|
static Object read(Parser parser) {
|
||||||
return parser.parseAny();
|
return parser.parseAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T read(Class<T> 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 <T> the type that is needed
|
||||||
|
* @return Object the specified type
|
||||||
|
*/
|
||||||
|
public static <T> T read(Class<T> 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 <T> the type that is needed
|
||||||
|
* @return Object the specified type
|
||||||
|
*/
|
||||||
public static <T> T read(Class<T> type, String jsonString) {
|
public static <T> T read(Class<T> type, String jsonString) {
|
||||||
return read(type, new Parser(jsonString));
|
return read(type, getParser(jsonString));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
static <T> T read(Class<T> type, Parser parser) {
|
private static <T> T read(Class<T> type, Parser parser) {
|
||||||
return (T) getReader(type).read(parser);
|
return (T) getReader(type).read(parser);
|
||||||
// class.cast() does not work for primitives;
|
// class.cast() does not work well for primitives;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> JsonValueReader<?> getReader(Class<T> type) {
|
private static <T> JsonValueReader<?> getReader(Class<T> type) {
|
||||||
return readers.get(type);
|
return readers.computeIfAbsent(type, k -> readSuppliers.get(k).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
static <T> void register(Class<T> type, JsonValueReader<T> objectReader) {
|
private static <T> void register(Class<T> type, Supplier<JsonValueReader<?>> objectReader) {
|
||||||
readers.put(type, objectReader);
|
readSuppliers.put(type, objectReader);
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
private static void registerPrimitiveTypeReaders() {
|
||||||
register(Boolean.class, new BooleanReader());
|
register(Boolean.class, BooleanReader::new);
|
||||||
register(boolean.class, new BooleanReader());
|
register(boolean.class, BooleanReader::new);
|
||||||
register(Integer.class, new IntegerReader());
|
register(Integer.class, IntegerReader::new);
|
||||||
register(int.class, new IntegerReader());
|
register(int.class, IntegerReader::new);
|
||||||
register(Long.class, new LongReader());
|
register(Long.class, LongReader::new);
|
||||||
register(long.class, new LongReader());
|
register(long.class, LongReader::new);
|
||||||
register(Byte.class, new ByteReader());
|
register(Byte.class, ByteReader::new);
|
||||||
register(byte.class, new ByteReader());
|
register(byte.class, ByteReader::new);
|
||||||
register(Short.class, new ShortReader());
|
register(Short.class, ShortReader::new);
|
||||||
register(short.class, new ShortReader());
|
register(short.class, ShortReader::new);
|
||||||
register(Double.class, new DoubleReader());
|
register(Double.class, DoubleReader::new);
|
||||||
register(double.class, new DoubleReader());
|
register(double.class, DoubleReader::new);
|
||||||
register(Float.class, new FloatReader());
|
register(Float.class, FloatReader::new);
|
||||||
register(float.class, new FloatReader());
|
register(float.class, FloatReader::new);
|
||||||
register(Date.class, new DateReader());
|
register(Date.class, DateReader::new);
|
||||||
register(Character.class, new CharReader());
|
register(Character.class, CharReader::new);
|
||||||
register(char.class, new CharReader());
|
register(char.class, CharReader::new);
|
||||||
register(String.class, new StringReader());
|
register(String.class, StringReader::new);
|
||||||
register(LocalDateTime.class, new LocalDateTimeReader());
|
register(LocalDateTime.class, LocalDateTimeReader::new);
|
||||||
register(List.class, new ListReader());
|
register(List.class, ListReader::new);
|
||||||
register(Map.class, new MapReader());
|
register(Map.class, MapReader::new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,24 @@ package nl.sander.jsontoy2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* implements lowest level operations on inputstream.
|
* implements lowest level operations on inputstream.
|
||||||
*/
|
*/
|
||||||
public class Lexer {
|
public class Lexer implements AutoCloseable {
|
||||||
protected final InputStream inputStream;
|
protected InputStream inputStream;
|
||||||
protected final ByteBuf characterBuffer = new ByteBuf();
|
protected final ByteBuf characterBuffer = new ByteBuf();
|
||||||
protected byte current;
|
protected byte current;
|
||||||
|
protected int charCount = 0;
|
||||||
|
|
||||||
public Lexer(InputStream inputStream) {
|
public Lexer(InputStream inputStream) {
|
||||||
this.inputStream = inputStream;
|
this.inputStream = inputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
void advance() {
|
void advance() {
|
||||||
|
charCount++;
|
||||||
try {
|
try {
|
||||||
current = (byte) inputStream.read();
|
current = (byte) inputStream.read();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -29,21 +32,22 @@ public class Lexer {
|
||||||
while (current > -1 && Character.isWhitespace(current)) {
|
while (current > -1 && Character.isWhitespace(current)) {
|
||||||
current = (byte) inputStream.read();
|
current = (byte) inputStream.read();
|
||||||
}
|
}
|
||||||
if (current == -1) {
|
|
||||||
throw new JsonParseException("end of source reached");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void eatUntil(char until) {
|
void eatUntil(char... until) {
|
||||||
while (current > -1 && (current != until | Character.isWhitespace(current))) {
|
while (current > -1 && (!contains(until, current) | Character.isWhitespace(current))) {
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean contains(char[] characters, byte c) {
|
||||||
|
return Arrays.binarySearch(characters, (char) c) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
void expect(Supplier<JsonParseException> exceptionSupplier, char... word) {
|
void expect(Supplier<JsonParseException> exceptionSupplier, char... word) {
|
||||||
int increment = 0;
|
int increment = 0;
|
||||||
|
|
||||||
|
|
@ -55,4 +59,12 @@ public class Lexer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new JsonParseException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package nl.sander.jsontoy2;
|
package nl.sander.jsontoy2;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -26,8 +25,14 @@ public class Parser extends Lexer {
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Parser(String jsonString) {
|
public void init(InputStream inputStream) {
|
||||||
super(new ByteArrayInputStream(jsonString.getBytes()));
|
this.inputStream = inputStream;
|
||||||
|
linecount = 0;
|
||||||
|
charCount = 0;
|
||||||
|
escaping = false;
|
||||||
|
encoded = false;
|
||||||
|
encodedCodePointBuffer.clear();
|
||||||
|
characterBuffer.clear();
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,18 +130,23 @@ public class Parser extends Lexer {
|
||||||
if (current != '{') {
|
if (current != '{') {
|
||||||
throw new JsonParseException("no map found");
|
throw new JsonParseException("no map found");
|
||||||
}
|
}
|
||||||
|
advance();
|
||||||
while (current != -1 && current != '}') {
|
while (current != -1 && current != '}') {
|
||||||
skipWhitespace();
|
skipWhitespace();
|
||||||
if (current == '"') {
|
if (current == '"') {
|
||||||
String key = parseString();
|
String key = parseString();
|
||||||
eatUntil(':');
|
skipWhitespace();
|
||||||
|
if (current == ':') {
|
||||||
|
advance();
|
||||||
|
} else {
|
||||||
|
throw new JsonParseException("expected colon");
|
||||||
|
}
|
||||||
skipWhitespace();
|
skipWhitespace();
|
||||||
Maybe<Object> maybeValue = parseValue();
|
Maybe<Object> maybeValue = parseValue();
|
||||||
maybeValue.ifPresent(o -> map.put(key, o));
|
maybeValue.ifPresent(value -> map.put(key, value));
|
||||||
eatUntil(',');
|
|
||||||
} else {
|
|
||||||
advance();
|
|
||||||
}
|
}
|
||||||
|
advance();
|
||||||
|
skipWhitespace();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
@ -275,4 +285,6 @@ public class Parser extends Lexer {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/test/java/nl/sander/jsontoy2/RandomObject.java
Normal file
16
src/test/java/nl/sander/jsontoy2/RandomObject.java
Normal file
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/test/resources/random_object.json
Normal file
21
src/test/resources/random_object.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue