/*
* Copyright (C) 2009 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.ws.frameworks.json.impl;
import org.exoplatform.ws.frameworks.json.JsonHandler;
import org.exoplatform.ws.frameworks.json.JsonParser;
import org.exoplatform.ws.frameworks.json.impl.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.util.Stack;
/**
* @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
* @version $Id: JsonParserImpl.java 34417 2009-07-23 14:42:56Z dkatayev $
*/
public class JsonParserImpl implements JsonParser
{
/**
* JsonHandler will serve events from parser.
*/
private JsonHandler jsonHandler;
/**
* @see {@link java.io.PushbackReader} .
*/
private PushbackReader pushbackReader;
/**
* Stack of JSON tokens.
*/
private Stack<JsonToken> jsonTokens = new Stack<JsonToken>();
/**
* Default constructor.
*/
public JsonParserImpl()
{
}
/**
* {@inheritDoc}
*/
public void parse(Reader reader, JsonHandler handler) throws JsonException
{
jsonHandler = handler;
pushbackReader = new PushbackReader(reader);
try
{
char c = 0;
while ((c = next()) != 0)
{
if (c == '{')
readObject();
else
throw new JsonException("Syntax error. Unexpected '" + c + "'. Must be '{'.");
}
if (!jsonTokens.isEmpty())
throw new JsonException("Syntax error. Missing one or more close bracket(s).");
}
catch (Exception e)
{
throw new JsonException(e);
}
}
/**
* {@inheritDoc}
*/
public void parse(InputStream sream, JsonHandler handler) throws JsonException
{
parse(new InputStreamReader(sream, JsonUtils.DEFAULT_CHARSET), handler);
}
/**
* 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 = 0;
// inform handler about start of object
jsonHandler.startObject();
jsonTokens.push(JsonToken.object);
for (;;)
{
switch (c = next())
{
case 0 :
throw new JsonException("Syntax error. Unexpected end of object. Object must end by '}'.");
case '{' :
readObject();
break;
case '}' :
// inform handler about end of object
jsonHandler.endObject();
// check is allowed end of object now
if (JsonToken.object != jsonTokens.pop())
throw new JsonException("Syntax error. Unexpected end of object.");
// check is allowed char after end of json object
switch (c = next())
{
// end of stream
case 0 :
break;
case ',' :
case ']' :
case '}' :
back(c);
break;
default :
// must not happen
throw new JsonException("Syntax error. Excpected " + "for ',' or ']' or '}' but found '" + c
+ "'.");
}
return; // end for(;;)
case '[' :
readArray();
break;
case ',' : // nothing to do just must not be default is switch
break;
default :
back(c);
// all characters from start object to ':' - key.
readKey();
next(':');
c = next();
back(c);
// object/array/value
if (c != '{' && c != '[')
readValue();
break;
}
}
}
/**
* 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 = 0;
// inform handler about start of array
jsonHandler.startArray();
jsonTokens.push(JsonToken.array);
for (;;)
{
switch (c = next())
{
case 0 :
throw new JsonException("Syntax error. Unexpected end of array. Array must end by ']'.");
case ']' :
// inform handler about end of array
jsonHandler.endArray();
// check is allowed end of array now
if (JsonToken.array != jsonTokens.pop())
throw new JsonException("Syntax error. Unexpected end of array.");
// check is allowed char after end of json array
c = next(",]}");
back(c);
return; // end for(;;)
case '[' :
readArray();
break;
case '{' :
readObject();
break;
case ',' : // nothing to do just must not be default
break;
default :
back(c);
readValue();
break;
}
}
}
/**
* 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("Syntax error. Key must start from quote, but found '" + c + "'.");
back(c);
String s = new String(nextString());
// if key as ""
if (s.length() == 2)
throw new JsonException("Missing key.");
jsonHandler.key(s.substring(1, s.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();
back(c);
if (c == '"')
{
// value will be read as string
jsonHandler.characters(nextString());
}
else
{
// not string (numeric or boolean or null)
CharArrayWriter cw = new CharArrayWriter();
while ("{[,]}\"".indexOf(c = next()) < 0)
{
// Bug : WS-66
if (c == 0)
throw new JsonException("Unexpected end of stream.");
cw.append(c);
}
back(c);
jsonHandler.characters(cw.toCharArray());
}
c = next(",]}");
back(c);
}
/**
* Get next char from stream, skipping whitespace and comments. Comments: One
* line comment from // to end of line; Multi-line comments from / and * to *
* and /
*
* @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 = 0;
while ((c = pushbackReader.read()) != -1)
{
if (c == '/')
{
c = pushbackReader.read();
if (c == '/')
{
do
{
c = pushbackReader.read();
}
while (c != -1 && c != '\n' && c != '\r');
}
else if (c == '*')
{
for (;;)
{
c = pushbackReader.read();
if (c == '*')
{
c = pushbackReader.read();
if (c == '/')
break;
}
if (c == -1)
{
throw new JsonException("Syntax error. Missing end of comment.");
}
}
}
else
{
back((char)c);
return '/';
}
}
else if (c == -1 || c > ' ')
break;
}
return (c == -1) ? 0 : (char)c;
}
catch (IOException e)
{
throw new JsonException(e);
}
}
/**
* 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) ? 0 : (char)c;
}
catch (IOException e)
{
throw new JsonException(e);
}
}
/**
* Get next char from stream. And check is this char equals expected.
*
* @param c the expected char.
* @return the next char.
* @throws JsonException if JSON document has wrong format or i/o error
* occurs.
*/
private char next(char c) throws JsonException
{
char n = next();
if (n != c)
throw new JsonException("Expected for '" + c + "' but found '" + n + "'.");
return n;
}
/**
* Get next char from stream. And check is this char presents in given string.
*
* @param s the string.
* @return the next char.
* @throws JsonException if JSON document has wrong format or i/o error
* occurs.
*/
private char next(String s) throws JsonException
{
char n = next();
// if char present in string
if (s.indexOf(n) >= 0)
return n;
// else error
char[] ch = s.toCharArray();
StringBuffer sb = new StringBuffer();
int i = 0;
for (char c : ch)
{
if (i > 0)
sb.append(" or ");
i++;
sb.append('\'').append(c).append('\'');
}
throw new JsonException("Expected for " + sb.toString() + " but found '" + n + "'.");
}
/**
* Get next n characters from stream.
*
* @param n the number of characters.
* @return the array of characters.
* @throws JsonException if JSON document has wrong format or i/o error
* occurs.
*/
private char[] next(int n) throws JsonException
{
char[] buff = new char[n];
try
{
int i = pushbackReader.read(buff);
if (i == -1)
throw new JsonException("Unexpected end of stream.");
return buff;
}
catch (Exception e)
{
throw new JsonException(e);
}
}
/**
* 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 cw = new CharArrayWriter();
char c = nextAny(); // read '"'
cw.append(c);
for (;;)
{
switch (c = nextAny())
{
case 0 :
case '\n' :
case '\r' :
throw new JsonException("Syntax error. Unterminated string.");
case '\\' :
switch (c = nextAny())
{
case 0 :
case '\n' :
case '\r' :
throw new JsonException("Syntax error. Unterminated string");
case 'n' :
cw.append('\n');
break;
case 'r' :
cw.append('\r');
break;
case 'b' :
cw.append('\b');
break;
case 't' :
cw.append('\t');
break;
case 'f' :
cw.append('\f');
break;
case 'u' : // unicode
String s = new String(next(4));
cw.append((char)Integer.parseInt(s, 16));
break;
default :
cw.append(c);
break;
}
break;
default :
cw.append(c);
if (c == '"')
return cw.toCharArray();
break;
}
}
}
/**
* 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 back(char c) throws JsonException
{
try
{
pushbackReader.unread(c);
}
catch (Exception e)
{
throw new JsonException(e);
}
}
}