diff options
Diffstat (limited to 'core/java/android/util')
-rw-r--r-- | core/java/android/util/AndroidException.java | 4 | ||||
-rw-r--r-- | core/java/android/util/AndroidRuntimeException.java | 4 | ||||
-rw-r--r-- | core/java/android/util/CharsetUtils.java | 70 | ||||
-rw-r--r-- | core/java/android/util/Finalizers.java | 127 | ||||
-rw-r--r-- | core/java/android/util/JsonReader.java | 1058 | ||||
-rw-r--r-- | core/java/android/util/JsonScope.java | 68 | ||||
-rw-r--r-- | core/java/android/util/JsonToken.java | 82 | ||||
-rw-r--r-- | core/java/android/util/JsonWriter.java | 472 | ||||
-rw-r--r-- | core/java/android/util/Patterns.java | 12 | ||||
-rw-r--r-- | core/java/android/util/SparseArray.java | 10 | ||||
-rw-r--r-- | core/java/android/util/TimingLogger.java | 32 |
11 files changed, 1896 insertions, 43 deletions
diff --git a/core/java/android/util/AndroidException.java b/core/java/android/util/AndroidException.java index a767ea1..dfe00c9 100644 --- a/core/java/android/util/AndroidException.java +++ b/core/java/android/util/AndroidException.java @@ -27,6 +27,10 @@ public class AndroidException extends Exception { super(name); } + public AndroidException(String name, Throwable cause) { + super(name, cause); + } + public AndroidException(Exception cause) { super(cause); } diff --git a/core/java/android/util/AndroidRuntimeException.java b/core/java/android/util/AndroidRuntimeException.java index 4ed17bc..2b824bf 100644 --- a/core/java/android/util/AndroidRuntimeException.java +++ b/core/java/android/util/AndroidRuntimeException.java @@ -27,6 +27,10 @@ public class AndroidRuntimeException extends RuntimeException { super(name); } + public AndroidRuntimeException(String name, Throwable cause) { + super(name, cause); + } + public AndroidRuntimeException(Exception cause) { super(cause); } diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java index 9d91aca..a763a69 100644 --- a/core/java/android/util/CharsetUtils.java +++ b/core/java/android/util/CharsetUtils.java @@ -17,36 +17,58 @@ package android.util; import android.os.Build; +import android.text.TextUtils; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.Map; /** + * <p> * A class containing utility methods related to character sets. This * class is primarily useful for code that wishes to be vendor-aware - * in its interpretation of Japanese encoding names. - * - * <p>As of this writing, the only vendor that is recognized by this - * class is Docomo (identified case-insensitively as {@code "docomo"}).</p> - * - * <b>Note:</b> This class is hidden in Cupcake, with a plan to - * un-hide in Donut. This was done because the first deployment to use - * this code is based on Cupcake, but the API had to be introduced - * after the public API freeze for that release. The upshot is that - * only system applications can safely use this class until Donut is - * available. - * + * in its interpretation of Japanese charset names (used in DoCoMo, + * KDDI, and SoftBank). + * </p> + * + * <p> + * <b>Note:</b> Developers will need to add an appropriate mapping for + * each vendor-specific charset. You may need to modify the C libraries + * like icu4c in order to let Android support an additional charset. + * </p> + * * @hide */ public final class CharsetUtils { /** - * name of the vendor "Docomo". <b>Note:</b> This isn't a public + * name of the vendor "DoCoMo". <b>Note:</b> This isn't a public * constant, in order to keep this class from becoming a de facto * reference list of vendor names. */ private static final String VENDOR_DOCOMO = "docomo"; - + /** + * Name of the vendor "KDDI". + */ + private static final String VENDOR_KDDI = "kddi"; + /** + * Name of the vendor "SoftBank". + */ + private static final String VENDOR_SOFTBANK = "softbank"; + + /** + * Represents one-to-one mapping from a vendor name to a charset specific to the vendor. + */ + private static final Map<String, String> sVendorShiftJisMap = new HashMap<String, String>(); + + static { + // These variants of Shift_JIS come from icu's mapping data (convrtrs.txt) + sVendorShiftJisMap.put(VENDOR_DOCOMO, "docomo-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_KDDI, "kddi-shift_jis-2007"); + sVendorShiftJisMap.put(VENDOR_SOFTBANK, "softbank-shift_jis-2007"); + } + /** * This class is uninstantiable. */ @@ -58,20 +80,22 @@ public final class CharsetUtils { * Returns the name of the vendor-specific character set * corresponding to the given original character set name and * vendor. If there is no vendor-specific character set for the - * given name/vendor pair, this returns the original character set - * name. The vendor name is matched case-insensitively. - * + * given name/vendor pair, this returns the original character set name. + * * @param charsetName the base character set name - * @param vendor the vendor to specialize for + * @param vendor the vendor to specialize for. All characters should be lower-cased. * @return the specialized character set name, or {@code charsetName} if * there is no specialized name */ public static String nameForVendor(String charsetName, String vendor) { - // TODO: Eventually, this may want to be table-driven. - - if (vendor.equalsIgnoreCase(VENDOR_DOCOMO) - && isShiftJis(charsetName)) { - return "docomo-shift_jis-2007"; + if (!TextUtils.isEmpty(charsetName) && !TextUtils.isEmpty(vendor)) { + // You can add your own mapping here. + if (isShiftJis(charsetName)) { + final String vendorShiftJis = sVendorShiftJisMap.get(vendor); + if (vendorShiftJis != null) { + return vendorShiftJis; + } + } } return charsetName; diff --git a/core/java/android/util/Finalizers.java b/core/java/android/util/Finalizers.java new file mode 100644 index 0000000..671f2d4 --- /dev/null +++ b/core/java/android/util/Finalizers.java @@ -0,0 +1,127 @@ +/* + * 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 android.util; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; + +/** + * This class can be used to implement reliable finalizers. + * + * @hide + */ +public final class Finalizers { + private static final String LOG_TAG = "Finalizers"; + + private static final Object[] sLock = new Object[0]; + private static boolean sInit; + private static Reclaimer sReclaimer; + + /** + * Subclass of PhantomReference used to reclaim resources. + */ + public static abstract class ReclaimableReference<T> extends PhantomReference<T> { + public ReclaimableReference(T r, ReferenceQueue<Object> q) { + super(r, q); + } + + public abstract void reclaim(); + } + + /** + * Returns the queue used to reclaim ReclaimableReferences. + * + * @return A reference queue or null before initialization + */ + public static ReferenceQueue<Object> getQueue() { + synchronized (sLock) { + if (!sInit) { + return null; + } + if (!sReclaimer.isRunning()) { + sReclaimer = new Reclaimer(sReclaimer.mQueue); + sReclaimer.start(); + } + return sReclaimer.mQueue; + } + } + + /** + * Invoked by Zygote. Don't touch! + */ + public static void init() { + synchronized (sLock) { + if (!sInit && sReclaimer == null) { + sReclaimer = new Reclaimer(); + sReclaimer.start(); + sInit = true; + } + } + } + + private static class Reclaimer extends Thread { + ReferenceQueue<Object> mQueue; + + private volatile boolean mRunning = false; + + Reclaimer() { + this(new ReferenceQueue<Object>()); + } + + Reclaimer(ReferenceQueue<Object> queue) { + super("Reclaimer"); + setDaemon(true); + mQueue = queue; + } + + @Override + public void start() { + mRunning = true; + super.start(); + } + + boolean isRunning() { + return mRunning; + } + + @SuppressWarnings({"InfiniteLoopStatement"}) + @Override + public void run() { + try { + while (true) { + try { + cleanUp(mQueue.remove()); + } catch (InterruptedException e) { + // Ignore + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "Reclaimer thread exiting: ", e); + } finally { + mRunning = false; + } + } + + private void cleanUp(Reference<?> reference) { + do { + reference.clear(); + ((ReclaimableReference<?>) reference).reclaim(); + } while ((reference = mQueue.poll()) != null); + } + } +} diff --git a/core/java/android/util/JsonReader.java b/core/java/android/util/JsonReader.java new file mode 100644 index 0000000..3345bfa --- /dev/null +++ b/core/java/android/util/JsonReader.java @@ -0,0 +1,1058 @@ +/* + * 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 android.util; + +import java.io.IOException; +import java.io.Reader; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +/** + * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) + * encoded value as a stream of tokens. This stream includes both literal + * values (strings, numbers, booleans, and nulls) as well as the begin and + * end delimiters of objects and arrays. The tokens are traversed in + * depth-first order, the same order that they appear in the JSON document. + * Within JSON objects, name/value pairs are represented by a single token. + * + * <h3>Parsing JSON</h3> + * To create a recursive descent parser your own JSON streams, first create an + * entry point method that creates a {@code JsonReader}. + * + * <p>Next, create handler methods for each structure in your JSON text. You'll + * need a method for each object type and for each array type. + * <ul> + * <li>Within <strong>array handling</strong> methods, first call {@link + * #beginArray} to consume the array's opening bracket. Then create a + * while loop that accumulates values, terminating when {@link #hasNext} + * is false. Finally, read the array's closing bracket by calling {@link + * #endArray}. + * <li>Within <strong>object handling</strong> methods, first call {@link + * #beginObject} to consume the object's opening brace. Then create a + * while loop that assigns values to local variables based on their name. + * This loop should terminate when {@link #hasNext} is false. Finally, + * read the object's closing brace by calling {@link #endObject}. + * </ul> + * <p>When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + * <p>When an unknown name is encountered, strict parsers should fail with an + * exception. Lenient parsers should call {@link #skipValue()} to recursively + * skip the value's nested tokens, which may otherwise conflict. + * + * <p>If a value may be null, you should first check using {@link #peek()}. + * Null literals can be consumed using either {@link #nextNull()} or {@link + * #skipValue()}. + * + * <h3>Example</h3> + * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code + * [ + * { + * "id": 912345678901, + * "text": "How do I read JSON on Android?", + * "geo": null, + * "user": { + * "name": "android_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@android_newb just use android.util.JsonReader!", + * "geo": [50.454722, -104.606667], + * "user": { + * "name": "jesse", + * "followers_count": 2 + * } + * } + * ]}</pre> + * This code implements the parser for the above structure: <pre> {@code + * + * public List<Message> readJsonStream(InputStream in) throws IOException { + * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8")); + * return readMessagesArray(reader); + * } + * + * public List<Message> readMessagesArray(JsonReader reader) throws IOException { + * List<Message> messages = new ArrayList<Message>(); + * + * reader.beginArray(); + * while (reader.hasNext()) { + * messages.add(readMessage(reader)); + * } + * reader.endArray(); + * return messages; + * } + * + * public Message readMessage(JsonReader reader) throws IOException { + * long id = -1; + * String text = null; + * User user = null; + * List<Double> geo = null; + * + * reader.beginObject(); + * while (reader.hasNext()) { + * String name = reader.nextName(); + * if (name.equals("id")) { + * id = reader.nextLong(); + * } else if (name.equals("text")) { + * text = reader.nextString(); + * } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) { + * geo = readDoublesArray(reader); + * } else if (name.equals("user")) { + * user = readUser(reader); + * } else { + * reader.skipValue(); + * } + * } + * reader.endObject(); + * return new Message(id, text, user, geo); + * } + * + * public List<Double> readDoublesArray(JsonReader reader) throws IOException { + * List<Double> doubles = new ArrayList<Double>(); + * + * reader.beginArray(); + * while (reader.hasNext()) { + * doubles.add(reader.nextDouble()); + * } + * reader.endArray(); + * return doubles; + * } + * + * public User readUser(JsonReader reader) throws IOException { + * String username = null; + * int followersCount = -1; + * + * reader.beginObject(); + * while (reader.hasNext()) { + * String name = reader.nextName(); + * if (name.equals("name")) { + * username = reader.nextString(); + * } else if (name.equals("followers_count")) { + * followersCount = reader.nextInt(); + * } else { + * reader.skipValue(); + * } + * } + * reader.endObject(); + * return new User(username, followersCount); + * }}</pre> + * + * <h3>Number Handling</h3> + * This reader permits numeric values to be read as strings and string values to + * be read as numbers. For example, both elements of the JSON array {@code + * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. + * This behavior is intended to prevent lossy numeric conversions: double is + * JavaScript's only numeric type and very large values like {@code + * 9007199254740993} cannot be represented exactly on that platform. To minimize + * precision loss, extremely large values should be written and read as strings + * in JSON. + * + * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + */ +public final class JsonReader implements Closeable { + + /** The input JSON. */ + private final Reader in; + + /** True to accept non-spec compliant JSON */ + private boolean lenient = false; + + /** + * Use a manual buffer to easily read and unread upcoming characters, and + * also so we can create strings without an intermediate StringBuilder. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + private final List<JsonScope> stack = new ArrayList<JsonScope>(); + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * True if we've already read the next token. If we have, the string value + * for that token will be assigned to {@code value} if such a string value + * exists. And the token type will be assigned to {@code token} if the token + * type is known. The token type may be null for literals, since we derive + * that lazily. + */ + private boolean hasToken; + + /** + * The type of the next token to be returned by {@link #peek} and {@link + * #advance}, or {@code null} if it is unknown and must first be derived + * from {@code value}. This value is undefined if {@code hasToken} is false. + */ + private JsonToken token; + + /** The text of the next name. */ + private String name; + + /** The text of the next literal value. */ + private String value; + + /** True if we're currently handling a skipValue() call. */ + private boolean skipping = false; + + /** + * Creates a new instance that reads a JSON-encoded stream from {@code in}. + */ + public JsonReader(Reader in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.in = in; + } + + /** + * Configure this parser to be be liberal in what it accepts. By default, + * this parser is strict and only accepts JSON as specified by <a + * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the + * parser to lenient causes it to ignore the following syntax errors: + * + * <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>Names that are unquoted or {@code 'single quoted'}. + * <li>Strings that are unquoted or {@code 'single quoted'}. + * <li>Array elements separated by {@code ;} instead of {@code ,}. + * <li>Unnecessary array separators. These are interpreted as if null + * was the omitted value. + * <li>Names and values separated by {@code =} or {@code =>} instead of + * {@code :}. + * <li>Name/value pairs separated by {@code ;} instead of {@code ,}. + * </ul> + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new array. + */ + public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + } + + /** + * Consumes {@code expected}. + */ + private void expect(JsonToken expected) throws IOException { + quickPeek(); + if (token != expected) { + throw new IllegalStateException("Expected " + expected + " but was " + peek()); + } + advance(); + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + quickPeek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + quickPeek(); + + if (token == null) { + decodeLiteral(); + } + + return token; + } + + /** + * Ensures that a token is ready. After this call either {@code token} or + * {@code value} will be non-null. To ensure {@code token} has a definitive + * value, use {@link #peek()} + */ + private JsonToken quickPeek() throws IOException { + if (hasToken) { + return token; + } + + switch (peekStack()) { + case EMPTY_DOCUMENT: + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + JsonToken firstToken = nextValue(); + if (token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { + throw new IOException( + "Expected JSON document to start with '[' or '{' but was " + token); + } + return firstToken; + case EMPTY_ARRAY: + return nextInArray(true); + case NONEMPTY_ARRAY: + return nextInArray(false); + case EMPTY_OBJECT: + return nextInObject(true); + case DANGLING_NAME: + return objectValue(); + case NONEMPTY_OBJECT: + return nextInObject(false); + case NONEMPTY_DOCUMENT: + hasToken = true; + return token = JsonToken.END_DOCUMENT; + case CLOSED: + throw new IllegalStateException("JsonReader is closed"); + default: + throw new AssertionError(); + } + } + + /** + * Advances the cursor in the JSON stream to the next token. + */ + private JsonToken advance() throws IOException { + quickPeek(); + + JsonToken result = token; + hasToken = false; + token = null; + value = null; + name = null; + return result; + } + + /** + * Returns the next token, a {@link JsonToken#NAME property name}, and + * consumes it. + * + * @throws IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + quickPeek(); + if (token != JsonToken.NAME) { + throw new IllegalStateException("Expected a name but was " + peek()); + } + String result = name; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#STRING string} value of the next token, + * consuming it. If the next token is a number, this method will return its + * string form. + * + * @throws IllegalStateException if the next token is not a string or if + * this reader is closed. + */ + public String nextString() throws IOException { + peek(); + if (value == null || (token != JsonToken.STRING && token != JsonToken.NUMBER)) { + throw new IllegalStateException("Expected a string but was " + peek()); + } + + String result = value; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, + * consuming it. + * + * @throws IllegalStateException if the next token is not a boolean or if + * this reader is closed. + */ + public boolean nextBoolean() throws IOException { + quickPeek(); + if (value == null || token == JsonToken.STRING) { + throw new IllegalStateException("Expected a boolean but was " + peek()); + } + + boolean result; + if (value.equalsIgnoreCase("true")) { + result = true; + } else if (value.equalsIgnoreCase("false")) { + result = false; + } else { + throw new IllegalStateException("Not a boolean: " + value); + } + + advance(); + return result; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is a + * literal null. + * + * @throws IllegalStateException if the next token is not null or if this + * reader is closed. + */ + public void nextNull() throws IOException { + quickPeek(); + if (value == null || token == JsonToken.STRING) { + throw new IllegalStateException("Expected null but was " + peek()); + } + + if (!value.equalsIgnoreCase("null")) { + throw new IllegalStateException("Not a null: " + value); + } + + advance(); + } + + /** + * Returns the {@link JsonToken#NUMBER double} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a double. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a double, or is non-finite. + */ + public double nextDouble() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a double but was " + peek()); + } + + double result = Double.parseDouble(value); + + if ((result >= 1.0d && value.startsWith("0")) + || Double.isNaN(result) + || Double.isInfinite(result)) { + throw new NumberFormatException( + "JSON forbids octal prefixes, NaN and infinities: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER long} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a long. If the next token's numeric value cannot be exactly + * represented by a Java {@code long}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as a long. + */ + public long nextLong() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a long but was " + peek()); + } + + long result; + try { + result = Long.parseLong(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (long) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER int} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as an int. If the next token's numeric value cannot be exactly + * represented by a Java {@code int}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as an int. + */ + public int nextInt() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected an int but was " + peek()); + } + + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (int) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Closes this JSON reader and the underlying {@link Reader}. + */ + public void close() throws IOException { + hasToken = false; + value = null; + token = null; + stack.clear(); + stack.add(JsonScope.CLOSED); + in.close(); + } + + /** + * Skips the next value recursively. If it is an object or array, all nested + * elements are skipped. This method is intended for use when the JSON token + * stream contains unrecognized or unhandled values. + */ + public void skipValue() throws IOException { + skipping = true; + try { + int count = 0; + do { + JsonToken token = advance(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } + } while (count != 0); + } finally { + skipping = false; + } + } + + private JsonScope peekStack() { + return stack.get(stack.size() - 1); + } + + private JsonScope pop() { + return stack.remove(stack.size() - 1); + } + + private void push(JsonScope newTop) { + stack.add(newTop); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope newTop) { + stack.set(stack.size() - 1, newTop); + } + + private JsonToken nextInArray(boolean firstElement) throws IOException { + if (firstElement) { + replaceTop(JsonScope.NONEMPTY_ARRAY); + } else { + /* Look for a comma before each element after the first element. */ + switch (nextNonWhitespace()) { + case ']': + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated array"); + } + } + + switch (nextNonWhitespace()) { + case ']': + if (firstElement) { + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + } + // fall-through to handle ",]" + case ';': + case ',': + /* In lenient mode, a 0-length literal means 'null' */ + checkLenient(); + pos--; + hasToken = true; + value = "null"; + return token = JsonToken.NULL; + default: + pos--; + return nextValue(); + } + } + + private JsonToken nextInObject(boolean firstElement) throws IOException { + /* + * Read delimiters. Either a comma/semicolon separating this and the + * previous name-value pair, or a close brace to denote the end of the + * object. + */ + if (firstElement) { + /* Peek to see if this is the empty object. */ + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + default: + pos--; + } + } else { + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + case ';': + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + + /* Read the name. */ + int quote = nextNonWhitespace(); + switch (quote) { + case '\'': + checkLenient(); // fall-through + case '"': + name = nextString((char) quote); + break; + default: + checkLenient(); + pos--; + name = nextLiteral(); + if (name.isEmpty()) { + throw syntaxError("Expected name"); + } + } + + replaceTop(JsonScope.DANGLING_NAME); + hasToken = true; + return token = JsonToken.NAME; + } + + private JsonToken objectValue() throws IOException { + /* + * Read the name/value separator. Usually a colon ':'. In lenient mode + * we also accept an equals sign '=', or an arrow "=>". + */ + switch (nextNonWhitespace()) { + case ':': + break; + case '=': + checkLenient(); + if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + break; + default: + throw syntaxError("Expected ':'"); + } + + replaceTop(JsonScope.NONEMPTY_OBJECT); + return nextValue(); + } + + private JsonToken nextValue() throws IOException { + int c = nextNonWhitespace(); + switch (c) { + case '{': + push(JsonScope.EMPTY_OBJECT); + hasToken = true; + return token = JsonToken.BEGIN_OBJECT; + + case '[': + push(JsonScope.EMPTY_ARRAY); + hasToken = true; + return token = JsonToken.BEGIN_ARRAY; + + case '\'': + checkLenient(); // fall-through + case '"': + value = nextString((char) c); + hasToken = true; + return token = JsonToken.STRING; + + default: + pos--; + return readLiteral(); + } + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + if (limit != pos) { + limit -= pos; + System.arraycopy(buffer, pos, buffer, 0, limit); + } else { + limit = 0; + } + + pos = 0; + int total; + while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int nextNonWhitespace() throws IOException { + while (pos < limit || fillBuffer(1)) { + int c = buffer[pos++]; + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == limit && !fillBuffer(1)) { + return c; + } + + checkLenient(); + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + pos += 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 behaviour, but it's required to parse + * existing documents. See http://b/2571423. + */ + checkLenient(); + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + throw syntaxError("End of input"); + } + + private void checkLenient() throws IOException { + if (!lenient) { + throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); + } + } + + /** + * 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. + */ + private void skipToEndOfLine() throws IOException { + while (pos < limit || fillBuffer(1)) { + char c = buffer[pos++]; + if (c == '\r' || c == '\n') { + break; + } + } + } + + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() < limit || fillBuffer(toFind.length()); pos++) { + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + /** + * 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. + * + * @param quote either ' or ". + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private String nextString(char quote) throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + + if (c == quote) { + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return new String(buffer, start, pos - start - 1); + } else { + builder.append(buffer, start, pos - start - 1); + return builder.toString(); + } + + } else if (c == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + throw syntaxError("Unterminated string"); + } + + /** + * Returns the string up to but not including any delimiter characters. This + * does not consume the delimiter character. + */ + private String nextLiteral() throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + switch (c) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + pos--; + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return new String(buffer, start, pos - start); + } else { + builder.append(buffer, start, pos - start); + return builder.toString(); + } + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + return builder.toString(); + } + + @Override public String toString() { + return getClass().getSimpleName() + " near " + getSnippet(); + } + + /** + * 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". + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private char readEscapeCharacter() throws IOException { + if (pos == limit && !fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + char escaped = buffer[pos++]; + switch (escaped) { + case 'u': + if (pos + 4 > limit && !fillBuffer(4)) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = new String(buffer, pos, 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. + */ + private JsonToken readLiteral() throws IOException { + String literal = nextLiteral(); + if (literal.isEmpty()) { + throw syntaxError("Expected literal value"); + } + value = literal; + hasToken = true; + return token = null; // use decodeLiteral() to get the token type + } + + /** + * Assigns {@code nextToken} based on the value of {@code nextValue}. + */ + private void decodeLiteral() throws IOException { + if (value.equalsIgnoreCase("null")) { + token = JsonToken.NULL; + } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + token = JsonToken.BOOLEAN; + } else { + try { + Double.parseDouble(value); // this work could potentially be cached + token = JsonToken.NUMBER; + } catch (NumberFormatException ignored) { + // this must be an unquoted string + checkLenient(); + token = JsonToken.STRING; + } + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + public IOException syntaxError(String message) throws IOException { + throw new JsonSyntaxException(message + " near " + getSnippet()); + } + + private CharSequence getSnippet() { + StringBuilder snippet = new StringBuilder(); + int beforePos = Math.min(pos, 20); + snippet.append(buffer, pos - beforePos, beforePos); + int afterPos = Math.min(limit - pos, 20); + snippet.append(buffer, pos, afterPos); + return snippet; + } + + private static class JsonSyntaxException extends IOException { + private JsonSyntaxException(String s) { + super(s); + } + } +} diff --git a/core/java/android/util/JsonScope.java b/core/java/android/util/JsonScope.java new file mode 100644 index 0000000..ca534e9 --- /dev/null +++ b/core/java/android/util/JsonScope.java @@ -0,0 +1,68 @@ +/* + * 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 android.util; + +/** + * Lexical scoping elements within a JSON reader or writer. + */ +enum JsonScope { + + /** + * 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 name/value pairs 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_NAME, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * No object or array has been started. + */ + EMPTY_DOCUMENT, + + /** + * A document with at an array or object. + */ + NONEMPTY_DOCUMENT, + + /** + * A document that's been closed and cannot be accessed. + */ + CLOSED, +} diff --git a/core/java/android/util/JsonToken.java b/core/java/android/util/JsonToken.java new file mode 100644 index 0000000..45bc6ca --- /dev/null +++ b/core/java/android/util/JsonToken.java @@ -0,0 +1,82 @@ +/* + * 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 android.util; + +/** + * A structure, name or value type in a JSON-encoded string. + */ +public enum JsonToken { + + /** + * The opening of a JSON array. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_ARRAY, + + /** + * The closing of a JSON array. Written using {@link JsonWriter#endArray} + * and read using {@link JsonReader#endArray}. + */ + END_ARRAY, + + /** + * The opening of a JSON object. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_OBJECT, + + /** + * The closing of a JSON object. Written using {@link JsonWriter#endObject} + * and read using {@link JsonReader#endObject}. + */ + END_OBJECT, + + /** + * A JSON property name. Within objects, tokens alternate between names and + * their values. Written using {@link JsonWriter#name} and read using {@link + * JsonReader#nextName} + */ + NAME, + + /** + * A JSON string. + */ + STRING, + + /** + * A JSON number represented in this API by a Java {@code double}, {@code + * long}, or {@code int}. + */ + NUMBER, + + /** + * A JSON {@code true} or {@code false}. + */ + BOOLEAN, + + /** + * A JSON {@code null}. + */ + NULL, + + /** + * The end of the JSON stream. This sentinel value is returned by {@link + * JsonReader#peek()} to signal that the JSON-encoded value has no more + * tokens. + */ + END_DOCUMENT +} diff --git a/core/java/android/util/JsonWriter.java b/core/java/android/util/JsonWriter.java new file mode 100644 index 0000000..fecc1c8 --- /dev/null +++ b/core/java/android/util/JsonWriter.java @@ -0,0 +1,472 @@ +/* + * 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 android.util; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) + * encoded value to a stream, one token at a time. The stream includes both + * literal values (strings, numbers, booleans and nulls) as well as the begin + * and end delimiters of objects and arrays. + * + * <h3>Encoding JSON</h3> + * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON + * document must contain one top-level array or object. Call methods on the + * writer as you walk the structure's contents, nesting arrays and objects as + * necessary: + * <ul> + * <li>To write <strong>arrays</strong>, first call {@link #beginArray()}. + * Write each of the array's elements with the appropriate {@link #value} + * methods or by nesting other arrays and objects. Finally close the array + * using {@link #endArray()}. + * <li>To write <strong>objects</strong>, first call {@link #beginObject()}. + * Write each of the object's properties by alternating calls to + * {@link #name} with the property's value. Write property values with the + * appropriate {@link #value} method or by nesting other objects or arrays. + * Finally close the object using {@link #endObject()}. + * </ul> + * + * <h3>Example</h3> + * Suppose we'd like to encode a stream of messages such as the following: <pre> {@code + * [ + * { + * "id": 912345678901, + * "text": "How do I write JSON on Android?", + * "geo": null, + * "user": { + * "name": "android_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@android_newb just use android.util.JsonWriter!", + * "geo": [50.454722, -104.606667], + * "user": { + * "name": "jesse", + * "followers_count": 2 + * } + * } + * ]}</pre> + * This code encodes the above structure: <pre> {@code + * public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException { + * JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8")); + * writer.setIndentSpaces(4); + * writeMessagesArray(writer, messages); + * writer.close(); + * } + * + * public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException { + * writer.beginArray(); + * for (Message message : messages) { + * writeMessage(writer, message); + * } + * writer.endArray(); + * } + * + * public void writeMessage(JsonWriter writer, Message message) throws IOException { + * writer.beginObject(); + * writer.name("id").value(message.getId()); + * writer.name("text").value(message.getText()); + * if (message.getGeo() != null) { + * writer.name("geo"); + * writeDoublesArray(writer, message.getGeo()); + * } else { + * writer.name("geo").nullValue(); + * } + * writer.name("user"); + * writeUser(writer, message.getUser()); + * writer.endObject(); + * } + * + * public void writeUser(JsonWriter writer, User user) throws IOException { + * writer.beginObject(); + * writer.name("name").value(user.getName()); + * writer.name("followers_count").value(user.getFollowersCount()); + * writer.endObject(); + * } + * + * public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException { + * writer.beginArray(); + * for (Double value : doubles) { + * writer.value(value); + * } + * writer.endArray(); + * }}</pre> + * + * <p>Each {@code JsonWriter} may be used to write a single JSON stream. + * Instances of this class are not thread safe. Calls that would result in a + * malformed JSON string will fail with an {@link IllegalStateException}. + */ +public final class JsonWriter implements Closeable { + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private final List<JsonScope> stack = new ArrayList<JsonScope>(); + { + stack.add(JsonScope.EMPTY_DOCUMENT); + } + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + /** + * Creates a new instance that writes a JSON-encoded stream to {@code out}. + * For best performance, ensure {@link Writer} is buffered; wrapping in + * {@link java.io.BufferedWriter BufferedWriter} if necessary. + */ + public JsonWriter(Writer out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + this.out = out; + } + + /** + * Sets the indentation string to be repeated for each level of indentation + * in the encoded document. If {@code indent.isEmpty()} the encoded document + * will be compact. Otherwise the encoded document will be more + * human-readable. + * + * @param indent a string containing only whitespace. + */ + public void setIndent(String indent) { + if (indent.isEmpty()) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Begins encoding a new array. Each call to this method must be paired with + * a call to {@link #endArray}. + * + * @return this writer. + */ + public JsonWriter beginArray() throws IOException { + return open(JsonScope.EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]"); + } + + /** + * Begins encoding a new object. Each call to this method must be paired + * with a call to {@link #endObject}. + * + * @return this writer. + */ + public JsonWriter beginObject() throws IOException { + return open(JsonScope.EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(JsonScope empty, String openBracket) throws IOException { + beforeValue(true); + stack.add(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket) + throws IOException { + JsonScope context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem: " + stack); + } + + stack.remove(stack.size() - 1); + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + */ + private JsonScope peek() { + return stack.get(stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope topOfStack) { + stack.set(stack.size() - 1, topOfStack); + } + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. May not be null. + * @return this writer. + */ + public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + beforeName(); + string(name); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + beforeValue(false); + string(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + beforeValue(false); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + beforeValue(false); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities}. + * @return this writer. + */ + public JsonWriter value(double value) throws IOException { + if (Double.isNaN(value) || Double.isInfinite(value)) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(false); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + beforeValue(false); + out.write(Long.toString(value)); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Writer}. + * + * @throws IOException if the JSON document is incomplete. + */ + public void close() throws IOException { + out.close(); + + if (peek() != JsonScope.NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + } + + private void string(String value) throws IOException { + out.write("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = 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 (c) { + case '"': + case '\\': + case '/': + out.write('\\'); + out.write(c); + break; + + case '\t': + out.write("\\t"); + break; + + case '\b': + out.write("\\b"); + break; + + case '\n': + out.write("\\n"); + break; + + case '\r': + out.write("\\r"); + break; + + case '\f': + out.write("\\f"); + break; + + default: + if (c <= 0x1F) { + out.write(String.format("\\u%04x", (int) c)); + } else { + out.write(c); + } + break; + } + + } + out.write("\""); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write("\n"); + for (int i = 1; i < stack.size(); i++) { + out.write(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + JsonScope context = peek(); + if (context == JsonScope.NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem: " + stack); + } + newline(); + replaceTop(JsonScope.DANGLING_NAME); + } + + /** + * 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. + * + * @param root true if the value is a new array or object, the two values + * permitted as top-level elements. + */ + private void beforeValue(boolean root) throws IOException { + switch (peek()) { + case EMPTY_DOCUMENT: // first in document + if (!root) { + throw new IllegalStateException( + "JSON must start with an array or an object."); + } + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(JsonScope.NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case DANGLING_NAME: // value for name + out.append(separator); + replaceTop(JsonScope.NONEMPTY_OBJECT); + break; + + case NONEMPTY_DOCUMENT: + throw new IllegalStateException( + "JSON must have only one top-level value."); + + default: + throw new IllegalStateException("Nesting problem: " + stack); + } + } +} diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java index 5cbfd29..3bcd266 100644 --- a/core/java/android/util/Patterns.java +++ b/core/java/android/util/Patterns.java @@ -25,7 +25,7 @@ import java.util.regex.Pattern; public class Patterns { /** * Regular expression to match all IANA top-level domains. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -53,8 +53,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw])"; /** @@ -65,7 +65,7 @@ public class Patterns { /** * Regular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -94,8 +94,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw]))"; /** diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java index 1c8b330..7fc43b9 100644 --- a/core/java/android/util/SparseArray.java +++ b/core/java/android/util/SparseArray.java @@ -90,6 +90,16 @@ public class SparseArray<E> { delete(key); } + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + private void gc() { // Log.e("SparseArray", "gc start with " + mSize); diff --git a/core/java/android/util/TimingLogger.java b/core/java/android/util/TimingLogger.java index 0f39c97..be442da 100644 --- a/core/java/android/util/TimingLogger.java +++ b/core/java/android/util/TimingLogger.java @@ -24,22 +24,26 @@ import android.os.SystemClock; * A utility class to help log timings splits throughout a method call. * Typical usage is: * - * TimingLogger timings = new TimingLogger(TAG, "methodA"); - * ... do some work A ... - * timings.addSplit("work A"); - * ... do some work B ... - * timings.addSplit("work B"); - * ... do some work C ... - * timings.addSplit("work C"); - * timings.dumpToLog(); + * <pre> + * TimingLogger timings = new TimingLogger(TAG, "methodA"); + * // ... do some work A ... + * timings.addSplit("work A"); + * // ... do some work B ... + * timings.addSplit("work B"); + * // ... do some work C ... + * timings.addSplit("work C"); + * timings.dumpToLog(); + * </pre> * - * The dumpToLog call would add the following to the log: + * <p>The dumpToLog call would add the following to the log:</p> * - * D/TAG ( 3459): methodA: begin - * D/TAG ( 3459): methodA: 9 ms, work A - * D/TAG ( 3459): methodA: 1 ms, work B - * D/TAG ( 3459): methodA: 6 ms, work C - * D/TAG ( 3459): methodA: end, 16 ms + * <pre> + * D/TAG ( 3459): methodA: begin + * D/TAG ( 3459): methodA: 9 ms, work A + * D/TAG ( 3459): methodA: 1 ms, work B + * D/TAG ( 3459): methodA: 6 ms, work C + * D/TAG ( 3459): methodA: end, 16 ms + * </pre> */ public class TimingLogger { |