improved unicode support, now correctly parses up to utf-32

restricted boolean values to lowercase false/true
This commit is contained in:
Sander Hautvast 2020-07-09 22:21:22 +02:00
parent 52def01e5f
commit 48b4745210
32 changed files with 590 additions and 358 deletions

View file

@ -28,9 +28,9 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-text</artifactId>
<version>2.6</version> <version>1.8</version>
</dependency> </dependency>
<dependency> <dependency>

View file

@ -4,7 +4,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.*; import java.nio.charset.*;
/** /**
* storage like ArrayList, with specialized array * storage like ArrayList, with bytebuffer instead of array
*/ */
public class ByteBuf { public class ByteBuf {
@ -19,19 +19,22 @@ public class ByteBuf {
} }
public String toString() { public String toString() {
return toString(StandardCharsets.UTF_8);
}
public String toString(Charset charset) {
data.flip(); data.flip();
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); // decode is not threadsafe, might put it in threadlocal CharsetDecoder decoder = charset.newDecoder(); // decode is not threadsafe, might put it in threadlocal
// but I don't think this (newDecoder()+config) is expensive // but I don't think this (newDecoder()+config) is expensive
decoder.onMalformedInput(CodingErrorAction.REPLACE) decoder.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE);
;
try { try {
return decoder.decode(data).toString(); return decoder.decode(data).toString();
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
throw new JsonReadException(e); throw new JsonParseException(e);
} }
} }

View file

