made parser threadlocal

This commit is contained in:
Sander Hautvast 2020-07-16 19:15:08 +02:00
parent 48b4745210
commit fd62bc9167
6 changed files with 186 additions and 51 deletions

View file

@ -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>

View file

@ -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);
} }
} }

View file

@ -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);
}
}
} }

View file

@ -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;
} }
} }
} }

View 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());
}
}

View 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"
}
]
}