diff --git a/pom.xml b/pom.xml index 2907a20..a69f439 100644 --- a/pom.xml +++ b/pom.xml @@ -28,9 +28,9 @@ - commons-io - commons-io - 2.6 + org.apache.commons + commons-text + 1.8 diff --git a/src/main/java/nl/sander/jsontoy2/ByteBuf.java b/src/main/java/nl/sander/jsontoy2/ByteBuf.java index 4ecd709..efaafbc 100644 --- a/src/main/java/nl/sander/jsontoy2/ByteBuf.java +++ b/src/main/java/nl/sander/jsontoy2/ByteBuf.java @@ -4,7 +4,7 @@ import java.nio.ByteBuffer; import java.nio.charset.*; /** - * storage like ArrayList, with specialized array + * storage like ArrayList, with bytebuffer instead of array */ public class ByteBuf { @@ -19,19 +19,22 @@ public class ByteBuf { } public String toString() { + return toString(StandardCharsets.UTF_8); + } + + public String toString(Charset charset) { 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 decoder.onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE) - ; + .onUnmappableCharacter(CodingErrorAction.REPLACE); try { return decoder.decode(data).toString(); } catch (CharacterCodingException e) { - throw new JsonReadException(e); + throw new JsonParseException(e); } } diff --git a/src/main/java/nl/sander/jsontoy2/IoReader.java b/src/main/java/nl/sander/jsontoy2/IoReader.java deleted file mode 100644 index 4fa4f23..0000000 --- a/src/main/java/nl/sander/jsontoy2/IoReader.java +++ /dev/null @@ -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 list = new ArrayList<>(); - advance(); - while (current != -1 && current != ']') { - Optional maybeValue = readValue(); - if (maybeValue.isEmpty()) { - break; - } else { - list.add(maybeValue.get()); - eatUntil(','); - } - } - - return list; - } - - public Map readMap() { - HashMap 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 maybeValue = readValue(); - maybeValue.ifPresent(o -> map.put(key, o)); - eatUntil(','); - } else { - advance(); - } - } - return map; - } - - private Optional 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(); - } -} diff --git a/src/main/java/nl/sander/jsontoy2/JsonParseException.java b/src/main/java/nl/sander/jsontoy2/JsonParseException.java new file mode 100644 index 0000000..dd8d8da --- /dev/null +++ b/src/main/java/nl/sander/jsontoy2/JsonParseException.java @@ -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); + } +} diff --git a/src/main/java/nl/sander/jsontoy2/JsonReadException.java b/src/main/java/nl/sander/jsontoy2/JsonReadException.java deleted file mode 100644 index 86501b3..0000000 --- a/src/main/java/nl/sander/jsontoy2/JsonReadException.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/nl/sander/jsontoy2/JsonReader.java b/src/main/java/nl/sander/jsontoy2/JsonReader.java index f2aeae6..c34deb3 100644 --- a/src/main/java/nl/sander/jsontoy2/JsonReader.java +++ b/src/main/java/nl/sander/jsontoy2/JsonReader.java @@ -16,17 +16,42 @@ import java.util.concurrent.ConcurrentMap; public class JsonReader { private static final ConcurrentMap, 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 read(Class type, InputStream reader) { - return read(type, new IoReader(reader)); + return read(type, new Parser(reader)); } public static T read(Class type, String jsonString) { - return read(type, new IoReader(jsonString)); + return read(type, new Parser(jsonString)); } @SuppressWarnings("unchecked") - public static T read(Class type, IoReader ioReader) { - return (T) getReader(type).read(ioReader); + static T read(Class type, Parser parser) { + return (T) getReader(type).read(parser); // class.cast() does not work for primitives; } diff --git a/src/main/java/nl/sander/jsontoy2/JsonValueReader.java b/src/main/java/nl/sander/jsontoy2/JsonValueReader.java index 18e2cd7..7be16a5 100644 --- a/src/main/java/nl/sander/jsontoy2/JsonValueReader.java +++ b/src/main/java/nl/sander/jsontoy2/JsonValueReader.java @@ -1,7 +1,5 @@ package nl.sander.jsontoy2; -import java.io.Reader; - public interface JsonValueReader { - T read(IoReader ioReader); + T read(Parser parser); } diff --git a/src/main/java/nl/sander/jsontoy2/Lexer.java b/src/main/java/nl/sander/jsontoy2/Lexer.java new file mode 100644 index 0000000..a108e10 --- /dev/null +++ b/src/main/java/nl/sander/jsontoy2/Lexer.java @@ -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 exceptionSupplier, char... word) { + int increment = 0; + + while (current > -1 && increment < word.length) { + if (current != word[increment++]) { + throw exceptionSupplier.get(); + } else { + advance(); + } + } + } +} diff --git a/src/main/java/nl/sander/jsontoy2/Maybe.java b/src/main/java/nl/sander/jsontoy2/Maybe.java new file mode 100644 index 0000000..75806b7 --- /dev/null +++ b/src/main/java/nl/sander/jsontoy2/Maybe.java @@ -0,0 +1,40 @@ +package nl.sander.jsontoy2; + +import java.util.function.Consumer; + +/* + * Option that may contain null + */ +public class Maybe { + private final boolean hasValue; + private final T value; + + private Maybe(boolean hasValue, T value) { + this.hasValue = hasValue; + this.value = value; + } + + public static Maybe none() { + return new Maybe<>(false, null); + } + + public static Maybe of(T value) { + return new Maybe<>(true, value); + } + + public boolean isPresent() { + return hasValue; + } + + public T get() { + return value; + } + + public void ifPresent(Consumer action) { + if (hasValue) { + action.accept(value); + } + } + + +} \ No newline at end of file diff --git a/src/main/java/nl/sander/jsontoy2/Parser.java b/src/main/java/nl/sander/jsontoy2/Parser.java new file mode 100644 index 0000000..09da074 --- /dev/null +++ b/src/main/java/nl/sander/jsontoy2/Parser.java @@ -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). + *

+ * 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 list = new ArrayList<>(); + advance(); + while (current != -1 && current != ']') { + Maybe maybeValue = parseValue(); + if (!maybeValue.isPresent()) { + break; + } else { + list.add(maybeValue.get()); + eatUntil(','); + } + } + + return list; + } + + public Map parseObject() { + HashMap 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 maybeValue = parseValue(); + maybeValue.ifPresent(o -> map.put(key, o)); + eatUntil(','); + } else { + advance(); + } + } + return map; + } + + public Object parseAny() { + Maybe maybe = parseValue(); + if (maybe.isPresent()) { + return maybe.get(); + } else { + throw new JsonParseException("no value found"); + } + } + + private Maybe 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; + } + } +} diff --git a/src/main/java/nl/sander/jsontoy2/readers/AbstractDatesReader.java b/src/main/java/nl/sander/jsontoy2/readers/AbstractDatesReader.java index 9115f23..da1c3f9 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/AbstractDatesReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/AbstractDatesReader.java @@ -1,15 +1,10 @@ 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.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; -import java.util.function.Function; import java.util.function.Supplier; public abstract class AbstractDatesReader { diff --git a/src/main/java/nl/sander/jsontoy2/readers/BooleanReader.java b/src/main/java/nl/sander/jsontoy2/readers/BooleanReader.java index 7b71a53..74033a9 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/BooleanReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/BooleanReader.java @@ -1,11 +1,11 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class BooleanReader implements JsonValueReader { @Override - public Boolean read(IoReader ioReader) { - return ioReader.readBoolean(); + public Boolean read(Parser parser) { + return parser.parseBoolean(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/ByteReader.java b/src/main/java/nl/sander/jsontoy2/readers/ByteReader.java index b5c1264..56804ee 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/ByteReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/ByteReader.java @@ -1,11 +1,11 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class ByteReader implements JsonValueReader { @Override - public Byte read(IoReader ioReader) { - return ioReader.readByte(); + public Byte read(Parser parser) { + return parser.parseByte(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/CharReader.java b/src/main/java/nl/sander/jsontoy2/readers/CharReader.java index 081593f..cd79650 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/CharReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/CharReader.java @@ -1,11 +1,11 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class CharReader implements JsonValueReader { @Override - public Character read(IoReader ioReader) { - return ioReader.readCharacter(); + public Character read(Parser parser) { + return parser.parseCharacter(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/DateReader.java b/src/main/java/nl/sander/jsontoy2/readers/DateReader.java index 1caad35..99dbc11 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/DateReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/DateReader.java @@ -1,7 +1,7 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; import java.time.ZonedDateTime; import java.util.Date; @@ -9,8 +9,8 @@ import java.util.Date; public class DateReader extends AbstractDatesReader implements JsonValueReader { @Override - public Date read(IoReader ioReader) { - ZonedDateTime zdt = getZonedDateTime(ioReader::readString); + public Date read(Parser parser) { + ZonedDateTime zdt = getZonedDateTime(parser::parseString); return Date.from(zdt.toInstant()); } diff --git a/src/main/java/nl/sander/jsontoy2/readers/DoubleReader.java b/src/main/java/nl/sander/jsontoy2/readers/DoubleReader.java index ada38c2..148dfde 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/DoubleReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/DoubleReader.java @@ -1,12 +1,12 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class DoubleReader implements JsonValueReader { @Override - public Double read(IoReader ioReader) { - return ioReader.readDouble(); + public Double read(Parser parser) { + return parser.parseDouble(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/FloatReader.java b/src/main/java/nl/sander/jsontoy2/readers/FloatReader.java index a060397..eec7bcb 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/FloatReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/FloatReader.java @@ -1,12 +1,12 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class FloatReader implements JsonValueReader { @Override - public Float read(IoReader ioReader) { - return ioReader.readFloat(); + public Float read(Parser parser) { + return parser.parseFloat(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/IntegerReader.java b/src/main/java/nl/sander/jsontoy2/readers/IntegerReader.java index f417fc3..9072abf 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/IntegerReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/IntegerReader.java @@ -1,11 +1,11 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class IntegerReader implements JsonValueReader { @Override - public Integer read(IoReader ioReader) { - return ioReader.readInteger(); + public Integer read(Parser parser) { + return parser.parseInteger(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/ListReader.java b/src/main/java/nl/sander/jsontoy2/readers/ListReader.java index e603b3e..7a51106 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/ListReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/ListReader.java @@ -1,14 +1,14 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; import java.util.List; @SuppressWarnings("rawtypes") public class ListReader implements JsonValueReader { @Override - public List read(IoReader ioReader) { - return ioReader.readList(); + public List read(Parser parser) { + return parser.parseArray(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/LocalDateTimeReader.java b/src/main/java/nl/sander/jsontoy2/readers/LocalDateTimeReader.java index 1d07380..e8f6b98 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/LocalDateTimeReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/LocalDateTimeReader.java @@ -1,7 +1,7 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; import java.time.LocalDateTime; import java.time.ZonedDateTime; @@ -9,8 +9,8 @@ import java.time.ZonedDateTime; public class LocalDateTimeReader extends AbstractDatesReader implements JsonValueReader { @Override - public LocalDateTime read(IoReader ioReader) { - ZonedDateTime zdt = getZonedDateTime(ioReader::readString); + public LocalDateTime read(Parser parser) { + ZonedDateTime zdt = getZonedDateTime(parser::parseString); return LocalDateTime.from(zdt); } diff --git a/src/main/java/nl/sander/jsontoy2/readers/LongReader.java b/src/main/java/nl/sander/jsontoy2/readers/LongReader.java index 7192521..37bac56 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/LongReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/LongReader.java @@ -1,11 +1,11 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class LongReader implements JsonValueReader { @Override - public Long read(IoReader ioReader) { - return ioReader.readLong(); + public Long read(Parser parser) { + return parser.parseLong(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/MapReader.java b/src/main/java/nl/sander/jsontoy2/readers/MapReader.java index db7a485..2e42722 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/MapReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/MapReader.java @@ -1,13 +1,14 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; import java.util.Map; +@SuppressWarnings("rawtypes") public class MapReader implements JsonValueReader { @Override - public Map read(IoReader ioReader) { - return ioReader.readMap(); + public Map read(Parser parser) { + return parser.parseObject(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/ShortReader.java b/src/main/java/nl/sander/jsontoy2/readers/ShortReader.java index 9bb320e..c272956 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/ShortReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/ShortReader.java @@ -1,12 +1,12 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class ShortReader implements JsonValueReader { @Override - public Short read(IoReader ioReader) { - return ioReader.readShort(); + public Short read(Parser parser) { + return parser.parseShort(); } } diff --git a/src/main/java/nl/sander/jsontoy2/readers/StringReader.java b/src/main/java/nl/sander/jsontoy2/readers/StringReader.java index 421f734..d1cdfe1 100644 --- a/src/main/java/nl/sander/jsontoy2/readers/StringReader.java +++ b/src/main/java/nl/sander/jsontoy2/readers/StringReader.java @@ -1,12 +1,12 @@ package nl.sander.jsontoy2.readers; -import nl.sander.jsontoy2.IoReader; import nl.sander.jsontoy2.JsonValueReader; +import nl.sander.jsontoy2.Parser; public class StringReader implements JsonValueReader { @Override - public String read(IoReader ioReader) { - return ioReader.readString(); + public String read(Parser parser) { + return parser.parseString(); } } diff --git a/src/test/java/nl/sander/jsontoy2/Booleans.java b/src/test/java/nl/sander/jsontoy2/Booleans.java index 7682919..bac2f1b 100644 --- a/src/test/java/nl/sander/jsontoy2/Booleans.java +++ b/src/test/java/nl/sander/jsontoy2/Booleans.java @@ -2,8 +2,7 @@ package nl.sander.jsontoy2; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.*; public class Booleans { @@ -12,6 +11,16 @@ public class Booleans { 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 public void testFalse() { assertEquals(false, JsonReader.read(Boolean.class, "false")); diff --git a/src/test/java/nl/sander/jsontoy2/Chars.java b/src/test/java/nl/sander/jsontoy2/Chars.java index 06292ea..960437b 100644 --- a/src/test/java/nl/sander/jsontoy2/Chars.java +++ b/src/test/java/nl/sander/jsontoy2/Chars.java @@ -21,4 +21,49 @@ public class Chars { 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\"")); + } } diff --git a/src/test/java/nl/sander/jsontoy2/CommonsTextTest.java b/src/test/java/nl/sander/jsontoy2/CommonsTextTest.java new file mode 100644 index 0000000..4af79e2 --- /dev/null +++ b/src/test/java/nl/sander/jsontoy2/CommonsTextTest.java @@ -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")); + } +} diff --git a/src/test/java/nl/sander/jsontoy2/Lists.java b/src/test/java/nl/sander/jsontoy2/Lists.java index 190c335..83f229b 100644 --- a/src/test/java/nl/sander/jsontoy2/Lists.java +++ b/src/test/java/nl/sander/jsontoy2/Lists.java @@ -2,7 +2,9 @@ package nl.sander.jsontoy2; 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; @@ -66,7 +68,14 @@ public class Lists { @Test public void mapInList() { List list = JsonReader.read(List.class, "[[],{\"list\":[]]}]"); - 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); + } + + @Test + public void nullInList() { + List list = JsonReader.read(List.class, "[null]"); + List expected = Collections.singletonList(null); assertEquals(expected, list); } } \ No newline at end of file diff --git a/src/test/java/nl/sander/jsontoy2/Maps.java b/src/test/java/nl/sander/jsontoy2/Maps.java index 79fcba1..21f76fc 100644 --- a/src/test/java/nl/sander/jsontoy2/Maps.java +++ b/src/test/java/nl/sander/jsontoy2/Maps.java @@ -2,7 +2,10 @@ package nl.sander.jsontoy2; 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.assertThrows; @@ -33,32 +36,33 @@ public class Maps { @Test public void multipleStrings_noColon_error() { - assertThrows(JsonReadException.class, () -> JsonReader.read(Map.class, " {\"hello\" \"jason\"}")); + assertThrows(JsonParseException.class, () -> JsonReader.read(Map.class, " {\"hello\" \"jason\"}")); } @Test public void singleInts() { - Map map = JsonReader.read(Map.class, " { \"1\":2 }"); - Map expected = Collections.singletonMap("1",2); + Map map = JsonReader.read(Map.class, " { \"1\":2 }"); + Map expected = Collections.singletonMap("1", 2); assertEquals(expected, map); } @Test - @SuppressWarnings("raw") + @SuppressWarnings("rawtypes") public void nestedMap() { - Map list = JsonReader.read(Map.class, "{\"map\": {\"map\":{}}}"); - Map expected = new HashMap<>(); - HashMap n1 = new HashMap<>(); - n1.put("map",new HashMap<>()); + Map list = JsonReader.read(Map.class, "{\"map\": {\"map\":{}}}"); + Map expected = new HashMap<>(); + HashMap n1 = new HashMap<>(); + n1.put("map", new HashMap<>()); expected.put("map", n1); assertEquals(expected, list); } @Test - public void listInMap(){ - Map list = JsonReader.read(Map.class, " { \"list\" : [ 1 ] } "); - Map expected = new HashMap<>(); + @SuppressWarnings("rawtypes") + public void listInMap() { + Map list = JsonReader.read(Map.class, " { \"list\" : [ 1 ] } "); + Map expected = new HashMap<>(); expected.put("list", List.of(1)); assertEquals(expected, list); diff --git a/src/test/java/nl/sander/jsontoy2/Null.java b/src/test/java/nl/sander/jsontoy2/Null.java new file mode 100644 index 0000000..cc75e29 --- /dev/null +++ b/src/test/java/nl/sander/jsontoy2/Null.java @@ -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")); + } + +} diff --git a/src/test/java/nl/sander/jsontoy2/Strings.java b/src/test/java/nl/sander/jsontoy2/Strings.java index 2db72ed..09ba386 100644 --- a/src/test/java/nl/sander/jsontoy2/Strings.java +++ b/src/test/java/nl/sander/jsontoy2/Strings.java @@ -2,8 +2,6 @@ package nl.sander.jsontoy2; 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.assertThrows; @@ -15,24 +13,19 @@ public class Strings { } @Test - public void firstSurrogateButSecondMissing() throws NoSuchFieldException, IllegalAccessException { - String value = JsonReader.read(String.class, "\"\\uDADA\""); - - assertEquals("\u0000\u0000��", value); // question mark + public void firstSurrogateButSecondMissing() { + assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, "\"\\uDADA\"")); } - @Test public void incompleteSurrogateAndEscapeValid() { - String value = JsonReader.read(String.class, " \"\\uD800\n\""); - assertEquals("\u0000\u0000�\u0000\n", value); + assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, " \"\\uD800\n\"")); } @Test - public void firstValidSurrogateSecondInvalid() throws CharacterCodingException { + public void firstValidSurrogateSecondInvalid() { String value = JsonReader.read(String.class, "\"\\uD888\\u1334\""); - - assertEquals("\u0000\u0000؈\u0000\u0000\u00134", value); + assertEquals("?", value); } @Test @@ -43,7 +36,7 @@ public class Strings { @Test public void escapedSingleQuote() { - assertThrows(JsonReadException.class, () -> JsonReader.read(String.class, "\"\\'\"")); + assertThrows(JsonParseException.class, () -> JsonReader.read(String.class, "\"\\'\"")); } } diff --git a/src/test/java/nl/sander/jsontoy2/StringsWithJackson.java b/src/test/java/nl/sander/jsontoy2/StringsWithJackson.java index f798ebe..f31e834 100644 --- a/src/test/java/nl/sander/jsontoy2/StringsWithJackson.java +++ b/src/test/java/nl/sander/jsontoy2/StringsWithJackson.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import java.nio.charset.CharacterCodingException; - import static org.junit.jupiter.api.Assertions.*; public class StringsWithJackson { @@ -19,27 +17,25 @@ public class StringsWithJackson { } @Test - public void firstSurrogateButSecondMissing() throws NoSuchFieldException, IllegalAccessException, JsonProcessingException { - String value =jackson.readValue("\"\\uDADA\"", String.class); - - assertTrue(true); + public void firstSurrogateButSecondMissing() throws JsonProcessingException { + String value = jackson.readValue("\"\\uDADA\"", String.class); } @Test - public void incompleteSurrogateAndEscapeValid() throws JsonProcessingException { + public void incompleteSurrogateAndEscapeValid() { assertThrows(JsonParseException.class, () -> jackson.readValue("\"\\uD800\n\"", String.class)); } @Test public void firstValidSurrogateSecondInvalid() throws JsonProcessingException { - String value =jackson.readValue("\"\\uD888\\u1334\"", String.class); + String value = jackson.readValue("\"\\uD888\\u1334\"", String.class); assertTrue(true); } @Test public void escapedDoubleQuote() throws JsonProcessingException { - String value =jackson.readValue("\"\\\"\"", String.class); + String value = jackson.readValue("\"\\\"\"", String.class); assertEquals("\"", value); }