@ -1,249 +0,0 @@
package nl.sander.jsontoy2;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.*;
public class IoReader {
private static final int HEX_RADIX = 16;
private final InputStream inputStream;
private final ByteBuf characterBuffer = new ByteBuf();
private final ByteBuf encodedCodePointBuffer = new ByteBuf(4);
private final ByteBuffer convertBuffer = ByteBuffer.allocate(4);
private boolean escaping = false;
private boolean encoded = false;
private byte current;
private int linecount = 0;
protected IoReader(InputStream inputStream) {
this.inputStream = inputStream;
advance();
}
protected IoReader(String jsonString) {
this.inputStream = new ByteArrayInputStream(jsonString.getBytes());
advance();
}
public Integer readInteger() {
String value = readNumeric();
return Double.valueOf(value).intValue();
}
public Long readLong() {
String value = readNumeric();
return Long.parseLong(value);
}
public Float readFloat() {
String value = readNumeric();
return Float.parseFloat(value);
}
public Double readDouble() {
String value = readNumeric();
return Double.parseDouble(value);
}
public Short readShort() {
String value = readNumeric();
return Short.parseShort(value);
}
public Byte readByte() {
String value = readNumeric();
return Byte.parseByte(value);
}
public Character readCharacter() {
eatUntil('\"');
char currentChar = (char) current;
eatUntil('\"');
return currentChar;
}
public Boolean readBoolean() {
characterBuffer.clear();
while (Character.isAlphabetic(current)) {
characterBuffer.add(current);
advance();
}
return characterBuffer.toString().equalsIgnoreCase("TRUE");
}
boolean isNumeric(int c) {
return Character.isDigit(c) || c == '.' || c == 'e' || c == '-' || c == '+';
}
private String readNumeric() {
characterBuffer.clear();
if (current == '-') {
characterBuffer.add(current);
advance();
}
while (current > -1 && isNumeric(current)) {
characterBuffer.add(current);
advance();
}
return characterBuffer.toString();
}
public List<?> readList() {
skipWhitespace();
if (current != '[') {
throw new JsonReadException("no list found");
}
List<Object> list = new ArrayList<>();
advance();
while (current != -1 && current != ']') {
Optional<Object> maybeValue = readValue();
if (maybeValue.isEmpty()) {
break;
} else {
list.add(maybeValue.get());
eatUntil(',');
}
}
return list;
}
public Map<?, ?> readMap() {
HashMap<Object, Object> map = new HashMap<>();
skipWhitespace();
if (current != '{') {
throw new JsonReadException("no map found");
}
while (current != -1 && current != '}') {
skipWhitespace();
if (current == '"') {
String key = readString();
eatUntil(':');
skipWhitespace();
Optional<Object> maybeValue = readValue();
maybeValue.ifPresent(o -> map.put(key, o));
eatUntil(',');
} else {
advance();
}
}
return map;
}
private Optional<Object> readValue() {
Object value;
skipWhitespace();
if (current == ']' || current == '}') {
return Optional.empty();
} else if (current == '[') {
value = readList();
} else if (current == '{') {
value = readMap();
} else if (current == '\"') {
value = readString();
} else if (current == 'T' || current == 't' || current == 'F' || current == 'f') {
value = readBoolean();
} else {
String numeric = readNumeric();
double doubleValue = Double.parseDouble(numeric);
if ((int) doubleValue == doubleValue) {
value = (int) doubleValue;
} else {
value = doubleValue;
}
}
return Optional.of(value);
}
public String readString() {
eatUntil('\"');
characterBuffer.clear();
boolean endOfString = false;
while (current > -1 && !endOfString) {
if (current == '\\' && !escaping) {
escaping = true;
} else {
if (!escaping) {
// regular character
if (current != '\"') {
characterBuffer.add(current);
} else {
endOfString = true;
}
} else {
// unicode codepoint
if (current == 'u') {
encoded = true;
} else if (current == 'n') {
linecount++;
characterBuffer.add("\n".getBytes());
escaping = false;
} else if (current == '\"') {
characterBuffer.add("\"".getBytes());
escaping = false;
} else if (current == '\'') {
throw new JsonReadException("illegal escaped quote in line " + linecount);
} else {
if (encoded) {
// load next 4 characters in special buffer to convert to int
encodedCodePointBuffer.add(current);
if (encodedCodePointBuffer.length() == 4) {
byte[] bytes = parseCodePoint();
characterBuffer.add(bytes);
encoded = false;
escaping = false;
}
}
}
}
}
advance();
}
return characterBuffer.toString();
}
private byte[] parseCodePoint() {
String hex = encodedCodePointBuffer.toString();
int codepoint = Integer.parseInt(hex, HEX_RADIX);
convertBuffer.clear();
encodedCodePointBuffer.clear();
return convertBuffer.putInt(codepoint).array();
}
void advance() {
try {
current = (byte) inputStream.read();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
void skipWhitespace() {
try {
while (current > -1 && Character.isWhitespace(current)) {
current = (byte) inputStream.read();
}
if (current == -1) {
throw new JsonReadException("end of source reached");
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
String eatUntil(char until) {
characterBuffer.clear();
while (current > -1 && (current != until | Character.isWhitespace(current))) {
characterBuffer.add(current);
advance();
}
advance();
return characterBuffer.toString();
}
}

View file

@ -0,0 +1,12 @@
package nl.sander.jsontoy2;
public class JsonParseException extends RuntimeException {
public JsonParseException(String message) {
super(message);
}
public JsonParseException(Throwable e) {
super(e);
}
}

View file

@ -1,12 +0,0 @@
package nl.sander.jsontoy2;
public class JsonReadException extends RuntimeException {
public JsonReadException(String message) {
super(message);
}
public JsonReadException(Throwable e) {
super(e);
}
}

View file

@ -16,17 +16,42 @@ import java.util.concurrent.ConcurrentMap;
public class JsonReader { public class JsonReader {
private static final ConcurrentMap<Class<?>, JsonValueReader<?>> readers = new ConcurrentHashMap<>(); private static final ConcurrentMap<Class<?>, JsonValueReader<?>> readers = new ConcurrentHashMap<>();
/**
* reads a value for a type that is not known beforehand
*
* @param stream the underlying stream to read
* @return an Object with runtime type as follows:
* "null" => null
* "true"/"false" => Boolean
* integral number => Integer //TODO Long?
* floating point number => Double
* string => String
* object => HashMap
* array => List
*/
public static Object read(InputStream stream) {
return read(new Parser(stream));
}
public static Object read(String jsonString) {
return read(new Parser(jsonString));
}
static Object read(Parser parser) {
return parser.parseAny();
}
public static <T> T read(Class<T> type, InputStream reader) { public static <T> T read(Class<T> type, InputStream reader) {
return read(type, new IoReader(reader)); return read(type, new Parser(reader));
} }
public static <T> T read(Class<T> type, String jsonString) { public static <T> T read(Class<T> type, String jsonString) {
return read(type, new IoReader(jsonString)); return read(type, new Parser(jsonString));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T> T read(Class<T> type, IoReader ioReader) { static <T> T read(Class<T> type, Parser parser) {
return (T) getReader(type).read(ioReader); return (T) getReader(type).read(parser);
// class.cast() does not work for primitives; // class.cast() does not work for primitives;
} }

View file

@ -1,7 +1,5 @@
package nl.sander.jsontoy2; package nl.sander.jsontoy2;
import java.io.Reader;
public interface JsonValueReader<T> { public interface JsonValueReader<T> {
T read(IoReader ioReader); T read(Parser parser);
} }

View file

@ -0,0 +1,58 @@
package nl.sander.jsontoy2;
import java.io.IOException;
import java.io.InputStream;
import java.util.function.Supplier;
/**
* implements lowest level operations on inputstream.
*/
public class Lexer {
protected final InputStream inputStream;
protected final ByteBuf characterBuffer = new ByteBuf();
protected byte current;
public Lexer(InputStream inputStream) {
this.inputStream = inputStream;
}
void advance() {
try {
current = (byte) inputStream.read();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
void skipWhitespace() {
try {
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))) {
advance();
}
advance();
}
void expect(Supplier<JsonParseException> exceptionSupplier, char... word) {
int increment = 0;
while (current > -1 && increment < word.length) {
if (current != word[increment++]) {
throw exceptionSupplier.get();
} else {
advance();
}
}
}
}

View file

@ -0,0 +1,40 @@
package nl.sander.jsontoy2;
import java.util.function.Consumer;
/*
* Option that may contain null
*/
public class Maybe<T> {
private final boolean hasValue;
private final T value;
private Maybe(boolean hasValue, T value) {
this.hasValue = hasValue;
this.value = value;
}
public static <T> Maybe<T> none() {
return new Maybe<>(false, null);
}
public static <T> Maybe<T> of(T value) {
return new Maybe<>(true, value);
}
public boolean isPresent() {
return hasValue;
}
public T get() {
return value;
}
public void ifPresent(Consumer<T> action) {
if (hasValue) {
action.accept(value);
}
}
}

View file

@ -0,0 +1,278 @@
package nl.sander.jsontoy2;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Reads the input as a stream.
* Implements parsing the lowlevel types (primitives, list, map).
* <p>
* Not threadsafe, not meant to be used directly
*/
public class Parser extends Lexer {
private static final int HEX_RADIX = 16;
private final ByteBuf encodedCodePointBuffer = new ByteBuf(4);
private boolean escaping = false;
private boolean encoded = false;
private int linecount = 0;
protected Parser(InputStream inputStream) {
super(inputStream);
advance();
}
protected Parser(String jsonString) {
super(new ByteArrayInputStream(jsonString.getBytes()));
advance();
}
public Integer parseInteger() {
String value = parseNumber();
return Double.valueOf(value).intValue();
}
public Long parseLong() {
String value = parseNumber();
return Long.parseLong(value);
}
public Float parseFloat() {
String value = parseNumber();
return Float.parseFloat(value);
}
public Double parseDouble() {
String value = parseNumber();
return Double.parseDouble(value);
}
public Short parseShort() {
String value = parseNumber();
return Short.parseShort(value);
}
public Byte parseByte() {
String value = parseNumber();
return Byte.parseByte(value);
}
public Character parseCharacter() {
String string = parseString();
return string.charAt(0);
}
public Boolean parseBoolean() {
characterBuffer.clear();
while (Character.isAlphabetic(current)) {
characterBuffer.add(current);
advance();
}
String maybeBoolean = characterBuffer.toString();
boolean returnValue;
if ((returnValue = maybeBoolean.equals("true")) || maybeBoolean.equals("false")) {
return returnValue;
} else {
throw new JsonParseException("Illegal boolean value: " + maybeBoolean);
}
}
boolean isPartOfNumber(int c) {
return Character.isDigit(c) || c == '.' || c == 'E' || c == 'e' || c == '-' || c == '+';
}
private String parseNumber() {
characterBuffer.clear();
if (current == '-') {
characterBuffer.add(current);
advance();
}
while (current > -1 && isPartOfNumber(current)) {
characterBuffer.add(current);
advance();
}
return characterBuffer.toString();
}
public List<?> parseArray() {
skipWhitespace();
if (current != '[') {
throw new JsonParseException("no list found");
}
List<Object> list = new ArrayList<>();
advance();
while (current != -1 && current != ']') {
Maybe<Object> maybeValue = parseValue();
if (!maybeValue.isPresent()) {
break;
} else {
list.add(maybeValue.get());
eatUntil(',');
}
}
return list;
}
public Map<?, ?> parseObject() {
HashMap<Object, Object> map = new HashMap<>();
skipWhitespace();
if (current != '{') {
throw new JsonParseException("no map found");
}
while (current != -1 && current != '}') {
skipWhitespace();
if (current == '"') {
String key = parseString();
eatUntil(':');
skipWhitespace();
Maybe<Object> maybeValue = parseValue();
maybeValue.ifPresent(o -> map.put(key, o));
eatUntil(',');
} else {
advance();
}
}
return map;
}
public Object parseAny() {
Maybe<Object> maybe = parseValue();
if (maybe.isPresent()) {
return maybe.get();
} else {
throw new JsonParseException("no value found");
}
}
private Maybe<Object> parseValue() {
Object value;
skipWhitespace();
switch (current) {
case ']':
case '}': return Maybe.none();
case '[': value = parseArray();
break;
case '{': value = parseObject();
break;
case '\"': value = parseString();
break;
case 'T':
case 't':
case 'F':
case 'f': value = parseBoolean();
break;
case 'n': value = readNull();
break;
default: String numeric = parseNumber();
double doubleValue = Double.parseDouble(numeric);
if ((int) doubleValue == doubleValue) {
value = (int) doubleValue;
} else {
value = doubleValue;
}
}
return Maybe.of(value);
}
private Object readNull() {
expect(() -> new JsonParseException("Expected 'null', encountered " + (char) current), 'n', 'u', 'l', 'l');
return null;
}
public String parseString() {
eatUntil('\"');
characterBuffer.clear();
boolean endOfString = false;
while (current > -1 && !endOfString) {
if (current == '\\' && !escaping) {
escaping = true;
} else {
if (escaping) {
parseEscapedSequence();
} else {
endOfString = addOrEndOfString();
}
}
advance();
}
return characterBuffer.toString();
}
private void parseEscapedSequence() {
// unicode codepoint
if (encoded) {
parseEncoded();
} else {
parseNonEncodedEscapes();
}
}
private void parseNonEncodedEscapes() {
switch (current) {
case '\\': characterBuffer.add("\\".getBytes()); // backslash
break;
case '/': characterBuffer.add("/".getBytes()); // / the infamous escaped slash
break;
case 'b': characterBuffer.add((byte) 8); // backspace
break;
case 'f': characterBuffer.add((byte) 12); // formfeed
break;
case 't': characterBuffer.add((byte) 9); // tab
break;
case 'u': encoded = true; // \\ u hex hex hex hex encoded utf16/32
break;
case 'n': linecount++; // newline
characterBuffer.add("\n".getBytes());
escaping = false;
break;
case '\"': characterBuffer.add("\"".getBytes()); // quote
escaping = false;
break;
case '\'': throw new JsonParseException("illegal escaped quote in line " + linecount);
}
}
private void parseEncoded() {
StringBuilder buf = new StringBuilder();
char codePoint = parseCodePoint();
buf.append(codePoint);
if (Character.isHighSurrogate(codePoint)) {
expect(() -> new JsonParseException("Invalid unicode codepoint at line " + linecount), '\\', 'u');
char lowSurrogate = parseCodePoint();
if (Character.isLowSurrogate(lowSurrogate)) {
buf.append(lowSurrogate);
}
}
characterBuffer.add(buf.toString().getBytes());
encoded = false;
escaping = false;
}
// load next 4 characters in special buffer to convert to int
private char parseCodePoint() {
encodedCodePointBuffer.clear();
for (int i = 0; i < 4; i++) {
encodedCodePointBuffer.add(current);
advance();
}
return (char) Integer.parseInt(encodedCodePointBuffer.toString(), HEX_RADIX);
}
private boolean addOrEndOfString() {
// regular character
if (current != '\"') {
characterBuffer.add(current);
return false;
} else {
return true;
}
}
}

View file

@ -1,15 +1,10 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public abstract class AbstractDatesReader<T> { public abstract class AbstractDatesReader<T> {

View file

@ -1,11 +1,11 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class BooleanReader implements JsonValueReader<Boolean> { public class BooleanReader implements JsonValueReader<Boolean> {
@Override @Override
public Boolean read(IoReader ioReader) { public Boolean read(Parser parser) {
return ioReader.readBoolean(); return parser.parseBoolean();
} }
} }

View file

@ -1,11 +1,11 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class ByteReader implements JsonValueReader<Byte> { public class ByteReader implements JsonValueReader<Byte> {
@Override @Override
public Byte read(IoReader ioReader) { public Byte read(Parser parser) {
return ioReader.readByte(); return parser.parseByte();
} }
} }

View file

@ -1,11 +1,11 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class CharReader implements JsonValueReader<Character> { public class CharReader implements JsonValueReader<Character> {
@Override @Override
public Character read(IoReader ioReader) { public Character read(Parser parser) {
return ioReader.readCharacter(); return parser.parseCharacter();
} }
} }

View file

@ -1,7 +1,7 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
@ -9,8 +9,8 @@ import java.util.Date;
public class DateReader extends AbstractDatesReader<Date> implements JsonValueReader<Date> { public class DateReader extends AbstractDatesReader<Date> implements JsonValueReader<Date> {
@Override @Override
public Date read(IoReader ioReader) { public Date read(Parser parser) {
ZonedDateTime zdt = getZonedDateTime(ioReader::readString); ZonedDateTime zdt = getZonedDateTime(parser::parseString);
return Date.from(zdt.toInstant()); return Date.from(zdt.toInstant());
} }

View file

@ -1,12 +1,12 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class DoubleReader implements JsonValueReader<Double> { public class DoubleReader implements JsonValueReader<Double> {
@Override @Override
public Double read(IoReader ioReader) { public Double read(Parser parser) {
return ioReader.readDouble(); return parser.parseDouble();
} }
} }

View file

@ -1,12 +1,12 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class FloatReader implements JsonValueReader<Float> { public class FloatReader implements JsonValueReader<Float> {
@Override @Override
public Float read(IoReader ioReader) { public Float read(Parser parser) {
return ioReader.readFloat(); return parser.parseFloat();
} }
} }

View file

@ -1,11 +1,11 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class IntegerReader implements JsonValueReader<Integer> { public class IntegerReader implements JsonValueReader<Integer> {
@Override @Override
public Integer read(IoReader ioReader) { public Integer read(Parser parser) {
return ioReader.readInteger(); return parser.parseInteger();
} }
} }

View file

@ -1,14 +1,14 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
import java.util.List; import java.util.List;
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public class ListReader implements JsonValueReader<List> { public class ListReader implements JsonValueReader<List> {
@Override @Override
public List<?> read(IoReader ioReader) { public List<?> read(Parser parser) {
return ioReader.readList(); return parser.parseArray();
} }
} }

View file

@ -1,7 +1,7 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@ -9,8 +9,8 @@ import java.time.ZonedDateTime;
public class LocalDateTimeReader extends AbstractDatesReader<LocalDateTime> implements JsonValueReader<LocalDateTime> { public class LocalDateTimeReader extends AbstractDatesReader<LocalDateTime> implements JsonValueReader<LocalDateTime> {
@Override @Override
public LocalDateTime read(IoReader ioReader) { public LocalDateTime read(Parser parser) {
ZonedDateTime zdt = getZonedDateTime(ioReader::readString); ZonedDateTime zdt = getZonedDateTime(parser::parseString);
return LocalDateTime.from(zdt); return LocalDateTime.from(zdt);
} }

View file

@ -1,11 +1,11 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class LongReader implements JsonValueReader<Long> { public class LongReader implements JsonValueReader<Long> {
@Override @Override
public Long read(IoReader ioReader) { public Long read(Parser parser) {
return ioReader.readLong(); return parser.parseLong();
} }
} }

View file

@ -1,13 +1,14 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
import java.util.Map; import java.util.Map;
@SuppressWarnings("rawtypes")
public class MapReader implements JsonValueReader<Map> { public class MapReader implements JsonValueReader<Map> {
@Override @Override
public Map read(IoReader ioReader) { public Map read(Parser parser) {
return ioReader.readMap(); return parser.parseObject();
} }
} }

View file

@ -1,12 +1,12 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class ShortReader implements JsonValueReader<Short> { public class ShortReader implements JsonValueReader<Short> {
@Override @Override
public Short read(IoReader ioReader) { public Short read(Parser parser) {
return ioReader.readShort(); return parser.parseShort();
} }
} }

View file

@ -1,12 +1,12 @@
package nl.sander.jsontoy2.readers; package nl.sander.jsontoy2.readers;
import nl.sander.jsontoy2.IoReader;
import nl.sander.jsontoy2.JsonValueReader; import nl.sander.jsontoy2.JsonValueReader;
import nl.sander.jsontoy2.Parser;
public class StringReader implements JsonValueReader<String> { public class StringReader implements JsonValueReader<String> {
@Override @Override
public String read(IoReader ioReader) { public String read(Parser parser) {
return ioReader.readString(); return parser.parseString();
} }
} }

View file

@ -2,8 +2,7 @@ package nl.sander.jsontoy2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class Booleans { public class Booleans {
@ -12,6 +11,16 @@ public class Booleans {
assertEquals(true, JsonReader.read(Boolean.class, "true")); assertEquals(true, JsonReader.read(Boolean.class, "true"));
} }
@Test
public void testIllegalTrue() {
assertThrows(JsonParseException.class, () -> JsonReader.read(Boolean.class, "TRUE"));
}
@Test
public void testIllegalFalse() {
assertThrows(JsonParseException.class, () -> JsonReader.read(Boolean.class, "False"));
}
@Test @Test
public void testFalse() { public void testFalse() {
assertEquals(false, JsonReader.read(Boolean.class, "false")); assertEquals(false, JsonReader.read(Boolean.class, "false"));

View file

@ -21,4 +21,49 @@ public class Chars {
assertEquals('A', JsonReader.read(char.class, "\"AB\"")); assertEquals('A', JsonReader.read(char.class, "\"AB\""));
} }
@Test
public void tab() {
assertEquals('\t', JsonReader.read(char.class, "\"\\t\""));
}
@Test
public void backspace() {
assertEquals('\b', JsonReader.read(char.class, "\"\\b\""));
}
@Test
public void formfeed() {
assertEquals('\f', JsonReader.read(char.class, "\"\\f\""));
}
@Test
public void backslash() {
assertEquals('\\', JsonReader.read(char.class, "\"\\\\\""));
}
@Test
public void slash() {
assertEquals('/', JsonReader.read(char.class, "\"\\/\""));
}
@Test
public void newline() {
assertEquals('\n', JsonReader.read(char.class, "\"\\n\""));
}
@Test
public void unicode() {
assertEquals('\u0100', JsonReader.read(char.class, "\"\\u0100\""));
}
@Test
public void unicodeascii() {
assertEquals('A', JsonReader.read(char.class, "\"\\u0041\""));
}
@Test
public void testunicode32() {
// int codepoint = Character.codePointOf("\\uD834\\uDD1E");
assertEquals("\uD834\uDD1E", JsonReader.read("\"\\uD834\\uDD1E\""));
}
} }

View file

@ -0,0 +1,14 @@
package nl.sander.jsontoy2;
import org.apache.commons.text.StringEscapeUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CommonsTextTest {
@Test
public void testunicode() {
assertEquals("\uD834\uDD1E", StringEscapeUtils.unescapeJson("\\uD834\\uDD1E"));
}
}

View file

@ -2,7 +2,9 @@ package nl.sander.jsontoy2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.*; import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -69,4 +71,11 @@ public class Lists {
List<?> expected = List.of(List.of(), Map.of("list", List.of())); List<?> expected = List.of(List.of(), Map.of("list", List.of()));
assertEquals(expected, list); assertEquals(expected, list);
} }
@Test
public void nullInList() {
List<Object> list = JsonReader.read(List.class, "[null]");
List<?> expected = Collections.singletonList(null);
assertEquals(expected, list);
}
} }

View file

@ -2,7 +2,10 @@ package nl.sander.jsontoy2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.*; import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -33,7 +36,7 @@ public class Maps {
@Test @Test
public void multipleStrings_noColon_error() { public void multipleStrings_noColon_error() {
assertThrows(JsonReadException.class, () -> JsonReader.read(Map.class, " {\"hello\" \"jason\"}")); assertThrows(JsonParseException.class, () -> JsonReader.read(Map.class, " {\"hello\" \"jason\"}"));
} }
@Test @Test
@ -44,7 +47,7 @@ public class Maps {
} }
@Test @Test
@SuppressWarnings("raw") @SuppressWarnings("rawtypes")
public void nestedMap() { public void nestedMap() {
Map<String, Map> list = JsonReader.read(Map.class, "{\"map\": {\"map\":{}}}"); Map<String, Map> list = JsonReader.read(Map.class, "{\"map\": {\"map\":{}}}");
Map<String, Map> expected = new HashMap<>(); Map<String, Map> expected = new HashMap<>();
@ -56,6 +59,7 @@ public class Maps {
} }
@Test @Test
@SuppressWarnings("rawtypes")
public void listInMap() { public void listInMap() {
Map<String, List> list = JsonReader.read(Map.class, " { \"list\" : [ 1 ] } "); Map<String, List> list = JsonReader.read(Map.class, " { \"list\" : [ 1 ] } ");
Map<String, List> expected = new HashMap<>(); Map<String, List> expected = new HashMap<>();

View file

@ -0,0 +1,13 @@
package nl.sander.jsontoy2;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNull;
public class Null {
@Test
public void readNull() {
assertNull(JsonReader.read("null"));
}
}

View file

@ -2,8 +2,6 @@ package nl.sander.jsontoy2;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.charset.CharacterCodingException;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -15,24 +13,19 @@ public class Strings {
} }
@Test @Test
public void firstSurrogateButSecondMissing() throws NoSuchFieldException, IllegalAccessException { public void firstSurrogateButSecondMissing() {
String value = JsonReader.read(String.class, "\"\\uDADA\""); assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, "\"\\uDADA\""));
assertEquals("\u0000\u0000<EFBFBD><EFBFBD>", value); // question mark
} }
@Test @Test
public void incompleteSurrogateAndEscapeValid() { public void incompleteSurrogateAndEscapeValid() {
String value = JsonReader.read(String.class, " \"\\uD800\n\""); assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, " \"\\uD800\n\""));
assertEquals("\u0000\u0000<EFBFBD>\u0000\n", value);
} }
@Test @Test
public void firstValidSurrogateSecondInvalid() throws CharacterCodingException { public void firstValidSurrogateSecondInvalid() {
String value = JsonReader.read(String.class, "\"\\uD888\\u1334\""); String value = JsonReader.read(String.class, "\"\\uD888\\u1334\"");
assertEquals("?", value);
assertEquals("\u0000\u0000؈\u0000\u0000\u00134", value);
} }
@Test @Test
@ -43,7 +36,7 @@ public class Strings {
@Test @Test
public void escapedSingleQuote() { public void escapedSingleQuote() {
assertThrows(JsonReadException.class, () -> JsonReader.read(String.class, "\"\\'\"")); assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, "\"\\'\""));
} }
} }

View file

@ -5,8 +5,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.charset.CharacterCodingException;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class StringsWithJackson { public class StringsWithJackson {
@ -19,15 +17,13 @@ public class StringsWithJackson {
} }
@Test @Test
public void firstSurrogateButSecondMissing() throws NoSuchFieldException, IllegalAccessException, JsonProcessingException { public void firstSurrogateButSecondMissing() throws JsonProcessingException {
String value = jackson.readValue("\"\\uDADA\"", String.class); String value = jackson.readValue("\"\\uDADA\"", String.class);
assertTrue(true);
} }
@Test @Test
public void incompleteSurrogateAndEscapeValid() throws JsonProcessingException { public void incompleteSurrogateAndEscapeValid() {
assertThrows(JsonParseException.class, () -> jackson.readValue("\"\\uD800\n\"", String.class)); assertThrows(JsonParseException.class, () -> jackson.readValue("\"\\uD800\n\"", String.class));
} }