/** * * Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); You may not * use this file except in compliance with the License. You may obtain a copy of * the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.speedment.common.json.internal; import com.speedment.common.json.JsonSyntaxException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** * Internal class that parses a stream of JSON characters into an object * representation. * <p> * This implementation includes a line and column counter that makes it easier * to debug an errenous JSON string. These might add additional overhead to the * parsing time and should therefore only be used if the input is expected to * have errors. * * @author Emil Forslund * @since 1.0.0 */ public final class JsonDeserializer implements AutoCloseable { private final static String ENCODING = "UTF-8"; private final static int TAB_SIZE = 4; private final InputStreamReader reader; private final AtomicLong row; private final AtomicLong col; private int character; public JsonDeserializer(InputStream in) throws UnsupportedEncodingException { reader = new InputStreamReader(in, ENCODING); row = new AtomicLong(0); col = new AtomicLong(0); } private enum CloseMethod { EXIT_FROM_PARENT, CONTINUE_IN_PARENT, NOT_DECIDED } public Object get() throws IOException { switch (nextNonBlankspace()) { case 0x7B : // { (begin parsing object) return parseObject(); case 0x5B : // [ (begin parsing array) return parseArray(); case 0x22 : // " (begin parsing string) return parseString(); case 0x66 : // f (begin parsing false) return parseFalse(); case 0x74 : // t (begin parsing true) return parseTrue(); case 0x6E : // n (begin parsing null) return parseNull(); // Digit '0 - 9' case 0x30 : case 0x31 : case 0x32 : case 0x33 : case 0x34 : case 0x35 : case 0x36 : case 0x37 : case 0x38 : case 0x39 : case 0x2E : // . (decimal sign) case 0x2D : // - (minus sign) final AtomicReference<Number> number = new AtomicReference<>(); if (parseNumber(number::set) == CloseMethod.NOT_DECIDED) { return number.get(); } } throw unexpectedCharacterException(); } private Map<String, Object> parseObject() throws IOException { final Map<String, Object> object = new LinkedHashMap<>(); firstChar: switch (nextNonBlankspace()) { // If the map should be closed with no entries: case 0x7D : // } (close the map) return object; // If this character begins a new entry case 0x22 : // " (begin key) final CloseMethod close = parseEntryInto(object); switch (close) { case EXIT_FROM_PARENT : if (character == 0x7D) { // } return object; } else { throw unexpectedCharacterException(); } case CONTINUE_IN_PARENT : break firstChar; case NOT_DECIDED : switch (nextNonBlankspace()) { case 0x2C : // , (continue with next entry) break firstChar; case 0x7D : // } (close the map) return object; default : throw unexpectedCharacterException(); } default : throw new IllegalStateException( "Unknown enum constant '" + close + "'." ); } // If the first non-whitespace character was not neither // a '}' nor a '"': default : throw unexpectedCharacterException(); } // Parse each remaining entry while ((nextNonBlankspace()) == 0x22) { // " final CloseMethod close = parseEntryInto(object); switch (close) { case EXIT_FROM_PARENT: if (character == 0x7D) { // } return object; } else { throw unexpectedCharacterException(); } case CONTINUE_IN_PARENT: continue; case NOT_DECIDED: switch (nextNonBlankspace()) { case 0x2C: // , (continue with next entry) continue; case 0x7D: // } (close the map) return object; default: throw unexpectedCharacterException(); } default: throw new IllegalStateException( "Unknown enum constant '" + close + "'." ); } } throw unexpectedCharacterException(); } private CloseMethod parseEntryInto(Map<String, Object> object) throws IOException { final StringBuilder keyBuilder = new StringBuilder(); parseKey: while (true) { switch (next()) { // If this is an escape character, add the following character // without parsing it. case 0x5C : // backslash keyBuilder.append(Character.toChars(next())); continue; // If this terminates the key, break the loop. case 0x22 : // " (end key) break parseKey; // Every other character should be added to the key. default : keyBuilder.append(Character.toChars(character)); } } final String key = keyBuilder.toString(); // Read the assignment operator if (nextNonBlankspace() != 0x3A) { // : throw unexpectedCharacterException(); } // Read the value switch (nextNonBlankspace()) { case 0x7B : // { (begin parsing object) if (object.put(key, parseObject()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; case 0x5B : // [ (begin parsing array) if (object.put(key, parseArray()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; case 0x22 : // " (begin parsing string) if (object.put(key, parseString()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; case 0x66 : // f (begin parsing false) if (object.put(key, parseFalse()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; case 0x74 : // t (begin parsing true) if (object.put(key, parseTrue()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; case 0x6E : // n (begin parsing null) if (object.put(key, parseNull()) != null) { throw duplicateKeyException(key); } return CloseMethod.NOT_DECIDED; // Digit '0 - 9' case 0x30 : case 0x31 : case 0x32 : case 0x33 : case 0x34 : case 0x35 : case 0x36 : case 0x37 : case 0x38 : case 0x39 : case 0x2E : // . (decimal sign) case 0x2D : // - (minus sign) return parseNumber(num -> { if (object.put(key, num) != null) { throw duplicateKeyException(key); } }); default : throw unexpectedCharacterException(); } } private List<Object> parseArray() throws IOException { final List<Object> list = new LinkedList<>(); firstChar: switch (nextNonBlankspace()) { // If the list should be closed with no entries: case 0x5D : // ] (close the list) return list; case 0x7B : // { (begin parsing object) list.add(parseObject()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } case 0x5B : // [ (begin parsing array) list.add(parseArray()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } case 0x22 : // " (begin parsing string) list.add(parseString()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } case 0x66 : // f (begin parsing false) list.add(parseFalse()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } case 0x74 : // t (begin parsing true) list.add(parseTrue()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } case 0x6E : // n (begin parsing null) list.add(parseNull()); switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; // nextEntry case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } // Digit '0 - 9' case 0x30 : case 0x31 : case 0x32 : case 0x33 : case 0x34 : case 0x35 : case 0x36 : case 0x37 : case 0x38 : case 0x39 : case 0x2E : // . (decimal sign) case 0x2D : // - (minus sign) final CloseMethod method = parseNumber(list::add); switch (method) { case CONTINUE_IN_PARENT : break firstChar; case EXIT_FROM_PARENT : if (character == 0x5D) { // ] return list; } else { throw unexpectedCharacterException(); } case NOT_DECIDED : switch (nextNonBlankspace()) { case 0x2C : // , (continue with next element) break firstChar; case 0x5D : // ] (close the list) return list; default : throw unexpectedCharacterException(); } default : throw new IllegalStateException( "Unknown enum constant '" + method + "'." ); } default : throw unexpectedCharacterException(); } // Parse each remaining entry while (true) { switch (nextNonBlankspace()) { case 0x7B: // { (begin parsing object) list.add(parseObject()); break; case 0x5B: // [ (begin parsing array) list.add(parseArray()); break; case 0x22: // " (begin parsing string) list.add(parseString()); break; case 0x66: // f (begin parsing false) list.add(parseFalse()); break; case 0x74: // t (begin parsing true) list.add(parseTrue()); break; case 0x6E: // n (begin parsing null) list.add(parseNull()); break; // Digit '0 - 9' case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: case 0x2E: // . (decimal sign) case 0x2D: // - (minus sign) final CloseMethod method = parseNumber(list::add); switch (method) { case CONTINUE_IN_PARENT: continue; // nextEntry case EXIT_FROM_PARENT: if (character == 0x5D) { // ] return list; } else { throw unexpectedCharacterException(); } case NOT_DECIDED: switch (nextNonBlankspace()) { case 0x2C: // , (continue with next element) continue; // nextEntry case 0x5D: // ] (close the list) return list; default: throw unexpectedCharacterException(); } default: throw new IllegalStateException( "Unknown enum constant '" + method + "'." ); } default: throw unexpectedCharacterException(); } switch (nextNonBlankspace()) { case 0x2C: // , (continue with next element) continue; // nextEntry case 0x5D: // ] (close the list) return list; default: throw unexpectedCharacterException(); } } } private String parseString() throws IOException { final StringBuilder builder = new StringBuilder(); while (true) { switch (next()) { // If this is an escape character, add the following character // without parsing it. case 0x5C: // backslash builder.append(Character.toChars(next())); continue; // If this terminates the string, break the loop. case 0x22: // " (end key) return builder.toString(); // Every other character should be added to the key. default: builder.append(Character.toChars(character)); } } } private Boolean parseFalse() throws IOException { if (next() == 0x61 // a && next() == 0x6C // l && next() == 0x73 // s && next() == 0x65) { // e return Boolean.FALSE; } else { throw unexpectedCharacterException(); } } private Boolean parseTrue() throws IOException { if (next() == 0x72 // r && next() == 0x75 // u && next() == 0x65) { // e return Boolean.TRUE; } else { throw unexpectedCharacterException(); } } private Object parseNull() throws IOException { if (next() == 0x75 // u && next() == 0x6C // l && next() == 0x6C) { // l return null; } else { throw unexpectedCharacterException(); } } private CloseMethod parseNumber(Consumer<Number> consumer) throws IOException { final StringBuilder builder = new StringBuilder(); final CloseMethod method; parser: while (true) { switch (character) { // . (decimal sign) case 0x2E : builder.append('.'); return parseNumberDecimal(consumer, builder); // - (minus sign) case 0x2D : builder.append('-'); next(); continue; // Digit '0 - 9' case 0x30 : case 0x31 : case 0x32 : case 0x33 : case 0x34 : case 0x35 : case 0x36 : case 0x37 : case 0x38 : case 0x39 : builder.append(Character.toChars(character)); next(); continue; case 0x7D : // } case 0x5D : // ] method = CloseMethod.EXIT_FROM_PARENT; break parser; case 0x2C : // , method = CloseMethod.CONTINUE_IN_PARENT; break parser; case 0x0A : // new line row.incrementAndGet(); col.set(-1); method = CloseMethod.NOT_DECIDED; break parser; case 0x09 : // tab col.addAndGet(TAB_SIZE - 1); method = CloseMethod.NOT_DECIDED; break parser; case 0x20 : // space case 0x0D : // return (ignore) method = CloseMethod.NOT_DECIDED; break parser; default : throw unexpectedCharacterException(); } } consumer.accept(Long.parseLong(builder.toString())); return method; } private CloseMethod parseNumberDecimal(Consumer<Number> consumer, StringBuilder builder) throws IOException { final CloseMethod method; parser: while (true) { switch (next()) { // Digit '0 - 9' case 0x30 : case 0x31 : case 0x32 : case 0x33 : case 0x34 : case 0x35 : case 0x36 : case 0x37 : case 0x38 : case 0x39 : builder.append(Character.toChars(character)); continue; case 0x7D : // } case 0x5D : // ] method = CloseMethod.EXIT_FROM_PARENT; break parser; case 0x2C : // , method = CloseMethod.CONTINUE_IN_PARENT; break parser; case 0x0A : // new line row.incrementAndGet(); col.set(-1); method = CloseMethod.NOT_DECIDED; break parser; case 0x09 : // tab col.addAndGet(TAB_SIZE - 1); method = CloseMethod.NOT_DECIDED; break parser; case 0x20 : // space case 0x0D : // return (ignore) method = CloseMethod.NOT_DECIDED; break parser; default : throw unexpectedCharacterException(); } } final String result = builder.toString(); if (result.equals(".")) { throw new JsonSyntaxException(row, col, "Unexpected character '.'" ); } consumer.accept(Double.parseDouble(builder.toString())); return method; } private int nextNonBlankspace() throws IOException { while ((character = reader.read()) != -1) { col.incrementAndGet(); switch (character) { case 0x0A : // new line row.incrementAndGet(); col.set(-1); continue; case 0x09 : // tab col.addAndGet(TAB_SIZE - 1); continue; case 0x20 : // space case 0x0D : // return (ignore) continue; default : return character; } } throw unexpectedEndOfStreamException(); } private int next() throws IOException { if ((character = reader.read()) != -1) { col.incrementAndGet(); switch (character) { case 0x0A : // new line row.incrementAndGet(); col.set(-1); break; case 0x09 : // tab col.addAndGet(TAB_SIZE - 1); break; } return character; } throw unexpectedEndOfStreamException(); } private JsonSyntaxException unexpectedCharacterException() { final String c = new String(Character.toChars(character)); throw new JsonSyntaxException(row, col, "Unexpected character '" + c + "' (Unicode: " + codePoints(c) + ")" ); } private JsonSyntaxException duplicateKeyException(String key) { throw new JsonSyntaxException(row, col, "Duplicate key '" + key + "'" ); } private JsonSyntaxException unexpectedEndOfStreamException() { throw new JsonSyntaxException(row, col, "Unexpected end of stream" ); } private String codePoints(String c) { final StringJoiner str = new StringJoiner(" "); for (int i = 0; i < c.length(); i++) { str.add(String.valueOf(Character.codePointAt(c, i))); } return str.toString(); } @Override public void close() { try { reader.close(); } catch (final IOException ex) { throw new RuntimeException("Failed to safely close stream.", ex); } } }