/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.everrest.core.impl.provider.json;
import org.everrest.core.impl.provider.json.JsonUtils.JsonToken;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.Reader;
import java.nio.charset.Charset;
import static org.everrest.core.util.StringUtils.contains;
public class JsonParser {
private static final int END_OF_STREAM = 0;
/** JsonHandler will serve events from parser. */
private final JsonHandler eventHandler;
/** Stack of JSON tokens. */
private final JsonStack<JsonToken> stack;
/** @see {@link java.io.PushbackReader}. */
private PushbackReader pushbackReader;
public JsonParser() {
this(new JsonHandler());
}
protected JsonParser(JsonHandler eventHandler) {
this.eventHandler = eventHandler;
stack = new JsonStack<>();
}
public void parse(Reader reader) throws JsonException {
pushbackReader = new PushbackReader(reader);
eventHandler.reset();
stack.clear();
char c;
while ((c = next()) != END_OF_STREAM) {
if (c == '{') {
readObject();
} else if (c == '[') {
readArray();
} else {
throw new JsonException(String.format("Syntax error. Unexpected '%s'. Must be '{'.", c));
}
c = assertNextIs(",]}");
if (c != END_OF_STREAM) {
pushBack(c);
}
}
if (!stack.isEmpty()) {
throw new JsonException("Syntax error. Missing one or more close bracket(s).");
}
}
public void parse(InputStream stream) throws JsonException {
parse(new InputStreamReader(stream, Charset.forName("UTF-8")));
}
/**
* Get result of parsing.
*
* @return parsed JSON value
*/
public JsonValue getJsonObject() {
return eventHandler.getJsonObject();
}
/**
* Read JSON object token, it minds all characters from '{' to '}'.
*
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private void readObject() throws JsonException {
char c;
startObject();
while (true) {
switch (c = next()) {
case END_OF_STREAM:
throw new JsonException("Syntax error. Unexpected end of object. Object must end by '}'.");
case '}':
endObject();
return;
case '{':
readObject();
break;
case '[':
readArray();
break;
case ']':
endArray();
break;
case ',':
break;
default:
pushBack(c);
readKey();
assertNextIs(':');
c = next();
pushBack(c);
if (c != '{' && c != '[') {
readValue();
}
break;
}
}
}
private void endObject() throws JsonException {
if (JsonToken.object == stack.pop()) {
eventHandler.endObject();
} else {
throw new JsonException("Syntax error. Unexpected end of object.");
}
}
private void startObject() {
eventHandler.startObject();
stack.push(JsonToken.object);
}
/**
* Read JSON array token, it minds all characters from '[' to ']'.
*
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private void readArray() throws JsonException {
char c;
startArray();
while (true) {
switch (c = next()) {
case END_OF_STREAM:
throw new JsonException("Syntax error. Unexpected end of array. Array must end by ']'.");
case ']':
endArray();
return;
case '[':
readArray();
break;
case '{':
readObject();
break;
case '}':
endObject();
break;
case ',':
break;
default:
pushBack(c);
readValue();
break;
}
}
}
private void startArray() {
eventHandler.startArray();
stack.push(JsonToken.array);
}
private void endArray() throws JsonException {
if (JsonToken.array == stack.pop()) {
eventHandler.endArray();
} else {
throw new JsonException("Syntax error. Unexpected end of array.");
}
}
/**
* Read key from stream.
*
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private void readKey() throws JsonException {
char c = next();
if (c != '"') {
throw new JsonException(String.format("Syntax error. Key must start from quote, but found '%s'.", c));
}
pushBack(c);
String key = new String(nextString());
if (key.length() == 2) {
throw new JsonException("Missing key.");
}
eventHandler.key(key.substring(1, key.length() - 1));
}
/**
* Read value from stream.
*
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private void readValue() throws JsonException {
char c = next();
pushBack(c);
if (c == '"') {
eventHandler.characters(nextString());
} else {
CharArrayWriter charArrayWriter = new CharArrayWriter();
while (true) {
c = next();
if (c == END_OF_STREAM) {
throw new JsonException("Unexpected end of stream.");
} else if (contains("{[,]}\"", c)) {
break;
}
charArrayWriter.append(c);
}
pushBack(c);
eventHandler.characters(charArrayWriter.toCharArray());
}
c = assertNextIs(",]}");
pushBack(c);
}
/**
* Get next char from stream, skipping whitespace and comments. Comments: One
* line comment from // to end of line; Multi-line comments from ⁄* to *⁄
*
* @return the next char.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private char next() throws JsonException {
try {
int c;
while ((c = pushbackReader.read()) != -1) {
if (c == '/') {
c = pushbackReader.read();
if (c == '/') {
skipLine();
} else if (c == '*') {
skipComment();
}
} else if (c > ' ') {
break;
}
}
return (c == -1) ? END_OF_STREAM : (char)c;
} catch (IOException e) {
throw new JsonException(e.getMessage(), e);
}
}
private void skipLine() throws IOException {
int c;
do {
c = pushbackReader.read();
} while (c != -1 && c != '\n' && c != '\r');
}
private void skipComment() throws IOException, JsonException {
int c;
while (true) {
c = pushbackReader.read();
if (c == '*') {
c = pushbackReader.read();
if (c == '/') {
break;
}
}
if (c == -1) {
throw new JsonException("Syntax error. Missing end of comment.");
}
}
}
/**
* Get next char from stream.
*
* @return the next char.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private char nextAny() throws JsonException {
try {
int c = pushbackReader.read();
return (c == -1) ? END_OF_STREAM : (char)c;
} catch (IOException e) {
throw new JsonException(e.getMessage(), e);
}
}
/**
* Get next char from stream. And check is this char equals expected.
*
* @param expectedCharacter
* the expected char.
* @return the next char.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private char assertNextIs(char expectedCharacter) throws JsonException {
char c = next();
if (c == END_OF_STREAM || c == expectedCharacter) {
return c;
}
throw new JsonException(String.format("Expected for '%s' but found '%s'.", expectedCharacter, c));
}
/**
* Get next char from stream. And check is this char presents in given string.
*
* @param expectedCharacters
* the string.
* @return the next char.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private char assertNextIs(String expectedCharacters) throws JsonException {
char c = next();
if (c == END_OF_STREAM || contains(expectedCharacters, c)) {
return c;
}
char[] chars = expectedCharacters.toCharArray();
StringBuilder errorMessage = new StringBuilder("Expected ");
for (int i = 0; i < chars.length; i++) {
if (i > 0) {
errorMessage.append(" or ");
}
errorMessage.append('\'').append(chars[i]).append('\'');
}
errorMessage.append(" but found '").append(c).append('\'');
throw new JsonException(errorMessage.toString());
}
/**
* Get array chars up to given and include it.
*
* @return the char array.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private char[] nextString() throws JsonException {
CharArrayWriter charArrayWriter = new CharArrayWriter();
char c = nextAny(); // read '"'
charArrayWriter.append(c);
while (true) {
switch (c = nextAny()) {
case END_OF_STREAM:
case '\n':
case '\r':
throw new JsonException("Syntax error. Unterminated string");
case '\\':
switch (c = nextAny()) {
case END_OF_STREAM:
case '\n':
case '\r':
throw new JsonException("Syntax error. Unterminated string");
case 'n':
charArrayWriter.append('\n');
break;
case 'r':
charArrayWriter.append('\r');
break;
case 'b':
charArrayWriter.append('\b');
break;
case 't':
charArrayWriter.append('\t');
break;
case 'f':
charArrayWriter.append('\f');
break;
case 'u': // unicode
charArrayWriter.append(readUnicodeCharacter());
break;
default:
charArrayWriter.append(c);
break;
}
break;
default:
charArrayWriter.append(c);
if (c == '"') {
return charArrayWriter.toCharArray();
}
break;
}
}
}
private char readUnicodeCharacter() throws JsonException {
char[] buff = new char[4];
try {
int i = pushbackReader.read(buff);
if (i != 4) {
throw new JsonException("Unexpected end of stream.");
}
} catch (IOException e) {
throw new JsonException(e.getMessage(), e);
}
String unicodeString = new String(buff);
int c;
try {
c = Integer.parseInt(unicodeString, 16);
} catch (NumberFormatException e) {
throw new JsonException(String.format("Invalid unicode character %s", unicodeString));
}
return (char)c;
}
/**
* Push back given char to stream.
*
* @param c
* the char for pushing back.
* @throws JsonException
* if JSON document has wrong format or i/o error occurs.
*/
private void pushBack(char c) throws JsonException {
try {
pushbackReader.unread(c);
} catch (IOException e) {
throw new JsonException(e.getMessage(), e);
}
}
}