mirror of
https://github.com/servalproject/serval-dna.git
synced 2024-12-27 08:32:33 +00:00
502 lines
13 KiB
Java
502 lines
13 KiB
Java
/**
|
|
* Copyright (C) 2014 Serval Project Inc.
|
|
*
|
|
* This file is part of Serval Software (http://www.servalproject.org)
|
|
*
|
|
* Serval Software is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This source code is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this source code; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
package org.servalproject.json;
|
|
|
|
import java.lang.StringBuilder;
|
|
import java.lang.NumberFormatException;
|
|
import java.io.IOException;
|
|
import java.io.Reader;
|
|
import java.io.PushbackReader;
|
|
import java.util.Collection;
|
|
|
|
public class JSONTokeniser {
|
|
|
|
PushbackReader reader;
|
|
Object pushedToken;
|
|
|
|
private static final boolean DUMP_JSON_TO_STDERR = false;
|
|
|
|
public enum Token {
|
|
START_OBJECT,
|
|
END_OBJECT,
|
|
START_ARRAY,
|
|
END_ARRAY,
|
|
COMMA,
|
|
COLON,
|
|
NULL,
|
|
EOF
|
|
};
|
|
|
|
public static class SyntaxException extends JSONInputException
|
|
{
|
|
public SyntaxException(String message) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
public static class UnexpectedException extends JSONInputException
|
|
{
|
|
public UnexpectedException(String got) {
|
|
super("unexpected " + got);
|
|
}
|
|
|
|
public UnexpectedException(String got, Class expecting) {
|
|
super("unexpected " + got + ", expecting " + expecting.getName());
|
|
}
|
|
|
|
public UnexpectedException(String got, Object expecting) {
|
|
super("unexpected " + got + ", expecting " + jsonTokenDescription(expecting));
|
|
}
|
|
|
|
}
|
|
|
|
public static class UnexpectedEOFException extends UnexpectedException
|
|
{
|
|
public UnexpectedEOFException(Class expecting) {
|
|
super("EOF", expecting);
|
|
}
|
|
|
|
public UnexpectedEOFException(Object expecting) {
|
|
super("EOF", expecting);
|
|
}
|
|
|
|
}
|
|
|
|
public static class UnexpectedTokenException extends UnexpectedException
|
|
{
|
|
public UnexpectedTokenException(Object got) {
|
|
super(jsonTokenDescription(got));
|
|
}
|
|
|
|
public UnexpectedTokenException(Object got, Class expecting) {
|
|
super(jsonTokenDescription(got), expecting);
|
|
}
|
|
|
|
public UnexpectedTokenException(Object got, Object expecting) {
|
|
super(jsonTokenDescription(got), expecting);
|
|
}
|
|
|
|
}
|
|
|
|
// Can accept any PushbackReader, because we only need one character of unread().
|
|
public JSONTokeniser(PushbackReader pbrd)
|
|
{
|
|
reader = pbrd;
|
|
}
|
|
|
|
public JSONTokeniser(Reader rd)
|
|
{
|
|
reader = new PushbackReader(rd);
|
|
}
|
|
|
|
private int _read() throws IOException
|
|
{
|
|
int n = this.reader.read();
|
|
if (DUMP_JSON_TO_STDERR && n != -1)
|
|
System.err.print((char)n);
|
|
return n;
|
|
}
|
|
|
|
private int _read(char[] buf, int offset, int length) throws IOException
|
|
{
|
|
int n = this.reader.read(buf, offset, length);
|
|
if (DUMP_JSON_TO_STDERR && n != -1)
|
|
System.err.print(new String(buf, offset, n));
|
|
return n;
|
|
}
|
|
|
|
public static void unexpected(Object tok) throws UnexpectedTokenException
|
|
{
|
|
throw new UnexpectedTokenException(tok);
|
|
}
|
|
|
|
public static void match(Object tok, Token exactly) throws SyntaxException
|
|
{
|
|
if (tok != exactly)
|
|
throw new SyntaxException("JSON syntax error: expecting " + exactly + ", got " + jsonTokenDescription(tok));
|
|
}
|
|
|
|
public void consume(Token exactly) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
match(nextToken(), exactly);
|
|
}
|
|
|
|
public enum Narrow {
|
|
NO_NULL,
|
|
ALLOW_NULL
|
|
};
|
|
|
|
public static boolean supportsNarrowTo(Class cls) {
|
|
return cls == Boolean.class
|
|
|| cls == Integer.class
|
|
|| cls == Long.class
|
|
|| cls == Float.class
|
|
|| cls == Double.class
|
|
|| cls == String.class;
|
|
}
|
|
|
|
public static Object narrow(Object tok, Narrow opts) throws UnexpectedException
|
|
{
|
|
return narrow(tok, Object.class, opts);
|
|
}
|
|
|
|
public static <T> T narrow(Object tok, Class<T> cls) throws UnexpectedException
|
|
{
|
|
return narrow(tok, cls, Narrow.NO_NULL);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public static <T> T narrow(Object tok, Class<T> cls, Narrow opts) throws UnexpectedException
|
|
{
|
|
assert !cls.isAssignableFrom(Token.class); // can only narrow to values
|
|
if (tok == Token.EOF)
|
|
throw new UnexpectedEOFException(cls);
|
|
if (opts == Narrow.ALLOW_NULL && (tok == null || tok == Token.NULL))
|
|
return null;
|
|
if (tok instanceof Token)
|
|
throw new UnexpectedTokenException(tok, cls);
|
|
// Convert:
|
|
// Integer --> Long or Float or Double
|
|
// Long --> Float or Double
|
|
// Float --> Double
|
|
// Double --> Float
|
|
if (cls == Double.class && (tok instanceof Float || tok instanceof Long || tok instanceof Integer))
|
|
tok = new Double(((Number)tok).doubleValue());
|
|
else if (cls == Float.class && (tok instanceof Double || tok instanceof Long || tok instanceof Integer))
|
|
tok = new Float(((Number)tok).floatValue());
|
|
else if (cls == Long.class && tok instanceof Integer)
|
|
tok = new Long(((Number)tok).longValue());
|
|
if (cls.isInstance(tok))
|
|
return (T)tok; // unchecked cast
|
|
throw new UnexpectedTokenException(tok, cls);
|
|
}
|
|
|
|
public <T> T consume(Class<T> cls) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return consume(cls, Narrow.NO_NULL);
|
|
}
|
|
|
|
public <T> T consume(Class<T> cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return narrow(nextToken(), cls, opts);
|
|
}
|
|
|
|
public Object consume() throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return consume(Object.class, Narrow.NO_NULL);
|
|
}
|
|
|
|
public Object consume(Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return consume(Object.class, opts);
|
|
}
|
|
|
|
public String consume(String exactly) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
String tok = consume(String.class);
|
|
if (tok.equals(exactly))
|
|
return tok;
|
|
throw new UnexpectedTokenException(tok, exactly);
|
|
}
|
|
|
|
public int consumeArray(Collection<Object> collection, Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return consumeArray(collection, Object.class, opts);
|
|
}
|
|
|
|
public <T> int consumeArray(Collection<T> collection, Class<T> cls) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
return consumeArray(collection, cls, Narrow.NO_NULL);
|
|
}
|
|
|
|
public <T> int consumeArray(Collection<T> collection, Class<T> cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
int added = 0;
|
|
consume(Token.START_ARRAY);
|
|
Object tok = nextToken();
|
|
if (tok != Token.END_ARRAY) {
|
|
while (true) {
|
|
collection.add(narrow(tok, cls, opts));
|
|
++added;
|
|
tok = nextToken();
|
|
if (tok == Token.END_ARRAY)
|
|
break;
|
|
match(tok, Token.COMMA);
|
|
tok = nextToken();
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
public void consumeArray(Object[] array) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
consumeArray(array, Object.class, Narrow.NO_NULL);
|
|
}
|
|
|
|
public void consumeArray(Object[] array, Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
consumeArray(array, Object.class, opts);
|
|
}
|
|
|
|
public <T> void consumeArray(T[] array, Class<T> cls, Narrow opts) throws SyntaxException, UnexpectedException, IOException
|
|
{
|
|
consume(Token.START_ARRAY);
|
|
for (int i = 0; i < array.length; ++i) {
|
|
if (i != 0)
|
|
consume(Token.COMMA);
|
|
array[i] = consume(cls, opts);
|
|
}
|
|
consume(Token.END_ARRAY);
|
|
}
|
|
|
|
public static boolean jsonIsToken(Object tok)
|
|
{
|
|
return tok instanceof Token
|
|
|| tok instanceof String
|
|
|| tok instanceof Double
|
|
|| tok instanceof Long
|
|
|| tok instanceof Integer
|
|
|| tok instanceof Boolean;
|
|
}
|
|
|
|
public static String jsonTokenDescription(Object tok)
|
|
{
|
|
if (tok == null)
|
|
return "null";
|
|
if (tok instanceof String)
|
|
return "\"" + ((String)tok).replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
|
if (tok instanceof Number)
|
|
return "" + tok;
|
|
if (tok instanceof Boolean)
|
|
return "" + tok;
|
|
assert tok instanceof Token;
|
|
return tok.toString();
|
|
}
|
|
|
|
private void readWord(String word) throws SyntaxException, IOException
|
|
{
|
|
int len = 0;
|
|
while (len < word.length()) {
|
|
char[] buf = new char[word.length() - len];
|
|
int n = _read(buf, 0, buf.length);
|
|
if (n == -1)
|
|
throw new SyntaxException("EOF in middle of \"" + word + "\"");
|
|
for (int i = 0; i < n; ++i)
|
|
if (buf[i] != word.charAt(len++))
|
|
throw new SyntaxException("expecting \"" + word + "\"");
|
|
}
|
|
}
|
|
|
|
private int readHex(int digits) throws SyntaxException, IOException
|
|
{
|
|
assert digits <= 8;
|
|
char[] buf = new char[digits];
|
|
int len = 0;
|
|
while (len < buf.length) {
|
|
int n = _read(buf, len, buf.length - len);
|
|
if (n == -1)
|
|
throw new SyntaxException("EOF in middle of " + digits + " hex digits");
|
|
len += n;
|
|
}
|
|
String hex = new String(buf);
|
|
try {
|
|
return Integer.valueOf(hex, 16);
|
|
}
|
|
catch (NumberFormatException e) {
|
|
}
|
|
throw new SyntaxException("expecting " + digits + " hex digits, got \"" + hex + "\"");
|
|
}
|
|
|
|
public void pushToken(Object tok)
|
|
{
|
|
assert jsonIsToken(tok);
|
|
assert pushedToken == null;
|
|
pushedToken = tok;
|
|
}
|
|
|
|
public Object nextToken() throws SyntaxException, IOException
|
|
{
|
|
if (pushedToken != null) {
|
|
Object tok = pushedToken;
|
|
pushedToken = null;
|
|
return tok;
|
|
}
|
|
while (true) {
|
|
int c = _read();
|
|
switch (c) {
|
|
case -1:
|
|
return Token.EOF;
|
|
case '\t':
|
|
case '\r':
|
|
case '\n':
|
|
case ' ':
|
|
break;
|
|
case '{':
|
|
return Token.START_OBJECT;
|
|
case '}':
|
|
return Token.END_OBJECT;
|
|
case '[':
|
|
return Token.START_ARRAY;
|
|
case ']':
|
|
return Token.END_ARRAY;
|
|
case ',':
|
|
return Token.COMMA;
|
|
case ':':
|
|
return Token.COLON;
|
|
case 't':
|
|
this.reader.unread(c);
|
|
readWord("true");
|
|
return Boolean.TRUE;
|
|
case 'f':
|
|
this.reader.unread(c);
|
|
readWord("false");
|
|
return Boolean.FALSE;
|
|
case 'n':
|
|
this.reader.unread(c);
|
|
readWord("null");
|
|
return Token.NULL;
|
|
case '"': {
|
|
StringBuilder sb = new StringBuilder();
|
|
boolean slosh = false;
|
|
while (true) {
|
|
c = _read();
|
|
if (c == -1)
|
|
throw new SyntaxException("unexpected EOF in JSON string");
|
|
if (slosh) {
|
|
switch (c) {
|
|
case '"': case '/': case '\\': sb.append((char)c); break;
|
|
case 'b': sb.append('\b'); break;
|
|
case 'f': sb.append('\f'); break;
|
|
case 'n': sb.append('\n'); break;
|
|
case 'r': sb.append('\r'); break;
|
|
case 't': sb.append('\t'); break;
|
|
case 'u': sb.append((char)readHex(4)); break;
|
|
default: throw new SyntaxException("malformed JSON string");
|
|
}
|
|
slosh = false;
|
|
}
|
|
else {
|
|
switch (c) {
|
|
case '"':
|
|
return sb.toString();
|
|
case '\\':
|
|
slosh = true;
|
|
break;
|
|
default:
|
|
sb.append((char)c);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case '-': {
|
|
StringBuilder sb = new StringBuilder();
|
|
if (c == '-') {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
if (c == '0') {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
else if (Character.isDigit(c)) {
|
|
do {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
while (Character.isDigit(c));
|
|
}
|
|
else
|
|
throw new SyntaxException("malformed JSON number");
|
|
boolean isfloat = false;
|
|
if (c == '.') {
|
|
isfloat = true;
|
|
sb.append((char)c);
|
|
c = _read();
|
|
if (c == -1)
|
|
throw new SyntaxException("unexpected EOF in JSON number");
|
|
if (!Character.isDigit(c))
|
|
throw new SyntaxException("malformed JSON number");
|
|
do {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
while (Character.isDigit(c));
|
|
}
|
|
if (c == 'e' || c == 'E') {
|
|
isfloat = true;
|
|
sb.append((char)c);
|
|
c = _read();
|
|
if (c == '+' || c == '-') {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
if (c == -1)
|
|
throw new SyntaxException("unexpected EOF in JSON number");
|
|
if (!Character.isDigit(c))
|
|
throw new SyntaxException("malformed JSON number");
|
|
do {
|
|
sb.append((char)c);
|
|
c = _read();
|
|
}
|
|
while (Character.isDigit(c));
|
|
}
|
|
this.reader.unread(c);
|
|
String number = sb.toString();
|
|
try {
|
|
if (isfloat)
|
|
return Double.parseDouble(number);
|
|
else {
|
|
try {
|
|
return Integer.parseInt(number);
|
|
}
|
|
catch (NumberFormatException e) {
|
|
}
|
|
return Long.parseLong(number);
|
|
}
|
|
}
|
|
catch (NumberFormatException e) {
|
|
throw new SyntaxException("malformed JSON number: " + number);
|
|
}
|
|
}
|
|
default:
|
|
throw new SyntaxException("malformed JSON: '" + (char)c + "'");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void close() throws IOException
|
|
{
|
|
this.reader.close();
|
|
}
|
|
|
|
}
|