/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package forplay.flash.json.impl; import forplay.core.Asserts; import forplay.flash.json.JsonArray; import forplay.flash.json.JsonException; import forplay.flash.json.JsonFactory; import forplay.flash.json.JsonNumber; import forplay.flash.json.JsonObject; import forplay.flash.json.JsonValue; /** * Implementation of parsing a JSON string into instances of {@link * com.google.gwt.dev.json.JsonValue}. */ class JsonTokenizer { private static final int INVALID_CHAR = -1; private static final String STOPCHARS = ",:]}/\\\"[{;=#"; private JsonFactory jsonFactory; private boolean lenient = true; private int pushBackBuffer = INVALID_CHAR; private final String json; private int position = 0; JsonTokenizer(JreJsonFactory serverJsonFactory, String json) { this.jsonFactory = serverJsonFactory; this.json = json; } void back(char c) { Asserts.checkState(pushBackBuffer == INVALID_CHAR); pushBackBuffer = c; } void back(int c) { back((char) c); } int next() { if (pushBackBuffer != INVALID_CHAR) { final int c = pushBackBuffer; pushBackBuffer = INVALID_CHAR; return c; } return position < json.length() ? json.charAt(position++) : INVALID_CHAR; } String next(int n) throws JsonException { if (n == 0) { return ""; } char[] buffer = new char[n]; int pos = 0; if (pushBackBuffer != INVALID_CHAR) { buffer[0] = (char) pushBackBuffer; pos = 1; pushBackBuffer = INVALID_CHAR; } int len; while ((pos < n) && ((len = read(buffer, pos, n - pos)) != -1)) { pos += len; } if (pos < n) { throw new JsonException("TODO"/* TODO(knorton): Add message. */); } return String.valueOf(buffer); } int nextNonWhitespace() { while (true) { final int c = next(); if (!isSpace((char) c)) { return c; } } } String nextString(int startChar) throws JsonException { final StringBuffer buffer = new StringBuffer(); int c = next(); Asserts.check(c == '"' || (lenient && c == '\'')); while (true) { c = next(); switch (c) { case '\r': case '\n': throw new JsonException(""); case '\\': c = next(); switch (c) { case 'b': buffer.append('\b'); break; case 't': buffer.append('\t'); break; case 'n': buffer.append('\n'); break; case 'f': buffer.append('\f'); break; case 'r': buffer.append('\r'); break; // TODO(knorton): I'm not sure I should even support this escaping // mode since JSON is always UTF-8. case 'u': buffer.append((char) Integer.parseInt(next(4), 16)); break; default: buffer.append((char) c); } break; default: if (c == startChar) { return buffer.toString(); } buffer.append((char) c); } } } String nextUntilOneOf(String chars) { final StringBuffer buffer = new StringBuffer(); int c = next(); while (c != INVALID_CHAR) { if (isSpace((char) c) || chars.indexOf((char) c) >= 0) { back(c); break; } buffer.append((char) c); c = next(); } return buffer.toString(); } <T extends JsonValue> T nextValue() throws JsonException { final int c = nextNonWhitespace(); back(c); JsonValue result; switch (c) { case '"': case '\'': result = jsonFactory.create(nextString(c)); break; case '{': result = parseObject(); break; case '[': result = parseArray(); break; default: result = getValueForLiteral(nextUntilOneOf(STOPCHARS)); break; } @SuppressWarnings("unchecked") T value = (T) result; return value; } JsonArray parseArray() throws JsonException { final JsonArray array = jsonFactory.createArray(); int c = nextNonWhitespace(); Asserts.check(c == '['); while (true) { c = nextNonWhitespace(); switch (c) { case ']': return array; default: back(c); array.set(array.length(), nextValue()); final int d = nextNonWhitespace(); switch (d) { case ']': return array; case ',': break; default: throw new JsonException("Invalid array: expected , or ]"); } } } } JsonObject parseObject() throws JsonException { final JsonObject object = jsonFactory.createObject(); int c = nextNonWhitespace(); if (c != '{') { throw new JsonException( "Payload does not begin with '{'. Got " + c + "(" + Character.valueOf((char) c) + ")"); } while (true) { c = nextNonWhitespace(); switch (c) { case '}': // We're done. return object; case '"': case '\'': back(c); // Ready to start a key. final String key = nextString(c); if (nextNonWhitespace() != ':') { throw new JsonException( "Invalid object: expecting \":\""); } // TODO(knorton): Make sure this key is not already set. object.put(key, nextValue()); switch (nextNonWhitespace()) { case ',': break; case '}': return object; default: throw new JsonException( "Invalid object: expecting } or ,"); } break; case ',': break; default: if (lenient && (Character.isDigit((char) c) || Character.isLetterOrDigit((char) c))) { StringBuffer keyBuffer = new StringBuffer(); keyBuffer.append(c); while (true) { c = next(); if (Character.isDigit((char) c) || Character.isLetterOrDigit((char) c)) { keyBuffer.append(c); } else { back(c); break; } } if (nextNonWhitespace() != ':') { throw new JsonException( "Invalid object: expecting \":\""); } // TODO(knorton): Make sure this key is not already set. object.put(keyBuffer.toString(), nextValue()); switch (nextNonWhitespace()) { case ',': break; case '}': return object; default: throw new JsonException( "Invalid object: expecting } or ,"); } }else{ throw new JsonException("Invalid object: "); } } } } private JsonNumber getNumberForLiteral(String literal) throws JsonException { try { return jsonFactory.create(Double.parseDouble(literal)); } catch (NumberFormatException e) { throw new JsonException("Invalid number literal: " + literal); } } private JsonValue getValueForLiteral(String literal) throws JsonException { if ("".equals(literal)) { throw new JsonException("Missing value"); } if ("null".equals(literal) || "undefined".equals(literal)) { return jsonFactory.createNull(); } if ("true".equals(literal)) { return jsonFactory.create(true); } if ("false".equals(literal)) { return jsonFactory.create(false); } final char c = literal.charAt(0); if (c == '-' || Character.isDigit(c)) { return getNumberForLiteral(literal); } throw new JsonException("Invalid literal: \"" + literal + "\""); } private int read(char[] buffer, int pos, int len) { int maxLen = Math.min(json.length() - position, len); String src = json.substring(position, position + maxLen); char result[] = src.toCharArray(); System.arraycopy(result, 0, buffer, pos, maxLen); position += maxLen; return maxLen; } // TODO: switch to Character.isWhitespace if GWT ever implements it @SuppressWarnings("deprecation") private static boolean isSpace (char c) { return Character.isSpace(c); } }