Repository: tapestry-5 Updated Branches: refs/heads/5.4.x 840ada09d -> d0bc57159
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONStringer.java ---------------------------------------------------------------------- diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONStringer.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONStringer.java new file mode 100644 index 0000000..8612635 --- /dev/null +++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONStringer.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 org.apache.tapestry5.json; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most + * application developers should use those methods directly and disregard this + * API. For example:<pre> + * JSONObject object = ... + * String json = object.toString();</pre> + * + * <p>Stringers only encode well-formed JSON strings. In particular: + * <ul> + * <li>The stringer must have exactly one top-level array or object. + * <li>Lexical scopes must be balanced: every call to {@link #array} must + * have a matching call to {@link #endArray} and every call to {@link + * #object} must have a matching call to {@link #endObject}. + * <li>Arrays may not contain keys (property names). + * <li>Objects must alternate keys (property names) and values. + * <li>Values are inserted with either literal {@link #value(Object) value} + * calls, or by nesting arrays or objects. + * </ul> + * Calls that would result in a malformed JSON string will fail with a + * {@link RuntimeException}. + * + * <p>This class provides no facility for pretty-printing (ie. indenting) + * output. To encode indented output, use {@link JSONObject#toString(int)} or + * {@link JSONArray#toString(int)}. + * + * <p>Some implementations of the API support at most 20 levels of nesting. + * Attempts to create more than 20 levels of nesting may fail with a {@link + * RuntimeException}. + * + * <p>Each stringer may be used to encode a single top level value. Instances of + * this class are not thread safe. Although this class is nonfinal, it was not + * designed for inheritance and should not be subclassed. In particular, + * self-use by overrideable methods is not specified. See <i>Effective Java</i> + * Item 17, "Design and Document or inheritance or else prohibit it" for further + * information. + */ +class JSONStringer { + + /** + * The output data, containing at most one top-level array or object. + */ + final StringBuilder out = new StringBuilder(); + + /** + * Lexical scoping elements within this stringer, necessary to insert the + * appropriate separator characters (ie. commas and colons) and to detect + * nesting errors. + */ + enum Scope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + EMPTY_ARRAY, + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no keys or values requires no separators or newlines + * before it is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + DANGLING_KEY, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * A special bracketless array needed by JSONStringer.join() and + * JSONObject.quote() only. Not used for JSON encoding. + */ + NULL, + } + + /** + * Unlike the original implementation, this stack isn't limited to 20 + * levels of nesting. + */ + private final List<Scope> stack = new ArrayList<Scope>(); + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private final String indent; + + JSONStringer() { + indent = null; + } + + JSONStringer(int indentSpaces) { + char[] indentChars = new char[indentSpaces]; + Arrays.fill(indentChars, ' '); + indent = new String(indentChars); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + JSONStringer open(Scope empty, String openBracket){ + if (stack.isEmpty() && out.length() > 0) { + throw new RuntimeException("Nesting problem: multiple top-level roots"); + } + beforeValue(); + stack.add(empty); + out.append(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + JSONStringer close(Scope empty, Scope nonempty, String closeBracket){ + Scope context = peek(); + if (context != nonempty && context != empty) { + throw new RuntimeException("Nesting problem"); + } + + stack.remove(stack.size() - 1); + if (context == nonempty) { + newline(); + } + out.append(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + */ + private Scope peek(){ + if (stack.isEmpty()) { + throw new RuntimeException("Nesting problem"); + } + return stack.get(stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(Scope topOfStack) { + stack.set(stack.size() - 1, topOfStack); + } + + + void string(String value) { + out.append("\""); + char currentChar = 0; + + for (int i = 0, length = value.length(); i < length; i++) { + char previousChar = currentChar; + currentChar = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + */ + switch (currentChar) { + case '"': + case '\\': + out.append('\\').append(currentChar); + break; + + case '/': + // it makes life easier for HTML embedding of javascript if we escape </ sequences + if (previousChar == '<') { + out.append('\\'); + } + out.append(currentChar); + break; + + case '\t': + out.append("\\t"); + break; + + case '\b': + out.append("\\b"); + break; + + case '\n': + out.append("\\n"); + break; + + case '\r': + out.append("\\r"); + break; + + case '\f': + out.append("\\f"); + break; + + default: + if (currentChar <= 0x1F || (currentChar >= 0x0080 && currentChar < 0x00a0) || (currentChar >= 0x2000 && currentChar < 0x2100)) { + out.append(String.format("\\u%04x", (int) currentChar)); + } else { + out.append(currentChar); + } + break; + } + + } + out.append("\""); + } + + private void newline() { + if (indent == null) { + return; + } + + out.append("\n"); + for (int i = 0; i < stack.size(); i++) { + out.append(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + private void beforeValue(){ + if (stack.isEmpty()) { + return; + } + + Scope context = peek(); + if (context == Scope.EMPTY_ARRAY) { // first in array + replaceTop(Scope.NONEMPTY_ARRAY); + newline(); + } else if (context == Scope.NONEMPTY_ARRAY) { // another in array + out.append(','); + newline(); + } else if (context == Scope.DANGLING_KEY) { // value for key + out.append(indent == null ? ":" : ": "); + replaceTop(Scope.NONEMPTY_OBJECT); + } else if (context != Scope.NULL) { + throw new RuntimeException("Nesting problem"); + } + } + + /** + * Returns the encoded JSON string. + * + * <p>If invoked with unterminated arrays or unclosed objects, this method's + * return value is undefined. + * + * <p><strong>Warning:</strong> although it contradicts the general contract + * of {@link Object#toString}, this method returns null if the stringer + * contains no data. + */ + @Override + public String toString() { + return out.length() == 0 ? null : out.toString(); + } +} http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java ---------------------------------------------------------------------- diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java index f40730b..38fb443 100644 --- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java +++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java @@ -1,397 +1,514 @@ -// Copyright 2007 The Apache Software Foundation -// -// 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. +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 org.apache.tapestry5.json; -/* - Copyright (c) 2002 JSON.org - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - The Software shall be used for Good, not Evil. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// Note: this class was written without inspecting the non-free org.json sourcecode. + +import java.io.IOException; +import java.io.Reader; /** - * A JSONTokener takes a source string and extracts characters and tokens from it. It is used by the JSONObject and - * JSONArray constructors to parse JSON source strings. + * Parses a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) + * encoded string into the corresponding object. Most clients of + * this class will use only need the {@link #JSONTokener(String) constructor} + * and {@link #nextValue} method. Example usage: <pre> + * String json = "{" + * + " \"query\": \"Pizza\", " + * + " \"locations\": [ 94043, 90210 ] " + * + "}"; + * + * JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); + * String query = object.getString("query"); + * JSONArray locations = object.getJSONArray("locations");</pre> * - * @author JSON.org - * @version 2 + * <p>For best interoperability and performance use JSON that complies with + * RFC 4627. For legacy reasons + * this parser is lenient, so a successful parse does not indicate that the + * input string was valid JSON. All of the following syntax errors will be + * ignored: + * <ul> + * <li>End of line comments starting with {@code //} or {@code #} and ending + * with a newline character. + * <li>C-style comments starting with {@code /*} and ending with + * {@code *}{@code /}. Such comments may not be nested. + * <li>Strings that are unquoted or {@code 'single quoted'}. + * <li>Hexadecimal integers prefixed with {@code 0x} or {@code 0X}. + * <li>Octal integers prefixed with {@code 0}. + * <li>Array elements separated by {@code ;}. + * <li>Unnecessary array separators. These are interpreted as if null was the + * omitted value. + * <li>Key-value pairs separated by {@code =} or {@code =>}. + * <li>Key-value pairs separated by {@code ;}. + * </ul> + * + * <p>Each tokener may be used to parse a single JSON string. Instances of this + * class are not thread safe. Although this class is nonfinal, it was not + * designed for inheritance and should not be subclassed. In particular, + * self-use by overrideable methods is not specified. See <i>Effective Java</i> + * Item 17, "Design and Document or inheritance or else prohibit it" for further + * information. */ -class JSONTokener -{ +class JSONTokener { /** - * The index of the next character. + * The input JSON. */ - private int index; + private final String in; /** - * The source string being tokenized. + * The index of the next character to be returned by {@link #next}. When + * the input is exhausted, this equals the input's length. */ - private final String source; + private int pos; /** - * Construct a JSONTokener from a string. - * - * @param source A source string, in JSON format. + * @param in JSON encoded string. Null is not permitted and will yield a + * tokener that throws {@code NullPointerExceptions} when methods are + * called. */ - public JSONTokener(String source) - { - assert source != null; - - index = 0; - this.source = source; + JSONTokener(String in) { + // consume an optional byte order mark (BOM) if it exists + if (in != null && in.startsWith("\ufeff")) { + in = in.substring(1); + } + this.in = in; } - /** - * Back up one character. This provides a sort of lookahead capability, so that you can test for a digit or letter - * before attempting to parse the next number or identifier. - */ - public void back() - { - if (index > 0) - { - index -= 1; + JSONTokener(Reader input) throws IOException { + StringBuilder s = new StringBuilder(); + char[] readBuf = new char[102400]; + int n = input.read(readBuf); + while (n >= 0) { + s.append(readBuf, 0, n); + n = input.read(readBuf); } + in = s.toString(); + pos = 0; } /** - * Determine if the source string still contains characters that next() can consume. + * Returns the next value from the input. * - * @return true if not yet at the end of the source. + * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, + * Integer, Long, Double or {@link JSONObject#NULL}. + * @throws RuntimeException if the input is malformed. */ - public boolean more() - { - return index < source.length(); + Object nextValue(Class<?> desiredType) { + int c = nextCleanInternal(); + if (JSONObject.class.equals(desiredType) && c != '{'){ + throw syntaxError("A JSONObject text must begin with '{'"); + } + if (JSONArray.class.equals(desiredType) && c != '['){ + throw syntaxError("A JSONArray text must start with '['"); + } + switch (c) { + case -1: + throw syntaxError("End of input"); + + case '{': + return readObject(); + + case '[': + return readArray(); + + case '\'': + case '"': + return nextString((char) c); + + default: + pos--; + return readLiteral(); + } } - /** - * Get the next character in the source string. - * - * @return The next character, or 0 if past the end of the source string. - */ - public char next() - { - if (more()) - { - return source.charAt(index++); + private int nextCleanInternal() { + while (pos < in.length()) { + int c = in.charAt(pos++); + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == in.length()) { + return c; + } + + char peek = in.charAt(pos); + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + int commentEnd = in.indexOf("*/", pos); + if (commentEnd == -1) { + pos = in.length(); + throw syntaxError("Unclosed comment"); + } + pos = commentEnd + 2; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't + * specify this behavior, but it's required to parse + * existing documents. See http://b/2571423. + */ + skipToEndOfLine(); + continue; + + default: + return c; + } } - return 0; + return -1; } /** - * Get the next n characters. - * - * @param n The number of characters to take. - * @return A string of n characters. - * @throws RuntimeException Substring bounds error if there are not n characters remaining in the source string. + * Advances the position until after the next newline character. If the line + * is terminated by "\r\n", the '\n' must be consumed as whitespace by the + * caller. */ - public String next(int n) - { - int i = index; - int j = i + n; - if (j >= source.length()) - { - throw syntaxError("Substring bounds error"); + private void skipToEndOfLine() { + for (; pos < in.length(); pos++) { + char c = in.charAt(pos); + if (c == '\r' || c == '\n') { + pos++; + break; + } } - index += n; - return source.substring(i, j); } /** - * Get the next char in the string, skipping whitespace and comments (slashslash, slashstar, and hash). + * Returns the string up to but not including {@code quote}, unescaping any + * character escape sequences encountered along the way. The opening quote + * should have already been read. This consumes the closing quote, but does + * not include it in the returned string. * - * @return A character, or 0 if there are no more characters. - * @throws RuntimeException + * @param quote either ' or ". + * @return The unescaped string. + * @throws RuntimeException if the string isn't terminated by a closing quote correctly. */ - public char nextClean() - { - for (; ;) - { - char c = next(); - if (c == '/') - { - switch (next()) - { - case '/': - do - { - c = next(); - } while (c != '\n' && c != '\r' && c != 0); - - break; - case '*': - - while (true) - { - c = next(); - if (c == 0) - { - throw syntaxError("Unclosed comment"); - } - if (c == '*') - { - if (next() == '/') - { - break; - } - back(); - } - } - break; - - default: - back(); - return '/'; + private String nextString(char quote) { + /* + * For strings that are free of escape sequences, we can just extract + * the result as a substring of the input. But if we encounter an escape + * sequence, we need to use a StringBuilder to compose the result. + */ + StringBuilder builder = null; + + /* the index of the first character not yet appended to the builder. */ + int start = pos; + + while (pos < in.length()) { + int c = in.charAt(pos++); + if (c == quote) { + if (builder == null) { + // a new string avoids leaking memory + //noinspection RedundantStringConstructorCall + return new String(in.substring(start, pos - 1)); + } else { + builder.append(in, start, pos - 1); + return builder.toString(); } } - else if (c == '#') - { - do - { - c = next(); - } while (c != '\n' && c != '\r' && c != 0); - } - else if (c == 0 || c > ' ') - { - return c; + + if (c == '\\') { + if (pos == in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(in, start, pos - 1); + builder.append(readEscapeCharacter()); + start = pos; } } + + throw syntaxError("Unterminated string"); } /** - * Return the characters up to the next close quote character. Backslash processing is done. The formal JSON format - * does not allow strings in single quotes, but an implementation is allowed to accept them. - * - * @param quote The quoting character, either <code>"</code> <small>(double quote)</small> or - * <code>'</code> <small>(single quote)</small>. - * @return A String. - * @throws RuntimeException Unterminated string. + * Unescapes the character identified by the character or characters that + * immediately follow a backslash. The backslash '\' should have already + * been read. This supports both unicode escapes "u000A" and two-character + * escapes "\n". */ - public String nextString(char quote) - { - StringBuilder builder = new StringBuilder(); - - while (true) - { - char c = next(); - switch (c) - { - case 0: - case '\n': - case '\r': - throw syntaxError("Unterminated string"); - case '\\': - c = next(); - switch (c) - { - case 'b': - builder.append('\b'); - break; - case 't': - builder.append('\t'); - break; - case 'n': - builder.append('\n'); - break; - case 'f': - builder.append('\f'); - break; - case 'r': - builder.append('\r'); - break; - case 'u': - builder.append((char) Integer.parseInt(next(4), 16)); - break; - case 'x': - builder.append((char) Integer.parseInt(next(2), 16)); - break; - default: - builder.append(c); - } - break; - default: - if (c == quote) - { - return builder.toString(); - } - builder.append(c); + private char readEscapeCharacter() { + char escaped = in.charAt(pos++); + switch (escaped) { + case 'u': { + if (pos + 4 > in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = in.substring(pos, pos + 4); + pos += 4; + try { + return (char) Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { + throw syntaxError("Invalid escape sequence: " + hex); + } } - } - } + case 'x': { + if (pos + 2 > in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = in.substring(pos, pos + 2); + pos += 2; + try { + return (char) Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { + throw syntaxError("Invalid escape sequence: " + hex); + } + } + case 't': + return '\t'; - /** - * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the - * JSONObject.NULL object. - * - * @return An object. - * @throws RuntimeException If syntax error. - */ - public Object nextValue() - { - char c = nextClean(); - String s; + case 'b': + return '\b'; - switch (c) - { - case '"': - case '\'': - return nextString(c); - case '{': - back(); - return new JSONObject(this); - case '[': - back(); - return new JSONArray(this); - } + case 'n': + return '\n'; - /* - * Handle unquoted text. This could be the values true, false, or null, or it can be a - * number. An implementation (such as this one) is allowed to also accept non-standard - * forms. Accumulate characters until we reach the end of the text or a formatting - * character. - */ + case 'r': + return '\r'; - StringBuffer sb = new StringBuffer(); - char b = c; - while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) - { - sb.append(c); - c = next(); + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; } - back(); + } - /* - * If it is true, false, or null, return the proper value. - */ + /** + * Reads a null, boolean, numeric or unquoted string literal value. Numeric + * values will be returned as an Integer, Long, or Double, in that order of + * preference. + */ + private Object readLiteral() { + String literal = nextToInternal("{}[]/\\:,=;# \t\f"); - s = sb.toString().trim(); - if (s.equals("")) - { + if (literal.length() == 0) { throw syntaxError("Missing value"); - } - if (s.equalsIgnoreCase("true")) - { + } else if ("null".equalsIgnoreCase(literal)) { + return JSONObject.NULL; + } else if ("true".equalsIgnoreCase(literal)) { return Boolean.TRUE; - } - if (s.equalsIgnoreCase("false")) - { + } else if ("false".equalsIgnoreCase(literal)) { return Boolean.FALSE; } - if (s.equalsIgnoreCase("null")) - { - return JSONObject.NULL; + + /* try to parse as an integral type... */ + if (literal.indexOf('.') == -1) { + int base = 10; + String number = literal; + if (number.startsWith("0x") || number.startsWith("0X")) { + number = number.substring(2); + base = 16; + } else if (number.startsWith("0") && number.length() > 1) { + number = number.substring(1); + base = 8; + } + try { + long longValue = Long.parseLong(number, base); + if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { + return (int) longValue; + } else { + return longValue; + } + } catch (NumberFormatException e) { + /* + * This only happens for integral numbers greater than + * Long.MAX_VALUE, numbers in exponential form (5e-10) and + * unquoted strings. Fall through to try floating point. + */ + } } - /* - * If it might be a number, try converting it. We support the 0- and 0x- conventions. If a - * number cannot be produced, then the value will just be a string. Note that the 0-, 0x-, - * plus, and implied string conventions are non-standard. A JSON parser is free to accept - * non-JSON forms as long as it accepts all correct JSON forms. - */ + /* ...next try to parse as a floating point... */ + try { + return Double.valueOf(literal); + } catch (NumberFormatException ignored) { + } - if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') - { - if (b == '0') - { - if (s.length() > 2 && (s.charAt(1) == 'x' || s.charAt(1) == 'X')) - { - try - { - return Integer.parseInt(s.substring(2), 16); - } - catch (Exception e) - { - /* Ignore the error */ - } + /* ... finally give up. We have an unquoted string */ + //noinspection RedundantStringConstructorCall + return new String(literal); // a new string avoids leaking memory + } + + /** + * Returns the string up to but not including any of the given characters or + * a newline character. This does not consume the excluded character. + */ + private String nextToInternal(String excluded) { + int start = pos; + for (; pos < in.length(); pos++) { + char c = in.charAt(pos); + if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) { + return in.substring(start, pos); + } + } + return in.substring(start); + } + + /** + * Reads a sequence of key/value pairs and the trailing closing brace '}' of + * an object. The opening brace '{' should have already been read. + */ + private JSONObject readObject() { + JSONObject result = new JSONObject(); + + /* Peek to see if this is the empty object. */ + int first = nextCleanInternal(); + if (first == '}') { + return result; + } else if (first != -1) { + pos--; + } + + while (true) { + Object name = null; + try { + name = nextValue(null); + } catch (RuntimeException e){ + if (e.getMessage().equals("End of input" + this)){ + // hack to maintain compatibility with earlier releases of tapestry-json + throw syntaxError("A JSONObject text must end with '}'"); } - else - { - try - { - return Integer.parseInt(s, 8); - } - catch (Exception e) - { - /* Ignore the error */ - } + throw e; + } + if (!(name instanceof String)) { + if (name == null) { + throw syntaxError("Names cannot be null"); + } else { + throw syntaxError("Names must be strings, but " + name + + " is of type " + name.getClass().getName()); } } - try - { - return Integer.valueOf(s); + + /* + * Expect the name/value separator to be either a colon ':', an + * equals sign '=', or an arrow "=>". The last two are bogus but we + * include them because that's what the original implementation did. + */ + int separator = nextCleanInternal(); + if (separator != ':' && separator != '=') { + throw syntaxError("Expected a ':' after a key"); } - catch (Exception e) - { - try - { - return Long.valueOf(s); - } - catch (Exception f) - { - try - { - return Double.valueOf(s); - } - catch (Exception g) - { - return s; + if (pos < in.length() && in.charAt(pos) == '>') { + pos++; + } + + result.put((String) name, nextValue(null)); + + switch (nextCleanInternal()) { + case '}': + return result; + case ';': + case ',': + continue; + default: + throw syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Reads a sequence of values and the trailing closing brace ']' of an + * array. The opening brace '[' should have already been read. Note that + * "[]" yields an empty array, but "[,]" returns a two-element array + * equivalent to "[null,null]". + */ + private JSONArray readArray() { + JSONArray result = new JSONArray(); + + /* to cover input that ends with ",]". */ + boolean hasTrailingSeparator = false; + + while (true) { + switch (nextCleanInternal()) { + case -1: + throw syntaxError("Expected a ',' or ']'"); + case ']': + if (hasTrailingSeparator) { + //result.put(null); } - } + return result; + case ',': + case ';': + /* A separator without a value first means "null". */ + result.put(JSONObject.NULL); + hasTrailingSeparator = true; + continue; + default: + pos--; + } + + result.put(nextValue(null)); + + switch (nextCleanInternal()) { + case ']': + return result; + case ',': + case ';': + hasTrailingSeparator = true; + continue; + default: + throw syntaxError("Expected a ',' or ']'"); } } - return s; } /** - * Make a JSONException to signal a syntax error. + * Returns an exception containing the given message plus the current + * position and the entire input string. * - * @param message The error message. - * @return A JSONException object, suitable for throwing + * @param message The message we want to include. + * @return An exception that we can throw. */ - RuntimeException syntaxError(String message) - { - return new RuntimeException(message + toString()); + private RuntimeException syntaxError(String message) { + return new RuntimeException(message + this); } /** - * Make a printable string of this JSONTokener. - * - * @return " at character [myIndex] of [mySource]" + * Returns the current position and the entire input string. */ @Override - public String toString() - { - return " at character " + index + " of " + source; + public String toString() { + // consistent with the original implementation + return " at character " + pos + " of " + in; } + } http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/package-info.java ---------------------------------------------------------------------- diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/package-info.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/package-info.java index 5a757e7..464082c 100644 --- a/tapestry-json/src/main/java/org/apache/tapestry5/json/package-info.java +++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/package-info.java @@ -11,7 +11,7 @@ // limitations under the License. /** - * Repackaged, improved (and tested) version of code originally from json.org. + * Repackaged, improved (and tested) version of code originally https://github.com/tdunning/open-json * * Starting in release 5.4, {@link org.apache.tapestry5.json.JSONObject} and {@link org.apache.tapestry5.json.JSONArray} * are serializable.
