/* -*- Mode: java; tab-width: 4; indent-tabs-mode: 1; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Rhino code, released * May 6, 1999. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1997-1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Norris Boyd * Raphael Speyer * Hannes Wallnoefer * * Alternatively, the contents of this file may be used under the terms of * the GNU General Public License Version 2 or later (the "GPL"), in which * case the provisions of the GPL are applicable instead of those above. If * you wish to allow use of your version of this file only under the terms of * the GPL and not to allow others to use your version of this file under the * MPL, indicate your decision by deleting the provisions above and replacing * them with the notice and other provisions required by the GPL. If you do * not delete the provisions above, a recipient may use your version of this * file under either the MPL or the GPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.javascript.json; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptRuntime; import java.util.ArrayList; import java.util.List; /** * This class converts a stream of JSON tokens into a JSON value. * * See ECMA 15.12. * @author Raphael Speyer * @author Hannes Wallnoefer */ public class JsonParser { private Context cx; private Scriptable scope; private int pos; private int length; private String src; public JsonParser(Context cx, Scriptable scope) { this.cx = cx; this.scope = scope; } public synchronized Object parseValue(String json) throws ParseException { if (json == null) { throw new ParseException("Input string may not be null"); } pos = 0; length = json.length(); src = json; Object value = readValue(); consumeWhitespace(); if (pos < length) { throw new ParseException("Expected end of stream at char " + pos); } return value; } private Object readValue() throws ParseException { consumeWhitespace(); while (pos < length) { char c = src.charAt(pos++); switch (c) { case '{': return readObject(); case '[': return readArray(); case 't': return readTrue(); case 'f': return readFalse(); case '"': return readString(); case 'n': return readNull(); case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': case '-': return readNumber(c); default: throw new ParseException("Unexpected token: " + c); } } throw new ParseException("Empty JSON string"); } private Object readObject() throws ParseException { Scriptable object = cx.newObject(scope); String id; Object value; boolean needsComma = false; consumeWhitespace(); while (pos < length) { char c = src.charAt(pos++); switch(c) { case '}': return object; case ',': if (!needsComma) { throw new ParseException("Unexpected comma in object literal"); } needsComma = false; break; case '"': if (needsComma) { throw new ParseException("Missing comma in object literal"); } id = readString(); consume(':'); value = readValue(); long index = ScriptRuntime.indexFromString(id); if (index < 0) { object.put(id, object, value); } else { object.put((int)index, object, value); } needsComma = true; break; default: throw new ParseException("Unexpected token in object literal"); } consumeWhitespace(); } throw new ParseException("Unterminated object literal"); } private Object readArray() throws ParseException { List<Object> list = new ArrayList<Object>(); boolean needsComma = false; consumeWhitespace(); while (pos < length) { char c = src.charAt(pos); switch(c) { case ']': pos += 1; return cx.newArray(scope, list.toArray()); case ',': if (!needsComma) { throw new ParseException("Unexpected comma in array literal"); } needsComma = false; pos += 1; break; default: if (needsComma) { throw new ParseException("Missing comma in array literal"); } list.add(readValue()); needsComma = true; } consumeWhitespace(); } throw new ParseException("Unterminated array literal"); } private String readString() throws ParseException { StringBuilder b = new StringBuilder(); while (pos < length) { char c = src.charAt(pos++); if (c <= '\u001F') { throw new ParseException("String contains control character"); } switch(c) { case '\\': if (pos >= length) { throw new ParseException("Unterminated string"); } c = src.charAt(pos++); switch (c) { case '"': b.append('"'); break; case '\\': b.append('\\'); break; case '/': b.append('/'); break; case 'b': b.append('\b'); break; case 'f': b.append('\f'); break; case 'n': b.append('\n'); break; case 'r': b.append('\r'); break; case 't': b.append('\t'); break; case 'u': if (length - pos < 5) { throw new ParseException("Invalid character code: \\u" + src.substring(pos)); } try { b.append((char) Integer.parseInt(src.substring(pos, pos + 4), 16)); pos += 4; } catch (NumberFormatException nfx) { throw new ParseException("Invalid character code: " + src.substring(pos, pos + 4)); } break; default: throw new ParseException("Unexcpected character in string: '\\" + c + "'"); } break; case '"': return b.toString(); default: b.append(c); break; } } throw new ParseException("Unterminated string literal"); } private Number readNumber(char first) throws ParseException { StringBuilder b = new StringBuilder(); b.append(first); while (pos < length) { char c = src.charAt(pos); if (!Character.isDigit(c) && c != '-' && c != '+' && c != '.' && c != 'e' && c != 'E') { break; } pos += 1; b.append(c); } String num = b.toString(); int numLength = num.length(); try { // check for leading zeroes for (int i = 0; i < numLength; i++) { char c = num.charAt(i); if (Character.isDigit(c)) { if (c == '0' && numLength > i + 1 && Character.isDigit(num.charAt(i + 1))) { throw new ParseException("Unsupported number format: " + num); } break; } } final double dval = Double.parseDouble(num); final int ival = (int)dval; if (ival == dval) { return Integer.valueOf(ival); } else { return Double.valueOf(dval); } } catch (NumberFormatException nfe) { throw new ParseException("Unsupported number format: " + num); } } private Boolean readTrue() throws ParseException { if (length - pos < 3 || src.charAt(pos) != 'r' || src.charAt(pos + 1) != 'u' || src.charAt(pos + 2) != 'e') { throw new ParseException("Unexpected token: t"); } pos += 3; return Boolean.TRUE; } private Boolean readFalse() throws ParseException { if (length - pos < 4 || src.charAt(pos) != 'a' || src.charAt(pos + 1) != 'l' || src.charAt(pos + 2) != 's' || src.charAt(pos + 3) != 'e') { throw new ParseException("Unexpected token: f"); } pos += 4; return Boolean.FALSE; } private Object readNull() throws ParseException { if (length - pos < 3 || src.charAt(pos) != 'u' || src.charAt(pos + 1) != 'l' || src.charAt(pos + 2) != 'l') { throw new ParseException("Unexpected token: n"); } pos += 3; return null; } private void consumeWhitespace() { while (pos < length) { char c = src.charAt(pos); switch (c) { case ' ': case '\t': case '\r': case '\n': pos += 1; break; default: return; } } } private void consume(char token) throws ParseException { consumeWhitespace(); if (pos >= length) { throw new ParseException("Expected " + token + " but reached end of stream"); } char c = src.charAt(pos++); if (c == token) { return; } else { throw new ParseException("Expected " + token + " found " + c); } } public static class ParseException extends Exception { ParseException(String message) { super(message); } ParseException(Exception cause) { super(cause); } } }