package com.skaringa.json.parser;
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Stack;
import org.xml.sax.Attributes;
import com.skaringa.javaxml.DeserializerException;
import com.skaringa.javaxml.PropertyKeys;
import com.skaringa.javaxml.handler.AttrImpl;
import com.skaringa.javaxml.handler.sax.ObjectDeserializerHolder;
import com.skaringa.javaxml.impl.PropertyHelper;
import com.skaringa.javaxml.serializers.ComponentSerializer;
import com.skaringa.javaxml.serializers.SerializerRegistry;
import com.skaringa.util.Log;
/**
* Parse JSON into Java objects.
*
*/
public class JsonParser {
private static final char QUOTE = '"';
private static final char END_ARRAY = ']';
private static final char BEGIN_ARRAY = '[';
private static final char END_OBJECT = '}';
private static final char BEGIN_OBJECT = '{';
private static final char COLON = ':';
private static final char COMMA = ',';
private StreamTokenizer m_tokenizer;
private Stack _objHolderStack = new Stack();
private Class _rootType;
private String _name;
private ClassLoader _classLoader;
private Map _propertyMap = new HashMap();
private static final Attributes emptyAttrs = new AttrImpl();
private static final String endOfValue = new String(new char[] {END_ARRAY, END_OBJECT, COMMA});
/**
* Create a new parser.
*
* @param reader
* The reader that produces the JSON.
* @param rootType
* The type of the root object. If null, then LinkedList or HashMap
* is used.
* @param classLoader
* The class loader used to instantiate the Java objects.
*/
public JsonParser(Reader reader, Class rootType, ClassLoader classLoader) {
m_tokenizer = new StreamTokenizer(reader);
setSyntax();
_rootType = rootType;
_classLoader = classLoader;
}
/**
* Create a new parser.
*
* @param reader
* The reader that produces the JSON.
* @param rootType
* The type of the root object. If null, then LinkedList or HashMap
* is used.
* @param propertyMap
* The properties to control the parsing.
* @param classLoader
* The class loader used to instantiate the Java objects.
*/
public JsonParser(Reader reader, Class rootType, Map propertyMap,
ClassLoader classLoader) {
m_tokenizer = new StreamTokenizer(reader);
setSyntax();
_rootType = rootType;
_propertyMap = propertyMap;
_classLoader = classLoader;
}
/**
* Parse the JSON and instantiate the Java object tree.
*
* @throws IOException
* If the reader fails.
* @throws DeserializerException
* If the desrialization failes (e.g. because of invalid JSON).
*/
public void process() throws IOException, DeserializerException {
int type = m_tokenizer.nextToken();
while (type != StreamTokenizer.TT_EOF) {
processToken(type);
type = m_tokenizer.nextToken();
}
}
/**
* Set the type of the root object to be deserialized.
*
* @param type
* The class of the root object.
*/
public void setRootType(Class type) {
_rootType = type;
}
/**
* Get the deserialized object tree. The type of object is the same as
* rootType. If rootType is null, then the type is Collection or Map depending
* on the JSON parsed.
*
* @return The new Object.
*/
public Object getObject() {
return ((ObjectDeserializerHolder) _objHolderStack.peek()).getObj();
}
/**
* Set the class loader used to load classes during deserialization.
*
* @param loader
* The class loader.
*/
public void setClassLoader(ClassLoader loader) {
_classLoader = loader;
}
/**
* Get the class loader used to load classes during deserialization.
*
* @return The class loader.
*/
public ClassLoader getClassLoader() {
return _classLoader;
}
private void setSyntax() {
m_tokenizer.resetSyntax();
m_tokenizer.eolIsSignificant(false);
m_tokenizer.slashSlashComments(false);
m_tokenizer.slashStarComments(false);
m_tokenizer.ordinaryChar(QUOTE);
m_tokenizer.ordinaryChar(COLON);
m_tokenizer.ordinaryChar(BEGIN_OBJECT);
m_tokenizer.ordinaryChar(END_OBJECT);
m_tokenizer.ordinaryChar(BEGIN_ARRAY);
m_tokenizer.ordinaryChar(END_ARRAY);
m_tokenizer.wordChars('a', 'z');
m_tokenizer.wordChars('A', 'Z');
m_tokenizer.wordChars('0', '9');
m_tokenizer.wordChars('-', '.');
m_tokenizer.wordChars('+', '+');
m_tokenizer.whitespaceChars(' ', ' ');
m_tokenizer.whitespaceChars('\t', '\t');
m_tokenizer.whitespaceChars('\r', '\r');
m_tokenizer.whitespaceChars('\n', '\n');
}
private void processToken(int type) throws IOException, DeserializerException {
if (type == BEGIN_OBJECT) {
beginObject();
} else if (type == END_OBJECT) {
endObject();
} else if (type == BEGIN_ARRAY) {
beginArray();
} else if (type == END_ARRAY) {
endArray();
} else if (type == QUOTE) {
String sval = readQuotedString();
if (test(COLON)) {
// sval is a key
name(sval);
} else {
// sval is a string value
string(sval);
expectEndOfValue();
}
} else if (type == StreamTokenizer.TT_WORD) {
// sval is a numeric or boolean or null value
numberOrBooleanOrNull(m_tokenizer.sval);
expectEndOfValue();
} else if (type == COMMA) {
// eat
} else {
throw new DeserializerException("Unexpected token: "
+ (type == StreamTokenizer.TT_WORD ? m_tokenizer.sval : String
.valueOf((char) type)));
}
}
/**
* Read a quoted string. Unfortunately StreamTokenizer doesn't handle unicode
* sequences, like \u0000, correctly.
*
* @return The string without leading and trailing quotes.
* @throws IOException
*/
private String readQuotedString() throws IOException {
m_tokenizer.resetSyntax();
StringBuffer sval = new StringBuffer();
int type = m_tokenizer.nextToken();
while (type != QUOTE) {
if (type == StreamTokenizer.TT_EOF) {
throw new IOException("EOF in quoted string");
} else if (type == StreamTokenizer.TT_WORD) {
sval.append(m_tokenizer.sval);
} else if (type == '\\') {
type = m_tokenizer.nextToken();
switch (type) {
case '"':
sval.append('"');
break;
case '\\':
sval.append('\\');
break;
case '/':
sval.append('/');
break;
case 'b':
sval.append('\b');
break;
case 'f':
sval.append('\f');
break;
case 'n':
sval.append('\n');
break;
case 'r':
sval.append('\r');
break;
case 't':
sval.append('\t');
break;
case 'u':
sval.append(readUnicodeSeq());
break;
default:
sval.append((char) type);
break;
}
} else {
sval.append((char) type);
}
type = m_tokenizer.nextToken();
}
setSyntax();
return sval.toString();
}
private char readUnicodeSeq() throws IOException {
char[] buf = new char[4];
for (int i = 0; i < 4; ++i) {
int tok = m_tokenizer.nextToken();
if ((tok >= '0' && tok <= '9') || (tok >= 'a' && tok <= 'f')
|| (tok >= 'A' && tok <= 'F')) {
buf[i] = (char) tok;
} else {
throw new IOException("invalid unicode sequence");
}
}
return (char) Integer.parseInt(new String(buf), 16);
}
private void name(String sval) throws DeserializerException {
Log.debug("name", sval);
ObjectDeserializerHolder objHolder = (ObjectDeserializerHolder) _objHolderStack.peek();
if (END_OBJECT != objHolder.getJsonStructureTag()) {
throw new DeserializerException("wrong containment of ':' inside array.");
}
_name = sval;
}
private void beginObject() throws IOException, DeserializerException {
Log.debug("beginObject");
begin(HashMap.class, END_OBJECT);
}
private void endObject() throws DeserializerException {
Log.debug("endObject");
end(null, END_OBJECT);
}
private void beginArray() throws DeserializerException {
Log.debug("beginArray");
begin(LinkedList.class, END_ARRAY);
}
private void endArray() throws DeserializerException {
Log.debug("endArray");
end(null, END_ARRAY);
}
private void begin(Class fallbackType, char jsonStructureTag)
throws DeserializerException {
if (_rootType == null) {
// root type not set - deserializing into fallbackType
_rootType = fallbackType;
}
Object parent = getParent();
ComponentSerializer ser;
if (parent == null) {
ser = SerializerRegistry.getInstance().getSerializer(_rootType);
} else {
try {
ser = SerializerRegistry.getInstance().findDeserializer(parent, _name);
} catch (DeserializerException e) {
// use fallbackType
ser = SerializerRegistry.getInstance().getSerializer(fallbackType);
}
}
Object obj = ser.startDeserialize(_name, emptyAttrs, parent,
_objHolderStack, _classLoader);
_objHolderStack.push(new ObjectDeserializerHolder(obj, ser, _name,
jsonStructureTag));
_name = null;
}
private Object getParent() {
// get parent
Object parent = null;
try {
parent = getObject();
} catch (java.util.EmptyStackException e) {
// no parent object available
}
return parent;
}
private void end(String val, char jsonStructureTag)
throws DeserializerException {
ObjectDeserializerHolder objHolder = (ObjectDeserializerHolder) _objHolderStack
.pop();
if (jsonStructureTag != objHolder.getJsonStructureTag()) {
throw new DeserializerException("Wrong sequence: Expected '"
+ jsonStructureTag + "' but was '" + objHolder.getJsonStructureTag()
+ "'");
}
Object obj = objHolder.getSer().endDeserialize(objHolder.getObj(), val);
objHolder.setObj(obj);
try {
ObjectDeserializerHolder parentHolder = (ObjectDeserializerHolder) _objHolderStack
.peek();
parentHolder.getSer().setMember(parentHolder.getObj(),
objHolder.getName(), obj);
} catch (java.util.EmptyStackException e) {
// leave the top level object at the stack
_objHolderStack.push(objHolder);
} catch (NoSuchFieldException e) {
if (!PropertyHelper.parseBoolean(_propertyMap,
PropertyKeys.SKIP_UNKNOWN_FIELDS)) {
throw new DeserializerException("no such field: " + e.getMessage());
}
}
_name = null;
}
private void string(String sval) throws DeserializerException {
Log.debug("string", sval);
begin(String.class, QUOTE);
end(sval, QUOTE);
}
private void numberOrBooleanOrNull(String sval) throws DeserializerException {
Log.debug("value", sval);
Object parent = getParent();
ComponentSerializer ser;
boolean isNull = false;
if ("null".equals(sval)) {
// null
ser = SerializerRegistry.getInstance().getSerializer(Object.class);
isNull = true;
} else if ("true".equals(sval)) {
// boolean true
ser = SerializerRegistry.getInstance().getSerializer(Boolean.class);
} else if ("false".equals(sval)) {
// boolean false
ser = SerializerRegistry.getInstance().getSerializer(Boolean.class);
} else {
// number
try {
// find deserializer from parent's member
ser = SerializerRegistry.getInstance().findDeserializer(parent, _name);
} catch (DeserializerException e) {
// if not found guess numeric type
ser = SerializerRegistry.getInstance().guessDeserializerForNumber(sval);
}
}
Object obj = null;
if (!isNull) {
obj = ser.startDeserialize(_name, emptyAttrs, parent, _objHolderStack,
_classLoader);
}
_objHolderStack.push(new ObjectDeserializerHolder(obj, ser, _name, '\0'));
end(sval, '\0');
}
private boolean test(int type) throws IOException {
int isType = m_tokenizer.nextToken();
if (isType != type) {
m_tokenizer.pushBack();
}
return (isType == type);
}
private void expectEndOfValue() throws DeserializerException,
IOException {
int token = m_tokenizer.nextToken();
if (endOfValue.indexOf((char) token) < 0) {
throw new DeserializerException("Parsing error: Expected one of '" + endOfValue
+ "' but got '" + (char) token + "'");
}
m_tokenizer.pushBack();
}
}