diff options
Diffstat (limited to 'gson/src/main/java/com/google/gson')
66 files changed, 13528 insertions, 0 deletions
diff --git a/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java new file mode 100644 index 00000000..aa253340 --- /dev/null +++ b/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * This type adapter supports three subclasses of date: Date, Timestamp, and + * java.sql.Date. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +final class DefaultDateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> { + + // TODO: migrate to streaming adapter + + private final DateFormat enUsFormat; + private final DateFormat localFormat; + private final DateFormat iso8601Format; + + DefaultDateTypeAdapter() { + this(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US), + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)); + } + + DefaultDateTypeAdapter(String datePattern) { + this(new SimpleDateFormat(datePattern, Locale.US), new SimpleDateFormat(datePattern)); + } + + DefaultDateTypeAdapter(int style) { + this(DateFormat.getDateInstance(style, Locale.US), DateFormat.getDateInstance(style)); + } + + public DefaultDateTypeAdapter(int dateStyle, int timeStyle) { + this(DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US), + DateFormat.getDateTimeInstance(dateStyle, timeStyle)); + } + + DefaultDateTypeAdapter(DateFormat enUsFormat, DateFormat localFormat) { + this.enUsFormat = enUsFormat; + this.localFormat = localFormat; + this.iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + this.iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + // These methods need to be synchronized since JDK DateFormat classes are not thread-safe + // See issue 162 + public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { + synchronized (localFormat) { + String dateFormatAsString = enUsFormat.format(src); + return new JsonPrimitive(dateFormatAsString); + } + } + + public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (!(json instanceof JsonPrimitive)) { + throw new JsonParseException("The date should be a string value"); + } + Date date = deserializeToDate(json); + if (typeOfT == Date.class) { + return date; + } else if (typeOfT == Timestamp.class) { + return new Timestamp(date.getTime()); + } else if (typeOfT == java.sql.Date.class) { + return new java.sql.Date(date.getTime()); + } else { + throw new IllegalArgumentException(getClass() + " cannot deserialize to " + typeOfT); + } + } + + private Date deserializeToDate(JsonElement json) { + synchronized (localFormat) { + try { + return localFormat.parse(json.getAsString()); + } catch (ParseException ignored) { + } + try { + return enUsFormat.parse(json.getAsString()); + } catch (ParseException ignored) { + } + try { + return iso8601Format.parse(json.getAsString()); + } catch (ParseException e) { + throw new JsonSyntaxException(json.getAsString(), e); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(DefaultDateTypeAdapter.class.getSimpleName()); + sb.append('(').append(localFormat.getClass().getSimpleName()).append(')'); + return sb.toString(); + } +} diff --git a/gson/src/main/java/com/google/gson/ExclusionStrategy.java b/gson/src/main/java/com/google/gson/ExclusionStrategy.java new file mode 100644 index 00000000..6a3f43fb --- /dev/null +++ b/gson/src/main/java/com/google/gson/ExclusionStrategy.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +/** + * A strategy (or policy) definition that is used to decide whether or not a field or top-level + * class should be serialized or deserialized as part of the JSON output/input. For serialization, + * if the {@link #shouldSkipClass(Class)} method returns true then that class or field type + * will not be part of the JSON output. For deserialization, if {@link #shouldSkipClass(Class)} + * returns true, then it will not be set as part of the Java object structure. + * + * <p>The following are a few examples that shows how you can use this exclusion mechanism. + * + * <p><strong>Exclude fields and objects based on a particular class type:</strong> + * <pre class="code"> + * private static class SpecificClassExclusionStrategy implements ExclusionStrategy { + * private final Class<?> excludedThisClass; + * + * public SpecificClassExclusionStrategy(Class<?> excludedThisClass) { + * this.excludedThisClass = excludedThisClass; + * } + * + * public boolean shouldSkipClass(Class<?> clazz) { + * return excludedThisClass.equals(clazz); + * } + * + * public boolean shouldSkipField(FieldAttributes f) { + * return excludedThisClass.equals(f.getDeclaredClass()); + * } + * } + * </pre> + * + * <p><strong>Excludes fields and objects based on a particular annotation:</strong> + * <pre class="code"> + * public @interface FooAnnotation { + * // some implementation here + * } + * + * // Excludes any field (or class) that is tagged with an "@FooAnnotation" + * private static class FooAnnotationExclusionStrategy implements ExclusionStrategy { + * public boolean shouldSkipClass(Class<?> clazz) { + * return clazz.getAnnotation(FooAnnotation.class) != null; + * } + * + * public boolean shouldSkipField(FieldAttributes f) { + * return f.getAnnotation(FooAnnotation.class) != null; + * } + * } + * </pre> + * + * <p>Now if you want to configure {@code Gson} to use a user defined exclusion strategy, then + * the {@code GsonBuilder} is required. The following is an example of how you can use the + * {@code GsonBuilder} to configure Gson to use one of the above sample: + * <pre class="code"> + * ExclusionStrategy excludeStrings = new UserDefinedExclusionStrategy(String.class); + * Gson gson = new GsonBuilder() + * .setExclusionStrategies(excludeStrings) + * .create(); + * </pre> + * + * <p>For certain model classes, you may only want to serialize a field, but exclude it for + * deserialization. To do that, you can write an {@code ExclusionStrategy} as per normal; + * however, you would register it with the + * {@link GsonBuilder#addDeserializationExclusionStrategy(ExclusionStrategy)} method. + * For example: + * <pre class="code"> + * ExclusionStrategy excludeStrings = new UserDefinedExclusionStrategy(String.class); + * Gson gson = new GsonBuilder() + * .addDeserializationExclusionStrategy(excludeStrings) + * .create(); + * </pre> + * + * @author Inderjeet Singh + * @author Joel Leitch + * + * @see GsonBuilder#setExclusionStrategies(ExclusionStrategy...) + * @see GsonBuilder#addDeserializationExclusionStrategy(ExclusionStrategy) + * @see GsonBuilder#addSerializationExclusionStrategy(ExclusionStrategy) + * + * @since 1.4 + */ +public interface ExclusionStrategy { + + /** + * @param f the field object that is under test + * @return true if the field should be ignored; otherwise false + */ + public boolean shouldSkipField(FieldAttributes f); + + /** + * @param clazz the class object that is under test + * @return true if the class should be ignored; otherwise false + */ + public boolean shouldSkipClass(Class<?> clazz); +} diff --git a/gson/src/main/java/com/google/gson/FieldAttributes.java b/gson/src/main/java/com/google/gson/FieldAttributes.java new file mode 100644 index 00000000..cb89ff11 --- /dev/null +++ b/gson/src/main/java/com/google/gson/FieldAttributes.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2009 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 com.google.gson; + +import com.google.gson.internal.$Gson$Preconditions; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; + +/** + * A data object that stores attributes of a field. + * + * <p>This class is immutable; therefore, it can be safely shared across threads. + * + * @author Inderjeet Singh + * @author Joel Leitch + * + * @since 1.4 + */ +public final class FieldAttributes { + private final Field field; + + /** + * Constructs a Field Attributes object from the {@code f}. + * + * @param f the field to pull attributes from + */ + public FieldAttributes(Field f) { + $Gson$Preconditions.checkNotNull(f); + this.field = f; + } + + /** + * @return the declaring class that contains this field + */ + public Class<?> getDeclaringClass() { + return field.getDeclaringClass(); + } + + /** + * @return the name of the field + */ + public String getName() { + return field.getName(); + } + + /** + * <p>For example, assume the following class definition: + * <pre class="code"> + * public class Foo { + * private String bar; + * private List<String> red; + * } + * + * Type listParmeterizedType = new TypeToken<List<String>>() {}.getType(); + * </pre> + * + * <p>This method would return {@code String.class} for the {@code bar} field and + * {@code listParameterizedType} for the {@code red} field. + * + * @return the specific type declared for this field + */ + public Type getDeclaredType() { + return field.getGenericType(); + } + + /** + * Returns the {@code Class} object that was declared for this field. + * + * <p>For example, assume the following class definition: + * <pre class="code"> + * public class Foo { + * private String bar; + * private List<String> red; + * } + * </pre> + * + * <p>This method would return {@code String.class} for the {@code bar} field and + * {@code List.class} for the {@code red} field. + * + * @return the specific class object that was declared for the field + */ + public Class<?> getDeclaredClass() { + return field.getType(); + } + + /** + * Return the {@code T} annotation object from this field if it exist; otherwise returns + * {@code null}. + * + * @param annotation the class of the annotation that will be retrieved + * @return the annotation instance if it is bound to the field; otherwise {@code null} + */ + public <T extends Annotation> T getAnnotation(Class<T> annotation) { + return field.getAnnotation(annotation); + } + + /** + * Return the annotations that are present on this field. + * + * @return an array of all the annotations set on the field + * @since 1.4 + */ + public Collection<Annotation> getAnnotations() { + return Arrays.asList(field.getAnnotations()); + } + + /** + * Returns {@code true} if the field is defined with the {@code modifier}. + * + * <p>This method is meant to be called as: + * <pre class="code"> + * boolean hasPublicModifier = fieldAttribute.hasModifier(java.lang.reflect.Modifier.PUBLIC); + * </pre> + * + * @see java.lang.reflect.Modifier + */ + public boolean hasModifier(int modifier) { + return (field.getModifiers() & modifier) != 0; + } + + /** + * This is exposed internally only for the removing synthetic fields from the JSON output. + * + * @return true if the field is synthetic; otherwise false + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + Object get(Object instance) throws IllegalAccessException { + return field.get(instance); + } + + /** + * This is exposed internally only for the removing synthetic fields from the JSON output. + * + * @return true if the field is synthetic; otherwise false + */ + boolean isSynthetic() { + return field.isSynthetic(); + } +} diff --git a/gson/src/main/java/com/google/gson/FieldNamingPolicy.java b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java new file mode 100644 index 00000000..6b4c72ca --- /dev/null +++ b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Field; +import java.util.Locale; + +/** + * An enumeration that defines a few standard naming conventions for JSON field names. + * This enumeration should be used in conjunction with {@link com.google.gson.GsonBuilder} + * to configure a {@link com.google.gson.Gson} instance to properly translate Java field + * names into the desired JSON field names. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public enum FieldNamingPolicy implements FieldNamingStrategy { + + /** + * Using this naming policy with Gson will ensure that the field name is + * unchanged. + */ + IDENTITY() { + public String translateName(Field f) { + return f.getName(); + } + }, + + /** + * Using this naming policy with Gson will ensure that the first "letter" of the Java + * field name is capitalized when serialized to its JSON form. + * + * <p>Here's a few examples of the form "Java Field Name" ---> "JSON Field Name":</p> + * <ul> + * <li>someFieldName ---> SomeFieldName</li> + * <li>_someFieldName ---> _SomeFieldName</li> + * </ul> + */ + UPPER_CAMEL_CASE() { + public String translateName(Field f) { + return upperCaseFirstLetter(f.getName()); + } + }, + + /** + * Using this naming policy with Gson will ensure that the first "letter" of the Java + * field name is capitalized when serialized to its JSON form and the words will be + * separated by a space. + * + * <p>Here's a few examples of the form "Java Field Name" ---> "JSON Field Name":</p> + * <ul> + * <li>someFieldName ---> Some Field Name</li> + * <li>_someFieldName ---> _Some Field Name</li> + * </ul> + * + * @since 1.4 + */ + UPPER_CAMEL_CASE_WITH_SPACES() { + public String translateName(Field f) { + return upperCaseFirstLetter(separateCamelCase(f.getName(), " ")); + } + }, + + /** + * Using this naming policy with Gson will modify the Java Field name from its camel cased + * form to a lower case field name where each word is separated by an underscore (_). + * + * <p>Here's a few examples of the form "Java Field Name" ---> "JSON Field Name":</p> + * <ul> + * <li>someFieldName ---> some_field_name</li> + * <li>_someFieldName ---> _some_field_name</li> + * <li>aStringField ---> a_string_field</li> + * <li>aURL ---> a_u_r_l</li> + * </ul> + */ + LOWER_CASE_WITH_UNDERSCORES() { + public String translateName(Field f) { + return separateCamelCase(f.getName(), "_").toLowerCase(Locale.ENGLISH); + } + }, + + /** + * Using this naming policy with Gson will modify the Java Field name from its camel cased + * form to a lower case field name where each word is separated by a dash (-). + * + * <p>Here's a few examples of the form "Java Field Name" ---> "JSON Field Name":</p> + * <ul> + * <li>someFieldName ---> some-field-name</li> + * <li>_someFieldName ---> _some-field-name</li> + * <li>aStringField ---> a-string-field</li> + * <li>aURL ---> a-u-r-l</li> + * </ul> + * Using dashes in JavaScript is not recommended since dash is also used for a minus sign in + * expressions. This requires that a field named with dashes is always accessed as a quoted + * property like {@code myobject['my-field']}. Accessing it as an object field + * {@code myobject.my-field} will result in an unintended javascript expression. + * @since 1.4 + */ + LOWER_CASE_WITH_DASHES() { + public String translateName(Field f) { + return separateCamelCase(f.getName(), "-").toLowerCase(Locale.ENGLISH); + } + }; + + /** + * Converts the field name that uses camel-case define word separation into + * separate words that are separated by the provided {@code separatorString}. + */ + private static String separateCamelCase(String name, String separator) { + StringBuilder translation = new StringBuilder(); + for (int i = 0; i < name.length(); i++) { + char character = name.charAt(i); + if (Character.isUpperCase(character) && translation.length() != 0) { + translation.append(separator); + } + translation.append(character); + } + return translation.toString(); + } + + /** + * Ensures the JSON field names begins with an upper case letter. + */ + private static String upperCaseFirstLetter(String name) { + StringBuilder fieldNameBuilder = new StringBuilder(); + int index = 0; + char firstCharacter = name.charAt(index); + + while (index < name.length() - 1) { + if (Character.isLetter(firstCharacter)) { + break; + } + + fieldNameBuilder.append(firstCharacter); + firstCharacter = name.charAt(++index); + } + + if (index == name.length()) { + return fieldNameBuilder.toString(); + } + + if (!Character.isUpperCase(firstCharacter)) { + String modifiedTarget = modifyString(Character.toUpperCase(firstCharacter), name, ++index); + return fieldNameBuilder.append(modifiedTarget).toString(); + } else { + return name; + } + } + + private static String modifyString(char firstCharacter, String srcString, int indexOfSubstring) { + return (indexOfSubstring < srcString.length()) + ? firstCharacter + srcString.substring(indexOfSubstring) + : String.valueOf(firstCharacter); + } +}
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/FieldNamingStrategy.java b/gson/src/main/java/com/google/gson/FieldNamingStrategy.java new file mode 100644 index 00000000..9be453ad --- /dev/null +++ b/gson/src/main/java/com/google/gson/FieldNamingStrategy.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Field; + +/** + * A mechanism for providing custom field naming in Gson. This allows the client code to translate + * field names into a particular convention that is not supported as a normal Java field + * declaration rules. For example, Java does not support "-" characters in a field name. + * + * @author Inderjeet Singh + * @author Joel Leitch + * @since 1.3 + */ +public interface FieldNamingStrategy { + + /** + * Translates the field name into its JSON field name representation. + * + * @param f the field object that we are translating + * @return the translated field name. + * @since 1.3 + */ + public String translateName(Field f); +} diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java new file mode 100644 index 00000000..d3b172a8 --- /dev/null +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.Excluder; +import com.google.gson.internal.Primitives; +import com.google.gson.internal.Streams; +import com.google.gson.internal.bind.ArrayTypeAdapter; +import com.google.gson.internal.bind.CollectionTypeAdapterFactory; +import com.google.gson.internal.bind.DateTypeAdapter; +import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.internal.bind.MapTypeAdapterFactory; +import com.google.gson.internal.bind.ObjectTypeAdapter; +import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.internal.bind.SqlDateTypeAdapter; +import com.google.gson.internal.bind.TimeTypeAdapter; +import com.google.gson.internal.bind.TypeAdapters; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; + +/** + * This is the main class for using Gson. Gson is typically used by first constructing a + * Gson instance and then invoking {@link #toJson(Object)} or {@link #fromJson(String, Class)} + * methods on it. Gson instances are Thread-safe so you can reuse them freely across multiple + * threads. + * + * <p>You can create a Gson instance by invoking {@code new Gson()} if the default configuration + * is all you need. You can also use {@link GsonBuilder} to build a Gson instance with various + * configuration options such as versioning support, pretty printing, custom + * {@link JsonSerializer}s, {@link JsonDeserializer}s, and {@link InstanceCreator}s.</p> + * + * <p>Here is an example of how Gson is used for a simple Class: + * + * <pre> + * Gson gson = new Gson(); // Or use new GsonBuilder().create(); + * MyType target = new MyType(); + * String json = gson.toJson(target); // serializes target to Json + * MyType target2 = gson.fromJson(json, MyType.class); // deserializes json into target2 + * </pre></p> + * + * <p>If the object that your are serializing/deserializing is a {@code ParameterizedType} + * (i.e. contains at least one type parameter and may be an array) then you must use the + * {@link #toJson(Object, Type)} or {@link #fromJson(String, Type)} method. Here is an + * example for serializing and deserialing a {@code ParameterizedType}: + * + * <pre> + * Type listType = new TypeToken<List<String>>() {}.getType(); + * List<String> target = new LinkedList<String>(); + * target.add("blah"); + * + * Gson gson = new Gson(); + * String json = gson.toJson(target, listType); + * List<String> target2 = gson.fromJson(json, listType); + * </pre></p> + * + * <p>See the <a href="https://sites.google.com/site/gson/gson-user-guide">Gson User Guide</a> + * for a more complete set of examples.</p> + * + * @see com.google.gson.reflect.TypeToken + * + * @author Inderjeet Singh + * @author Joel Leitch + * @author Jesse Wilson + */ +public final class Gson { + static final boolean DEFAULT_JSON_NON_EXECUTABLE = false; + + private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n"; + + /** + * This thread local guards against reentrant calls to getAdapter(). In + * certain object graphs, creating an adapter for a type may recursively + * require an adapter for the same type! Without intervention, the recursive + * lookup would stack overflow. We cheat by returning a proxy type adapter. + * The proxy is wired up once the initial adapter has been created. + */ + private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls + = new ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>>(); + + private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache + = Collections.synchronizedMap(new HashMap<TypeToken<?>, TypeAdapter<?>>()); + + private final List<TypeAdapterFactory> factories; + private final ConstructorConstructor constructorConstructor; + + private final boolean serializeNulls; + private final boolean htmlSafe; + private final boolean generateNonExecutableJson; + private final boolean prettyPrinting; + + final JsonDeserializationContext deserializationContext = new JsonDeserializationContext() { + @SuppressWarnings("unchecked") + public <T> T deserialize(JsonElement json, Type typeOfT) throws JsonParseException { + return (T) fromJson(json, typeOfT); + } + }; + + final JsonSerializationContext serializationContext = new JsonSerializationContext() { + public JsonElement serialize(Object src) { + return toJsonTree(src); + } + public JsonElement serialize(Object src, Type typeOfSrc) { + return toJsonTree(src, typeOfSrc); + } + }; + + /** + * Constructs a Gson object with default configuration. The default configuration has the + * following settings: + * <ul> + * <li>The JSON generated by <code>toJson</code> methods is in compact representation. This + * means that all the unneeded white-space is removed. You can change this behavior with + * {@link GsonBuilder#setPrettyPrinting()}. </li> + * <li>The generated JSON omits all the fields that are null. Note that nulls in arrays are + * kept as is since an array is an ordered list. Moreover, if a field is not null, but its + * generated JSON is empty, the field is kept. You can configure Gson to serialize null values + * by setting {@link GsonBuilder#serializeNulls()}.</li> + * <li>Gson provides default serialization and deserialization for Enums, {@link Map}, + * {@link java.net.URL}, {@link java.net.URI}, {@link java.util.Locale}, {@link java.util.Date}, + * {@link java.math.BigDecimal}, and {@link java.math.BigInteger} classes. If you would prefer + * to change the default representation, you can do so by registering a type adapter through + * {@link GsonBuilder#registerTypeAdapter(Type, Object)}. </li> + * <li>The default Date format is same as {@link java.text.DateFormat#DEFAULT}. This format + * ignores the millisecond portion of the date during serialization. You can change + * this by invoking {@link GsonBuilder#setDateFormat(int)} or + * {@link GsonBuilder#setDateFormat(String)}. </li> + * <li>By default, Gson ignores the {@link com.google.gson.annotations.Expose} annotation. + * You can enable Gson to serialize/deserialize only those fields marked with this annotation + * through {@link GsonBuilder#excludeFieldsWithoutExposeAnnotation()}. </li> + * <li>By default, Gson ignores the {@link com.google.gson.annotations.Since} annotation. You + * can enable Gson to use this annotation through {@link GsonBuilder#setVersion(double)}.</li> + * <li>The default field naming policy for the output Json is same as in Java. So, a Java class + * field <code>versionNumber</code> will be output as <code>"versionNumber"</code> in + * Json. The same rules are applied for mapping incoming Json to the Java classes. You can + * change this policy through {@link GsonBuilder#setFieldNamingPolicy(FieldNamingPolicy)}.</li> + * <li>By default, Gson excludes <code>transient</code> or <code>static</code> fields from + * consideration for serialization and deserialization. You can change this behavior through + * {@link GsonBuilder#excludeFieldsWithModifiers(int...)}.</li> + * </ul> + */ + public Gson() { + this(Excluder.DEFAULT, FieldNamingPolicy.IDENTITY, + Collections.<Type, InstanceCreator<?>>emptyMap(), false, false, DEFAULT_JSON_NON_EXECUTABLE, + true, false, false, LongSerializationPolicy.DEFAULT, + Collections.<TypeAdapterFactory>emptyList()); + } + + Gson(final Excluder excluder, final FieldNamingStrategy fieldNamingPolicy, + final Map<Type, InstanceCreator<?>> instanceCreators, boolean serializeNulls, + boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, + boolean prettyPrinting, boolean serializeSpecialFloatingPointValues, + LongSerializationPolicy longSerializationPolicy, + List<TypeAdapterFactory> typeAdapterFactories) { + this.constructorConstructor = new ConstructorConstructor(instanceCreators); + this.serializeNulls = serializeNulls; + this.generateNonExecutableJson = generateNonExecutableGson; + this.htmlSafe = htmlSafe; + this.prettyPrinting = prettyPrinting; + + List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>(); + + // built-in type adapters that cannot be overridden + factories.add(TypeAdapters.JSON_ELEMENT_FACTORY); + factories.add(ObjectTypeAdapter.FACTORY); + + // the excluder must precede all adapters that handle user-defined types + factories.add(excluder); + + // user's type adapters + factories.addAll(typeAdapterFactories); + + // type adapters for basic platform types + factories.add(TypeAdapters.STRING_FACTORY); + factories.add(TypeAdapters.INTEGER_FACTORY); + factories.add(TypeAdapters.BOOLEAN_FACTORY); + factories.add(TypeAdapters.BYTE_FACTORY); + factories.add(TypeAdapters.SHORT_FACTORY); + factories.add(TypeAdapters.newFactory(long.class, Long.class, + longAdapter(longSerializationPolicy))); + factories.add(TypeAdapters.newFactory(double.class, Double.class, + doubleAdapter(serializeSpecialFloatingPointValues))); + factories.add(TypeAdapters.newFactory(float.class, Float.class, + floatAdapter(serializeSpecialFloatingPointValues))); + factories.add(TypeAdapters.NUMBER_FACTORY); + factories.add(TypeAdapters.CHARACTER_FACTORY); + factories.add(TypeAdapters.STRING_BUILDER_FACTORY); + factories.add(TypeAdapters.STRING_BUFFER_FACTORY); + factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL)); + factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER)); + factories.add(TypeAdapters.URL_FACTORY); + factories.add(TypeAdapters.URI_FACTORY); + factories.add(TypeAdapters.UUID_FACTORY); + factories.add(TypeAdapters.LOCALE_FACTORY); + factories.add(TypeAdapters.INET_ADDRESS_FACTORY); + factories.add(TypeAdapters.BIT_SET_FACTORY); + factories.add(DateTypeAdapter.FACTORY); + factories.add(TypeAdapters.CALENDAR_FACTORY); + factories.add(TimeTypeAdapter.FACTORY); + factories.add(SqlDateTypeAdapter.FACTORY); + factories.add(TypeAdapters.TIMESTAMP_FACTORY); + factories.add(ArrayTypeAdapter.FACTORY); + factories.add(TypeAdapters.CLASS_FACTORY); + + // type adapters for composite and user-defined types + factories.add(new CollectionTypeAdapterFactory(constructorConstructor)); + factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)); + factories.add(new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor)); + factories.add(TypeAdapters.ENUM_FACTORY); + factories.add(new ReflectiveTypeAdapterFactory( + constructorConstructor, fieldNamingPolicy, excluder)); + + this.factories = Collections.unmodifiableList(factories); + } + + private TypeAdapter<Number> doubleAdapter(boolean serializeSpecialFloatingPointValues) { + if (serializeSpecialFloatingPointValues) { + return TypeAdapters.DOUBLE; + } + return new TypeAdapter<Number>() { + @Override public Double read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return in.nextDouble(); + } + @Override public void write(JsonWriter out, Number value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + double doubleValue = value.doubleValue(); + checkValidFloatingPoint(doubleValue); + out.value(value); + } + }; + } + + private TypeAdapter<Number> floatAdapter(boolean serializeSpecialFloatingPointValues) { + if (serializeSpecialFloatingPointValues) { + return TypeAdapters.FLOAT; + } + return new TypeAdapter<Number>() { + @Override public Float read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return (float) in.nextDouble(); + } + @Override public void write(JsonWriter out, Number value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + float floatValue = value.floatValue(); + checkValidFloatingPoint(floatValue); + out.value(value); + } + }; + } + + private void checkValidFloatingPoint(double value) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + throw new IllegalArgumentException(value + + " is not a valid double value as per JSON specification. To override this" + + " behavior, use GsonBuilder.serializeSpecialFloatingPointValues() method."); + } + } + + private TypeAdapter<Number> longAdapter(LongSerializationPolicy longSerializationPolicy) { + if (longSerializationPolicy == LongSerializationPolicy.DEFAULT) { + return TypeAdapters.LONG; + } + return new TypeAdapter<Number>() { + @Override public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return in.nextLong(); + } + @Override public void write(JsonWriter out, Number value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.value(value.toString()); + } + }; + } + + /** + * Returns the type adapter for {@code} type. + * + * @throws IllegalArgumentException if this GSON cannot serialize and + * deserialize {@code type}. + */ + @SuppressWarnings("unchecked") + public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) { + TypeAdapter<?> cached = typeTokenCache.get(type); + if (cached != null) { + return (TypeAdapter<T>) cached; + } + + Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get(); + boolean requiresThreadLocalCleanup = false; + if (threadCalls == null) { + threadCalls = new HashMap<TypeToken<?>, FutureTypeAdapter<?>>(); + calls.set(threadCalls); + requiresThreadLocalCleanup = true; + } + + // the key and value type parameters always agree + FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type); + if (ongoingCall != null) { + return ongoingCall; + } + + try { + FutureTypeAdapter<T> call = new FutureTypeAdapter<T>(); + threadCalls.put(type, call); + + for (TypeAdapterFactory factory : factories) { + TypeAdapter<T> candidate = factory.create(this, type); + if (candidate != null) { + call.setDelegate(candidate); + typeTokenCache.put(type, candidate); + return candidate; + } + } + throw new IllegalArgumentException("GSON cannot handle " + type); + } finally { + threadCalls.remove(type); + + if (requiresThreadLocalCleanup) { + calls.remove(); + } + } + } + + /** + * This method is used to get an alternate type adapter for the specified type. This is used + * to access a type adapter that is overridden by a {@link TypeAdapterFactory} that you + * may have registered. This features is typically used when you want to register a type + * adapter that does a little bit of work but then delegates further processing to the Gson + * default type adapter. Here is an example: + * <p>Let's say we want to write a type adapter that counts the number of objects being read + * from or written to JSON. We can achieve this by writing a type adapter factory that uses + * the <code>getDelegateAdapter</code> method: + * <pre> {@code + * class StatsTypeAdapterFactory implements TypeAdapterFactory { + * public int numReads = 0; + * public int numWrites = 0; + * public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + * final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type); + * return new TypeAdapter<T>() { + * public void write(JsonWriter out, T value) throws IOException { + * ++numWrites; + * delegate.write(out, value); + * } + * public T read(JsonReader in) throws IOException { + * ++numReads; + * return delegate.read(in); + * } + * }; + * } + * } + * } </pre> + * This factory can now be used like this: + * <pre> {@code + * StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory(); + * Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create(); + * // Call gson.toJson() and fromJson methods on objects + * System.out.println("Num JSON reads" + stats.numReads); + * System.out.println("Num JSON writes" + stats.numWrites); + * }</pre> + * Note that this call will skip all factories registered before {@code skipPast}. In case of + * multiple TypeAdapterFactories registered it is up to the caller of this function to insure + * that the order of registration does not prevent this method from reaching a factory they + * would expect to reply from this call. + * Note that since you can not override type adapter factories for String and Java primitive + * types, our stats factory will not count the number of String or primitives that will be + * read or written. + * @param skipPast The type adapter factory that needs to be skipped while searching for + * a matching type adapter. In most cases, you should just pass <i>this</i> (the type adapter + * factory from where {@link #getDelegateAdapter} method is being invoked). + * @param type Type for which the delegate adapter is being searched for. + * + * @since 2.2 + */ + public <T> TypeAdapter<T> getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken<T> type) { + boolean skipPastFound = false; + // Skip past if and only if the specified factory is present in the factories. + // This is useful because the factories created through JsonAdapter annotations are not + // registered in this list. + if (!factories.contains(skipPast)) skipPastFound = true; + + for (TypeAdapterFactory factory : factories) { + if (!skipPastFound) { + if (factory == skipPast) { + skipPastFound = true; + } + continue; + } + + TypeAdapter<T> candidate = factory.create(this, type); + if (candidate != null) { + return candidate; + } + } + throw new IllegalArgumentException("GSON cannot serialize " + type); + } + + /** + * Returns the type adapter for {@code} type. + * + * @throws IllegalArgumentException if this GSON cannot serialize and + * deserialize {@code type}. + */ + public <T> TypeAdapter<T> getAdapter(Class<T> type) { + return getAdapter(TypeToken.get(type)); + } + + /** + * This method serializes the specified object into its equivalent representation as a tree of + * {@link JsonElement}s. This method should be used when the specified object is not a generic + * type. This method uses {@link Class#getClass()} to get the type for the specified object, but + * the {@code getClass()} loses the generic type information because of the Type Erasure feature + * of Java. Note that this method works fine if the any of the object fields are of generic type, + * just the object itself should not be of a generic type. If the object is of generic type, use + * {@link #toJsonTree(Object, Type)} instead. + * + * @param src the object for which Json representation is to be created setting for Gson + * @return Json representation of {@code src}. + * @since 1.4 + */ + public JsonElement toJsonTree(Object src) { + if (src == null) { + return JsonNull.INSTANCE; + } + return toJsonTree(src, src.getClass()); + } + + /** + * This method serializes the specified object, including those of generic types, into its + * equivalent representation as a tree of {@link JsonElement}s. This method must be used if the + * specified object is a generic type. For non-generic objects, use {@link #toJsonTree(Object)} + * instead. + * + * @param src the object for which JSON representation is to be created + * @param typeOfSrc The specific genericized type of src. You can obtain + * this type by using the {@link com.google.gson.reflect.TypeToken} class. For example, + * to get the type for {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @return Json representation of {@code src} + * @since 1.4 + */ + public JsonElement toJsonTree(Object src, Type typeOfSrc) { + JsonTreeWriter writer = new JsonTreeWriter(); + toJson(src, typeOfSrc, writer); + return writer.get(); + } + + /** + * This method serializes the specified object into its equivalent Json representation. + * This method should be used when the specified object is not a generic type. This method uses + * {@link Class#getClass()} to get the type for the specified object, but the + * {@code getClass()} loses the generic type information because of the Type Erasure feature + * of Java. Note that this method works fine if the any of the object fields are of generic type, + * just the object itself should not be of a generic type. If the object is of generic type, use + * {@link #toJson(Object, Type)} instead. If you want to write out the object to a + * {@link Writer}, use {@link #toJson(Object, Appendable)} instead. + * + * @param src the object for which Json representation is to be created setting for Gson + * @return Json representation of {@code src}. + */ + public String toJson(Object src) { + if (src == null) { + return toJson(JsonNull.INSTANCE); + } + return toJson(src, src.getClass()); + } + + /** + * This method serializes the specified object, including those of generic types, into its + * equivalent Json representation. This method must be used if the specified object is a generic + * type. For non-generic objects, use {@link #toJson(Object)} instead. If you want to write out + * the object to a {@link Appendable}, use {@link #toJson(Object, Type, Appendable)} instead. + * + * @param src the object for which JSON representation is to be created + * @param typeOfSrc The specific genericized type of src. You can obtain + * this type by using the {@link com.google.gson.reflect.TypeToken} class. For example, + * to get the type for {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @return Json representation of {@code src} + */ + public String toJson(Object src, Type typeOfSrc) { + StringWriter writer = new StringWriter(); + toJson(src, typeOfSrc, writer); + return writer.toString(); + } + + /** + * This method serializes the specified object into its equivalent Json representation. + * This method should be used when the specified object is not a generic type. This method uses + * {@link Class#getClass()} to get the type for the specified object, but the + * {@code getClass()} loses the generic type information because of the Type Erasure feature + * of Java. Note that this method works fine if the any of the object fields are of generic type, + * just the object itself should not be of a generic type. If the object is of generic type, use + * {@link #toJson(Object, Type, Appendable)} instead. + * + * @param src the object for which Json representation is to be created setting for Gson + * @param writer Writer to which the Json representation needs to be written + * @throws JsonIOException if there was a problem writing to the writer + * @since 1.2 + */ + public void toJson(Object src, Appendable writer) throws JsonIOException { + if (src != null) { + toJson(src, src.getClass(), writer); + } else { + toJson(JsonNull.INSTANCE, writer); + } + } + + /** + * This method serializes the specified object, including those of generic types, into its + * equivalent Json representation. This method must be used if the specified object is a generic + * type. For non-generic objects, use {@link #toJson(Object, Appendable)} instead. + * + * @param src the object for which JSON representation is to be created + * @param typeOfSrc The specific genericized type of src. You can obtain + * this type by using the {@link com.google.gson.reflect.TypeToken} class. For example, + * to get the type for {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @param writer Writer to which the Json representation of src needs to be written. + * @throws JsonIOException if there was a problem writing to the writer + * @since 1.2 + */ + public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOException { + try { + JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer)); + toJson(src, typeOfSrc, jsonWriter); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + /** + * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to + * {@code writer}. + * @throws JsonIOException if there was a problem writing to the writer + */ + @SuppressWarnings("unchecked") + public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException { + TypeAdapter<?> adapter = getAdapter(TypeToken.get(typeOfSrc)); + boolean oldLenient = writer.isLenient(); + writer.setLenient(true); + boolean oldHtmlSafe = writer.isHtmlSafe(); + writer.setHtmlSafe(htmlSafe); + boolean oldSerializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(serializeNulls); + try { + ((TypeAdapter<Object>) adapter).write(writer, src); + } catch (IOException e) { + throw new JsonIOException(e); + } finally { + writer.setLenient(oldLenient); + writer.setHtmlSafe(oldHtmlSafe); + writer.setSerializeNulls(oldSerializeNulls); + } + } + + /** + * Converts a tree of {@link JsonElement}s into its equivalent JSON representation. + * + * @param jsonElement root of a tree of {@link JsonElement}s + * @return JSON String representation of the tree + * @since 1.4 + */ + public String toJson(JsonElement jsonElement) { + StringWriter writer = new StringWriter(); + toJson(jsonElement, writer); + return writer.toString(); + } + + /** + * Writes out the equivalent JSON for a tree of {@link JsonElement}s. + * + * @param jsonElement root of a tree of {@link JsonElement}s + * @param writer Writer to which the Json representation needs to be written + * @throws JsonIOException if there was a problem writing to the writer + * @since 1.4 + */ + public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOException { + try { + JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer)); + toJson(jsonElement, jsonWriter); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a new JSON writer configured for the settings on this Gson instance. + */ + public JsonWriter newJsonWriter(Writer writer) throws IOException { + if (generateNonExecutableJson) { + writer.write(JSON_NON_EXECUTABLE_PREFIX); + } + JsonWriter jsonWriter = new JsonWriter(writer); + if (prettyPrinting) { + jsonWriter.setIndent(" "); + } + jsonWriter.setSerializeNulls(serializeNulls); + return jsonWriter; + } + + /** + * Writes the JSON for {@code jsonElement} to {@code writer}. + * @throws JsonIOException if there was a problem writing to the writer + */ + public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException { + boolean oldLenient = writer.isLenient(); + writer.setLenient(true); + boolean oldHtmlSafe = writer.isHtmlSafe(); + writer.setHtmlSafe(htmlSafe); + boolean oldSerializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(serializeNulls); + try { + Streams.write(jsonElement, writer); + } catch (IOException e) { + throw new JsonIOException(e); + } finally { + writer.setLenient(oldLenient); + writer.setHtmlSafe(oldHtmlSafe); + writer.setSerializeNulls(oldSerializeNulls); + } + } + + /** + * This method deserializes the specified Json into an object of the specified class. It is not + * suitable to use if the specified class is a generic type since it will not have the generic + * type information because of the Type Erasure feature of Java. Therefore, this method should not + * be used if the desired type is a generic type. Note that this method works fine if the any of + * the fields of the specified object are generics, just the object itself should not be a + * generic type. For the cases when the object is of generic type, invoke + * {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of + * a String, use {@link #fromJson(Reader, Class)} instead. + * + * @param <T> the type of the desired object + * @param json the string from which the object is to be deserialized + * @param classOfT the class of T + * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}. + * @throws JsonSyntaxException if json is not a valid representation for an object of type + * classOfT + */ + public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { + Object object = fromJson(json, (Type) classOfT); + return Primitives.wrap(classOfT).cast(object); + } + + /** + * This method deserializes the specified Json into an object of the specified type. This method + * is useful if the specified object is a generic type. For non-generic objects, use + * {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of + * a String, use {@link #fromJson(Reader, Type)} instead. + * + * @param <T> the type of the desired object + * @param json the string from which the object is to be deserialized + * @param typeOfT The specific genericized type of src. You can obtain this type by using the + * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}. + * @throws JsonParseException if json is not a valid representation for an object of type typeOfT + * @throws JsonSyntaxException if json is not a valid representation for an object of type + */ + @SuppressWarnings("unchecked") + public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException { + if (json == null) { + return null; + } + StringReader reader = new StringReader(json); + T target = (T) fromJson(reader, typeOfT); + return target; + } + + /** + * This method deserializes the Json read from the specified reader into an object of the + * specified class. It is not suitable to use if the specified class is a generic type since it + * will not have the generic type information because of the Type Erasure feature of Java. + * Therefore, this method should not be used if the desired type is a generic type. Note that + * this method works fine if the any of the fields of the specified object are generics, just the + * object itself should not be a generic type. For the cases when the object is of generic type, + * invoke {@link #fromJson(Reader, Type)}. If you have the Json in a String form instead of a + * {@link Reader}, use {@link #fromJson(String, Class)} instead. + * + * @param <T> the type of the desired object + * @param json the reader producing the Json from which the object is to be deserialized. + * @param classOfT the class of T + * @return an object of type T from the string. Returns {@code null} if {@code json} is at EOF. + * @throws JsonIOException if there was a problem reading from the Reader + * @throws JsonSyntaxException if json is not a valid representation for an object of type + * @since 1.2 + */ + public <T> T fromJson(Reader json, Class<T> classOfT) throws JsonSyntaxException, JsonIOException { + JsonReader jsonReader = new JsonReader(json); + Object object = fromJson(jsonReader, classOfT); + assertFullConsumption(object, jsonReader); + return Primitives.wrap(classOfT).cast(object); + } + + /** + * This method deserializes the Json read from the specified reader into an object of the + * specified type. This method is useful if the specified object is a generic type. For + * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the Json in a + * String form instead of a {@link Reader}, use {@link #fromJson(String, Type)} instead. + * + * @param <T> the type of the desired object + * @param json the reader producing Json from which the object is to be deserialized + * @param typeOfT The specific genericized type of src. You can obtain this type by using the + * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @return an object of type T from the json. Returns {@code null} if {@code json} is at EOF. + * @throws JsonIOException if there was a problem reading from the Reader + * @throws JsonSyntaxException if json is not a valid representation for an object of type + * @since 1.2 + */ + @SuppressWarnings("unchecked") + public <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { + JsonReader jsonReader = new JsonReader(json); + T object = (T) fromJson(jsonReader, typeOfT); + assertFullConsumption(object, jsonReader); + return object; + } + + private static void assertFullConsumption(Object obj, JsonReader reader) { + try { + if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonIOException("JSON document was not fully consumed."); + } + } catch (MalformedJsonException e) { + throw new JsonSyntaxException(e); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + /** + * Reads the next JSON value from {@code reader} and convert it to an object + * of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF. + * Since Type is not parameterized by T, this method is type unsafe and should be used carefully + * + * @throws JsonIOException if there was a problem writing to the Reader + * @throws JsonSyntaxException if json is not a valid representation for an object of type + */ + @SuppressWarnings("unchecked") + public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { + boolean isEmpty = true; + boolean oldLenient = reader.isLenient(); + reader.setLenient(true); + try { + reader.peek(); + isEmpty = false; + TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT); + TypeAdapter<T> typeAdapter = getAdapter(typeToken); + T object = typeAdapter.read(reader); + return object; + } catch (EOFException e) { + /* + * For compatibility with JSON 1.5 and earlier, we return null for empty + * documents instead of throwing. + */ + if (isEmpty) { + return null; + } + throw new JsonSyntaxException(e); + } catch (IllegalStateException e) { + throw new JsonSyntaxException(e); + } catch (IOException e) { + // TODO(inder): Figure out whether it is indeed right to rethrow this as JsonSyntaxException + throw new JsonSyntaxException(e); + } finally { + reader.setLenient(oldLenient); + } + } + + /** + * This method deserializes the Json read from the specified parse tree into an object of the + * specified type. It is not suitable to use if the specified class is a generic type since it + * will not have the generic type information because of the Type Erasure feature of Java. + * Therefore, this method should not be used if the desired type is a generic type. Note that + * this method works fine if the any of the fields of the specified object are generics, just the + * object itself should not be a generic type. For the cases when the object is of generic type, + * invoke {@link #fromJson(JsonElement, Type)}. + * @param <T> the type of the desired object + * @param json the root of the parse tree of {@link JsonElement}s from which the object is to + * be deserialized + * @param classOfT The class of T + * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null}. + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * @since 1.3 + */ + public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException { + Object object = fromJson(json, (Type) classOfT); + return Primitives.wrap(classOfT).cast(object); + } + + /** + * This method deserializes the Json read from the specified parse tree into an object of the + * specified type. This method is useful if the specified object is a generic type. For + * non-generic objects, use {@link #fromJson(JsonElement, Class)} instead. + * + * @param <T> the type of the desired object + * @param json the root of the parse tree of {@link JsonElement}s from which the object is to + * be deserialized + * @param typeOfT The specific genericized type of src. You can obtain this type by using the + * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * {@code Collection<Foo>}, you should use: + * <pre> + * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType(); + * </pre> + * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null}. + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * @since 1.3 + */ + @SuppressWarnings("unchecked") + public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException { + if (json == null) { + return null; + } + return (T) fromJson(new JsonTreeReader(json), typeOfT); + } + + static class FutureTypeAdapter<T> extends TypeAdapter<T> { + private TypeAdapter<T> delegate; + + public void setDelegate(TypeAdapter<T> typeAdapter) { + if (delegate != null) { + throw new AssertionError(); + } + delegate = typeAdapter; + } + + @Override public T read(JsonReader in) throws IOException { + if (delegate == null) { + throw new IllegalStateException(); + } + return delegate.read(in); + } + + @Override public void write(JsonWriter out, T value) throws IOException { + if (delegate == null) { + throw new IllegalStateException(); + } + delegate.write(out, value); + } + } + + @Override + public String toString() { + return new StringBuilder("{serializeNulls:") + .append(serializeNulls) + .append("factories:").append(factories) + .append(",instanceCreators:").append(constructorConstructor) + .append("}") + .toString(); + } +} diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java new file mode 100644 index 00000000..e6c0b8c0 --- /dev/null +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.internal.$Gson$Preconditions; +import com.google.gson.internal.Excluder; +import com.google.gson.internal.bind.TypeAdapters; +import com.google.gson.reflect.TypeToken; + +/** + * <p>Use this builder to construct a {@link Gson} instance when you need to set configuration + * options other than the default. For {@link Gson} with default configuration, it is simpler to + * use {@code new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its + * various configuration methods, and finally calling create.</p> + * + * <p>The following is an example shows how to use the {@code GsonBuilder} to construct a Gson + * instance: + * + * <pre> + * Gson gson = new GsonBuilder() + * .registerTypeAdapter(Id.class, new IdTypeAdapter()) + * .enableComplexMapKeySerialization() + * .serializeNulls() + * .setDateFormat(DateFormat.LONG) + * .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) + * .setPrettyPrinting() + * .setVersion(1.0) + * .create(); + * </pre></p> + * + * <p>NOTES: + * <ul> + * <li> the order of invocation of configuration methods does not matter.</li> + * <li> The default serialization of {@link Date} and its subclasses in Gson does + * not contain time-zone information. So, if you are using date/time instances, + * use {@code GsonBuilder} and its {@code setDateFormat} methods.</li> + * </ul> + * </p> + * + * @author Inderjeet Singh + * @author Joel Leitch + * @author Jesse Wilson + */ +public final class GsonBuilder { + private Excluder excluder = Excluder.DEFAULT; + private LongSerializationPolicy longSerializationPolicy = LongSerializationPolicy.DEFAULT; + private FieldNamingStrategy fieldNamingPolicy = FieldNamingPolicy.IDENTITY; + private final Map<Type, InstanceCreator<?>> instanceCreators + = new HashMap<Type, InstanceCreator<?>>(); + private final List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>(); + /** tree-style hierarchy factories. These come after factories for backwards compatibility. */ + private final List<TypeAdapterFactory> hierarchyFactories = new ArrayList<TypeAdapterFactory>(); + private boolean serializeNulls; + private String datePattern; + private int dateStyle = DateFormat.DEFAULT; + private int timeStyle = DateFormat.DEFAULT; + private boolean complexMapKeySerialization; + private boolean serializeSpecialFloatingPointValues; + private boolean escapeHtmlChars = true; + private boolean prettyPrinting; + private boolean generateNonExecutableJson; + + /** + * Creates a GsonBuilder instance that can be used to build Gson with various configuration + * settings. GsonBuilder follows the builder pattern, and it is typically used by first + * invoking various configuration methods to set desired options, and finally calling + * {@link #create()}. + */ + public GsonBuilder() { + } + + /** + * Configures Gson to enable versioning support. + * + * @param ignoreVersionsAfter any field or type marked with a version higher than this value + * are ignored during serialization or deserialization. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder setVersion(double ignoreVersionsAfter) { + excluder = excluder.withVersion(ignoreVersionsAfter); + return this; + } + + /** + * Configures Gson to excludes all class fields that have the specified modifiers. By default, + * Gson will exclude all fields marked transient or static. This method will override that + * behavior. + * + * @param modifiers the field modifiers. You must use the modifiers specified in the + * {@link java.lang.reflect.Modifier} class. For example, + * {@link java.lang.reflect.Modifier#TRANSIENT}, + * {@link java.lang.reflect.Modifier#STATIC}. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder excludeFieldsWithModifiers(int... modifiers) { + excluder = excluder.withModifiers(modifiers); + return this; + } + + /** + * Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some + * special text. This prevents attacks from third-party sites through script sourcing. See + * <a href="http://code.google.com/p/google-gson/issues/detail?id=42">Gson Issue 42</a> + * for details. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder generateNonExecutableJson() { + this.generateNonExecutableJson = true; + return this; + } + + /** + * Configures Gson to exclude all fields from consideration for serialization or deserialization + * that do not have the {@link com.google.gson.annotations.Expose} annotation. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder excludeFieldsWithoutExposeAnnotation() { + excluder = excluder.excludeFieldsWithoutExposeAnnotation(); + return this; + } + + /** + * Configure Gson to serialize null fields. By default, Gson omits all fields that are null + * during serialization. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.2 + */ + public GsonBuilder serializeNulls() { + this.serializeNulls = true; + return this; + } + + /** + * Enabling this feature will only change the serialized form if the map key is + * a complex type (i.e. non-primitive) in its <strong>serialized</strong> JSON + * form. The default implementation of map serialization uses {@code toString()} + * on the key; however, when this is called then one of the following cases + * apply: + * + * <h3>Maps as JSON objects</h3> + * For this case, assume that a type adapter is registered to serialize and + * deserialize some {@code Point} class, which contains an x and y coordinate, + * to/from the JSON Primitive string value {@code "(x,y)"}. The Java map would + * then be serialized as a {@link JsonObject}. + * + * <p>Below is an example: + * <pre> {@code + * Gson gson = new GsonBuilder() + * .register(Point.class, new MyPointTypeAdapter()) + * .enableComplexMapKeySerialization() + * .create(); + * + * Map<Point, String> original = new LinkedHashMap<Point, String>(); + * original.put(new Point(5, 6), "a"); + * original.put(new Point(8, 8), "b"); + * System.out.println(gson.toJson(original, type)); + * }</pre> + * The above code prints this JSON object:<pre> {@code + * { + * "(5,6)": "a", + * "(8,8)": "b" + * } + * }</pre> + * + * <h3>Maps as JSON arrays</h3> + * For this case, assume that a type adapter was NOT registered for some + * {@code Point} class, but rather the default Gson serialization is applied. + * In this case, some {@code new Point(2,3)} would serialize as {@code + * {"x":2,"y":5}}. + * + * <p>Given the assumption above, a {@code Map<Point, String>} will be + * serialize as an array of arrays (can be viewed as an entry set of pairs). + * + * <p>Below is an example of serializing complex types as JSON arrays: + * <pre> {@code + * Gson gson = new GsonBuilder() + * .enableComplexMapKeySerialization() + * .create(); + * + * Map<Point, String> original = new LinkedHashMap<Point, String>(); + * original.put(new Point(5, 6), "a"); + * original.put(new Point(8, 8), "b"); + * System.out.println(gson.toJson(original, type)); + * } + * + * The JSON output would look as follows: + * <pre> {@code + * [ + * [ + * { + * "x": 5, + * "y": 6 + * }, + * "a" + * ], + * [ + * { + * "x": 8, + * "y": 8 + * }, + * "b" + * ] + * ] + * }</pre> + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.7 + */ + public GsonBuilder enableComplexMapKeySerialization() { + complexMapKeySerialization = true; + return this; + } + + /** + * Configures Gson to exclude inner classes during serialization. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder disableInnerClassSerialization() { + excluder = excluder.disableInnerClassSerialization(); + return this; + } + + /** + * Configures Gson to apply a specific serialization policy for {@code Long} and {@code long} + * objects. + * + * @param serializationPolicy the particular policy to use for serializing longs. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder setLongSerializationPolicy(LongSerializationPolicy serializationPolicy) { + this.longSerializationPolicy = serializationPolicy; + return this; + } + + /** + * Configures Gson to apply a specific naming policy to an object's field during serialization + * and deserialization. + * + * @param namingConvention the JSON field naming convention to use for serialization and + * deserialization. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder setFieldNamingPolicy(FieldNamingPolicy namingConvention) { + this.fieldNamingPolicy = namingConvention; + return this; + } + + /** + * Configures Gson to apply a specific naming policy strategy to an object's field during + * serialization and deserialization. + * + * @param fieldNamingStrategy the actual naming strategy to apply to the fields + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrategy) { + this.fieldNamingPolicy = fieldNamingStrategy; + return this; + } + + /** + * Configures Gson to apply a set of exclusion strategies during both serialization and + * deserialization. Each of the {@code strategies} will be applied as a disjunction rule. + * This means that if one of the {@code strategies} suggests that a field (or class) should be + * skipped then that field (or object) is skipped during serializaiton/deserialization. + * + * @param strategies the set of strategy object to apply during object (de)serialization. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.4 + */ + public GsonBuilder setExclusionStrategies(ExclusionStrategy... strategies) { + for (ExclusionStrategy strategy : strategies) { + excluder = excluder.withExclusionStrategy(strategy, true, true); + } + return this; + } + + /** + * Configures Gson to apply the passed in exclusion strategy during serialization. + * If this method is invoked numerous times with different exclusion strategy objects + * then the exclusion strategies that were added will be applied as a disjunction rule. + * This means that if one of the added exclusion strategies suggests that a field (or + * class) should be skipped then that field (or object) is skipped during its + * serialization. + * + * @param strategy an exclusion strategy to apply during serialization. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.7 + */ + public GsonBuilder addSerializationExclusionStrategy(ExclusionStrategy strategy) { + excluder = excluder.withExclusionStrategy(strategy, true, false); + return this; + } + + /** + * Configures Gson to apply the passed in exclusion strategy during deserialization. + * If this method is invoked numerous times with different exclusion strategy objects + * then the exclusion strategies that were added will be applied as a disjunction rule. + * This means that if one of the added exclusion strategies suggests that a field (or + * class) should be skipped then that field (or object) is skipped during its + * deserialization. + * + * @param strategy an exclusion strategy to apply during deserialization. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.7 + */ + public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strategy) { + excluder = excluder.withExclusionStrategy(strategy, false, true); + return this; + } + + /** + * Configures Gson to output Json that fits in a page for pretty printing. This option only + * affects Json serialization. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder setPrettyPrinting() { + prettyPrinting = true; + return this; + } + + /** + * By default, Gson escapes HTML characters such as < > etc. Use this option to configure + * Gson to pass-through HTML characters as is. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder disableHtmlEscaping() { + this.escapeHtmlChars = false; + return this; + } + + /** + * Configures Gson to serialize {@code Date} objects according to the pattern provided. You can + * call this method or {@link #setDateFormat(int)} multiple times, but only the last invocation + * will be used to decide the serialization format. + * + * <p>The date format will be used to serialize and deserialize {@link java.util.Date}, {@link + * java.sql.Timestamp} and {@link java.sql.Date}. + * + * <p>Note that this pattern must abide by the convention provided by {@code SimpleDateFormat} + * class. See the documentation in {@link java.text.SimpleDateFormat} for more information on + * valid date and time patterns.</p> + * + * @param pattern the pattern that dates will be serialized/deserialized to/from + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.2 + */ + public GsonBuilder setDateFormat(String pattern) { + // TODO(Joel): Make this fail fast if it is an invalid date format + this.datePattern = pattern; + return this; + } + + /** + * Configures Gson to to serialize {@code Date} objects according to the style value provided. + * You can call this method or {@link #setDateFormat(String)} multiple times, but only the last + * invocation will be used to decide the serialization format. + * + * <p>Note that this style value should be one of the predefined constants in the + * {@code DateFormat} class. See the documentation in {@link java.text.DateFormat} for more + * information on the valid style constants.</p> + * + * @param style the predefined date style that date objects will be serialized/deserialized + * to/from + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.2 + */ + public GsonBuilder setDateFormat(int style) { + this.dateStyle = style; + this.datePattern = null; + return this; + } + + /** + * Configures Gson to to serialize {@code Date} objects according to the style value provided. + * You can call this method or {@link #setDateFormat(String)} multiple times, but only the last + * invocation will be used to decide the serialization format. + * + * <p>Note that this style value should be one of the predefined constants in the + * {@code DateFormat} class. See the documentation in {@link java.text.DateFormat} for more + * information on the valid style constants.</p> + * + * @param dateStyle the predefined date style that date objects will be serialized/deserialized + * to/from + * @param timeStyle the predefined style for the time portion of the date objects + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.2 + */ + public GsonBuilder setDateFormat(int dateStyle, int timeStyle) { + this.dateStyle = dateStyle; + this.timeStyle = timeStyle; + this.datePattern = null; + return this; + } + + /** + * Configures Gson for custom serialization or deserialization. This method combines the + * registration of an {@link TypeAdapter}, {@link InstanceCreator}, {@link JsonSerializer}, and a + * {@link JsonDeserializer}. It is best used when a single object {@code typeAdapter} implements + * all the required interfaces for custom serialization with Gson. If a type adapter was + * previously registered for the specified {@code type}, it is overwritten. + * + * <p>This registers the type specified and no other types: you must manually register related + * types! For example, applications registering {@code boolean.class} should also register {@code + * Boolean.class}. + * + * @param type the type definition for the type adapter being registered + * @param typeAdapter This object must implement at least one of the {@link TypeAdapter}, + * {@link InstanceCreator}, {@link JsonSerializer}, and a {@link JsonDeserializer} interfaces. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) { + $Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?> + || typeAdapter instanceof JsonDeserializer<?> + || typeAdapter instanceof InstanceCreator<?> + || typeAdapter instanceof TypeAdapter<?>); + if (typeAdapter instanceof InstanceCreator<?>) { + instanceCreators.put(type, (InstanceCreator) typeAdapter); + } + if (typeAdapter instanceof JsonSerializer<?> || typeAdapter instanceof JsonDeserializer<?>) { + TypeToken<?> typeToken = TypeToken.get(type); + factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter)); + } + if (typeAdapter instanceof TypeAdapter<?>) { + factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter)); + } + return this; + } + + /** + * Register a factory for type adapters. Registering a factory is useful when the type + * adapter needs to be configured based on the type of the field being processed. Gson + * is designed to handle a large number of factories, so you should consider registering + * them to be at par with registering an individual type adapter. + * + * @since 2.1 + */ + public GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory) { + factories.add(factory); + return this; + } + + /** + * Configures Gson for custom serialization or deserialization for an inheritance type hierarchy. + * This method combines the registration of a {@link TypeAdapter}, {@link JsonSerializer} and + * a {@link JsonDeserializer}. If a type adapter was previously registered for the specified + * type hierarchy, it is overridden. If a type adapter is registered for a specific type in + * the type hierarchy, it will be invoked instead of the one registered for the type hierarchy. + * + * @param baseType the class definition for the type adapter being registered for the base class + * or interface + * @param typeAdapter This object must implement at least one of {@link TypeAdapter}, + * {@link JsonSerializer} or {@link JsonDeserializer} interfaces. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.7 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public GsonBuilder registerTypeHierarchyAdapter(Class<?> baseType, Object typeAdapter) { + $Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?> + || typeAdapter instanceof JsonDeserializer<?> + || typeAdapter instanceof TypeAdapter<?>); + if (typeAdapter instanceof JsonDeserializer || typeAdapter instanceof JsonSerializer) { + hierarchyFactories.add(0, + TreeTypeAdapter.newTypeHierarchyFactory(baseType, typeAdapter)); + } + if (typeAdapter instanceof TypeAdapter<?>) { + factories.add(TypeAdapters.newTypeHierarchyFactory(baseType, (TypeAdapter)typeAdapter)); + } + return this; + } + + /** + * Section 2.4 of <a href="http://www.ietf.org/rfc/rfc4627.txt">JSON specification</a> disallows + * special double values (NaN, Infinity, -Infinity). However, + * <a href="http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf">Javascript + * specification</a> (see section 4.3.20, 4.3.22, 4.3.23) allows these values as valid Javascript + * values. Moreover, most JavaScript engines will accept these special values in JSON without + * problem. So, at a practical level, it makes sense to accept these values as valid JSON even + * though JSON specification disallows them. + * + * <p>Gson always accepts these special values during deserialization. However, it outputs + * strictly compliant JSON. Hence, if it encounters a float value {@link Float#NaN}, + * {@link Float#POSITIVE_INFINITY}, {@link Float#NEGATIVE_INFINITY}, or a double value + * {@link Double#NaN}, {@link Double#POSITIVE_INFINITY}, {@link Double#NEGATIVE_INFINITY}, it + * will throw an {@link IllegalArgumentException}. This method provides a way to override the + * default behavior when you know that the JSON receiver will be able to handle these special + * values. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 1.3 + */ + public GsonBuilder serializeSpecialFloatingPointValues() { + this.serializeSpecialFloatingPointValues = true; + return this; + } + + /** + * Creates a {@link Gson} instance based on the current configuration. This method is free of + * side-effects to this {@code GsonBuilder} instance and hence can be called multiple times. + * + * @return an instance of Gson configured with the options currently set in this builder + */ + public Gson create() { + List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>(); + factories.addAll(this.factories); + Collections.reverse(factories); + factories.addAll(this.hierarchyFactories); + addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories); + + return new Gson(excluder, fieldNamingPolicy, instanceCreators, + serializeNulls, complexMapKeySerialization, + generateNonExecutableJson, escapeHtmlChars, prettyPrinting, + serializeSpecialFloatingPointValues, longSerializationPolicy, factories); + } + + private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle, + List<TypeAdapterFactory> factories) { + DefaultDateTypeAdapter dateTypeAdapter; + if (datePattern != null && !"".equals(datePattern.trim())) { + dateTypeAdapter = new DefaultDateTypeAdapter(datePattern); + } else if (dateStyle != DateFormat.DEFAULT && timeStyle != DateFormat.DEFAULT) { + dateTypeAdapter = new DefaultDateTypeAdapter(dateStyle, timeStyle); + } else { + return; + } + + factories.add(TreeTypeAdapter.newFactory(TypeToken.get(Date.class), dateTypeAdapter)); + factories.add(TreeTypeAdapter.newFactory(TypeToken.get(Timestamp.class), dateTypeAdapter)); + factories.add(TreeTypeAdapter.newFactory(TypeToken.get(java.sql.Date.class), dateTypeAdapter)); + } +} diff --git a/gson/src/main/java/com/google/gson/InstanceCreator.java b/gson/src/main/java/com/google/gson/InstanceCreator.java new file mode 100644 index 00000000..d5096a07 --- /dev/null +++ b/gson/src/main/java/com/google/gson/InstanceCreator.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; + +/** + * This interface is implemented to create instances of a class that does not define a no-args + * constructor. If you can modify the class, you should instead add a private, or public + * no-args constructor. However, that is not possible for library classes, such as JDK classes, or + * a third-party library that you do not have source-code of. In such cases, you should define an + * instance creator for the class. Implementations of this interface should be registered with + * {@link GsonBuilder#registerTypeAdapter(Type, Object)} method before Gson will be able to use + * them. + * <p>Let us look at an example where defining an InstanceCreator might be useful. The + * {@code Id} class defined below does not have a default no-args constructor.</p> + * + * <pre> + * public class Id<T> { + * private final Class<T> clazz; + * private final long value; + * public Id(Class<T> clazz, long value) { + * this.clazz = clazz; + * this.value = value; + * } + * } + * </pre> + * + * <p>If Gson encounters an object of type {@code Id} during deserialization, it will throw an + * exception. The easiest way to solve this problem will be to add a (public or private) no-args + * constructor as follows:</p> + * + * <pre> + * private Id() { + * this(Object.class, 0L); + * } + * </pre> + * + * <p>However, let us assume that the developer does not have access to the source-code of the + * {@code Id} class, or does not want to define a no-args constructor for it. The developer + * can solve this problem by defining an {@code InstanceCreator} for {@code Id}:</p> + * + * <pre> + * class IdInstanceCreator implements InstanceCreator<Id> { + * public Id createInstance(Type type) { + * return new Id(Object.class, 0L); + * } + * } + * </pre> + * + * <p>Note that it does not matter what the fields of the created instance contain since Gson will + * overwrite them with the deserialized values specified in Json. You should also ensure that a + * <i>new</i> object is returned, not a common object since its fields will be overwritten. + * The developer will need to register {@code IdInstanceCreator} with Gson as follows:</p> + * + * <pre> + * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdInstanceCreator()).create(); + * </pre> + * + * @param <T> the type of object that will be created by this implementation. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public interface InstanceCreator<T> { + + /** + * Gson invokes this call-back method during deserialization to create an instance of the + * specified type. The fields of the returned instance are overwritten with the data present + * in the Json. Since the prior contents of the object are destroyed and overwritten, do not + * return an instance that is useful elsewhere. In particular, do not return a common instance, + * always use {@code new} to create a new instance. + * + * @param type the parameterized T represented as a {@link Type}. + * @return a default object instance of type T. + */ + public T createInstance(Type type); +} diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java new file mode 100644 index 00000000..c664a5e1 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonArray.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A class representing an array type in Json. An array is a list of {@link JsonElement}s each of + * which can be of a different type. This is an ordered list, meaning that the order in which + * elements are added is preserved. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public final class JsonArray extends JsonElement implements Iterable<JsonElement> { + private final List<JsonElement> elements; + + /** + * Creates an empty JsonArray. + */ + public JsonArray() { + elements = new ArrayList<JsonElement>(); + } + + @Override + JsonArray deepCopy() { + JsonArray result = new JsonArray(); + for (JsonElement element : elements) { + result.add(element.deepCopy()); + } + return result; + } + + /** + * Adds the specified boolean to self. + * + * @param bool the boolean that needs to be added to the array. + */ + public void add(Boolean bool) { + elements.add(bool == null ? JsonNull.INSTANCE : new JsonPrimitive(bool)); + } + + /** + * Adds the specified character to self. + * + * @param character the character that needs to be added to the array. + */ + public void add(Character character) { + elements.add(character == null ? JsonNull.INSTANCE : new JsonPrimitive(character)); + } + + /** + * Adds the specified number to self. + * + * @param number the number that needs to be added to the array. + */ + public void add(Number number) { + elements.add(number == null ? JsonNull.INSTANCE : new JsonPrimitive(number)); + } + + /** + * Adds the specified string to self. + * + * @param string the string that needs to be added to the array. + */ + public void add(String string) { + elements.add(string == null ? JsonNull.INSTANCE : new JsonPrimitive(string)); + } + + /** + * Adds the specified element to self. + * + * @param element the element that needs to be added to the array. + */ + public void add(JsonElement element) { + if (element == null) { + element = JsonNull.INSTANCE; + } + elements.add(element); + } + + /** + * Adds all the elements of the specified array to self. + * + * @param array the array whose elements need to be added to the array. + */ + public void addAll(JsonArray array) { + elements.addAll(array.elements); + } + + /** + * Replaces the element at the specified position in this array with the specified element. + * Element can be null. + * @param index index of the element to replace + * @param element element to be stored at the specified position + * @return the element previously at the specified position + * @throws IndexOutOfBoundsException if the specified index is outside the array bounds + */ + public JsonElement set(int index, JsonElement element) { + return elements.set(index, element); + } + + /** + * Removes the first occurrence of the specified element from this array, if it is present. + * If the array does not contain the element, it is unchanged. + * @param element element to be removed from this array, if present + * @return true if this array contained the specified element, false otherwise + * @since 2.3 + */ + public boolean remove(JsonElement element) { + return elements.remove(element); + } + + /** + * Removes the element at the specified position in this array. Shifts any subsequent elements + * to the left (subtracts one from their indices). Returns the element that was removed from + * the array. + * @param index index the index of the element to be removed + * @return the element previously at the specified position + * @throws IndexOutOfBoundsException if the specified index is outside the array bounds + * @since 2.3 + */ + public JsonElement remove(int index) { + return elements.remove(index); + } + + /** + * Returns true if this array contains the specified element. + * @return true if this array contains the specified element. + * @param element whose presence in this array is to be tested + * @since 2.3 + */ + public boolean contains(JsonElement element) { + return elements.contains(element); + } + + /** + * Returns the number of elements in the array. + * + * @return the number of elements in the array. + */ + public int size() { + return elements.size(); + } + + /** + * Returns an iterator to navigate the elemetns of the array. Since the array is an ordered list, + * the iterator navigates the elements in the order they were inserted. + * + * @return an iterator to navigate the elements of the array. + */ + public Iterator<JsonElement> iterator() { + return elements.iterator(); + } + + /** + * Returns the ith element of the array. + * + * @param i the index of the element that is being sought. + * @return the element present at the ith index. + * @throws IndexOutOfBoundsException if i is negative or greater than or equal to the + * {@link #size()} of the array. + */ + public JsonElement get(int i) { + return elements.get(i); + } + + /** + * convenience method to get this array as a {@link Number} if it contains a single element. + * + * @return get this element as a number if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid Number. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public Number getAsNumber() { + if (elements.size() == 1) { + return elements.get(0).getAsNumber(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link String} if it contains a single element. + * + * @return get this element as a String if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid String. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public String getAsString() { + if (elements.size() == 1) { + return elements.get(0).getAsString(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a double if it contains a single element. + * + * @return get this element as a double if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid double. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public double getAsDouble() { + if (elements.size() == 1) { + return elements.get(0).getAsDouble(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link BigDecimal} if it contains a single element. + * + * @return get this element as a {@link BigDecimal} if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive}. + * @throws NumberFormatException if the element at index 0 is not a valid {@link BigDecimal}. + * @throws IllegalStateException if the array has more than one element. + * @since 1.2 + */ + @Override + public BigDecimal getAsBigDecimal() { + if (elements.size() == 1) { + return elements.get(0).getAsBigDecimal(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a {@link BigInteger} if it contains a single element. + * + * @return get this element as a {@link BigInteger} if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive}. + * @throws NumberFormatException if the element at index 0 is not a valid {@link BigInteger}. + * @throws IllegalStateException if the array has more than one element. + * @since 1.2 + */ + @Override + public BigInteger getAsBigInteger() { + if (elements.size() == 1) { + return elements.get(0).getAsBigInteger(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a float if it contains a single element. + * + * @return get this element as a float if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid float. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public float getAsFloat() { + if (elements.size() == 1) { + return elements.get(0).getAsFloat(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a long if it contains a single element. + * + * @return get this element as a long if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid long. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public long getAsLong() { + if (elements.size() == 1) { + return elements.get(0).getAsLong(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as an integer if it contains a single element. + * + * @return get this element as an integer if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid integer. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public int getAsInt() { + if (elements.size() == 1) { + return elements.get(0).getAsInt(); + } + throw new IllegalStateException(); + } + + @Override + public byte getAsByte() { + if (elements.size() == 1) { + return elements.get(0).getAsByte(); + } + throw new IllegalStateException(); + } + + @Override + public char getAsCharacter() { + if (elements.size() == 1) { + return elements.get(0).getAsCharacter(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a primitive short if it contains a single element. + * + * @return get this element as a primitive short if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid short. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public short getAsShort() { + if (elements.size() == 1) { + return elements.get(0).getAsShort(); + } + throw new IllegalStateException(); + } + + /** + * convenience method to get this array as a boolean if it contains a single element. + * + * @return get this element as a boolean if it is single element array. + * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and + * is not a valid boolean. + * @throws IllegalStateException if the array has more than one element. + */ + @Override + public boolean getAsBoolean() { + if (elements.size() == 1) { + return elements.get(0).getAsBoolean(); + } + throw new IllegalStateException(); + } + + @Override + public boolean equals(Object o) { + return (o == this) || (o instanceof JsonArray && ((JsonArray) o).elements.equals(elements)); + } + + @Override + public int hashCode() { + return elements.hashCode(); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonDeserializationContext.java b/gson/src/main/java/com/google/gson/JsonDeserializationContext.java new file mode 100644 index 00000000..00c75054 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonDeserializationContext.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; + +/** + * Context for deserialization that is passed to a custom deserializer during invocation of its + * {@link JsonDeserializer#deserialize(JsonElement, Type, JsonDeserializationContext)} + * method. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public interface JsonDeserializationContext { + + /** + * Invokes default deserialization on the specified object. It should never be invoked on + * the element received as a parameter of the + * {@link JsonDeserializer#deserialize(JsonElement, Type, JsonDeserializationContext)} method. Doing + * so will result in an infinite loop since Gson will in-turn call the custom deserializer again. + * + * @param json the parse tree. + * @param typeOfT type of the expected return value. + * @param <T> The type of the deserialized object. + * @return An object of type typeOfT. + * @throws JsonParseException if the parse tree does not contain expected data. + */ + public <T> T deserialize(JsonElement json, Type typeOfT) throws JsonParseException; +}
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/JsonDeserializer.java b/gson/src/main/java/com/google/gson/JsonDeserializer.java new file mode 100644 index 00000000..0589eb28 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonDeserializer.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; + +/** + * <p>Interface representing a custom deserializer for Json. You should write a custom + * deserializer, if you are not happy with the default deserialization done by Gson. You will + * also need to register this deserializer through + * {@link GsonBuilder#registerTypeAdapter(Type, Object)}.</p> + * + * <p>Let us look at example where defining a deserializer will be useful. The {@code Id} class + * defined below has two fields: {@code clazz} and {@code value}.</p> + * + * <pre> + * public class Id<T> { + * private final Class<T> clazz; + * private final long value; + * public Id(Class<T> clazz, long value) { + * this.clazz = clazz; + * this.value = value; + * } + * public long getValue() { + * return value; + * } + * } + * </pre> + * + * <p>The default deserialization of {@code Id(com.foo.MyObject.class, 20L)} will require the + * Json string to be <code>{"clazz":com.foo.MyObject,"value":20}</code>. Suppose, you already know + * the type of the field that the {@code Id} will be deserialized into, and hence just want to + * deserialize it from a Json string {@code 20}. You can achieve that by writing a custom + * deserializer:</p> + * + * <pre> + * class IdDeserializer implements JsonDeserializer<Id>() { + * public Id deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + * throws JsonParseException { + * return new Id((Class)typeOfT, id.getValue()); + * } + * </pre> + * + * <p>You will also need to register {@code IdDeserializer} with Gson as follows:</p> + * + * <pre> + * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdDeserializer()).create(); + * </pre> + * + * <p>New applications should prefer {@link TypeAdapter}, whose streaming API + * is more efficient than this interface's tree API. + * + * @author Inderjeet Singh + * @author Joel Leitch + * + * @param <T> type for which the deserializer is being registered. It is possible that a + * deserializer may be asked to deserialize a specific generic type of the T. + */ +public interface JsonDeserializer<T> { + + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + * <p>In the implementation of this call-back method, you should consider invoking + * {@link JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects + * for any non-trivial field of the returned object. However, you should never invoke it on the + * the same type passing {@code json} since that will cause an infinite loop (Gson will call your + * call-back method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * @throws JsonParseException if json is not in the expected format of {@code typeofT} + */ + public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException; +} diff --git a/gson/src/main/java/com/google/gson/JsonElement.java b/gson/src/main/java/com/google/gson/JsonElement.java new file mode 100644 index 00000000..d9cd9184 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonElement.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * A class representing an element of Json. It could either be a {@link JsonObject}, a + * {@link JsonArray}, a {@link JsonPrimitive} or a {@link JsonNull}. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public abstract class JsonElement { + /** + * Returns a deep copy of this element. Immutable elements like primitives + * and nulls are not copied. + */ + abstract JsonElement deepCopy(); + + /** + * provides check for verifying if this element is an array or not. + * + * @return true if this element is of type {@link JsonArray}, false otherwise. + */ + public boolean isJsonArray() { + return this instanceof JsonArray; + } + + /** + * provides check for verifying if this element is a Json object or not. + * + * @return true if this element is of type {@link JsonObject}, false otherwise. + */ + public boolean isJsonObject() { + return this instanceof JsonObject; + } + + /** + * provides check for verifying if this element is a primitive or not. + * + * @return true if this element is of type {@link JsonPrimitive}, false otherwise. + */ + public boolean isJsonPrimitive() { + return this instanceof JsonPrimitive; + } + + /** + * provides check for verifying if this element represents a null value or not. + * + * @return true if this element is of type {@link JsonNull}, false otherwise. + * @since 1.2 + */ + public boolean isJsonNull() { + return this instanceof JsonNull; + } + + /** + * convenience method to get this element as a {@link JsonObject}. If the element is of some + * other type, a {@link IllegalStateException} will result. Hence it is best to use this method + * after ensuring that this element is of the desired type by calling {@link #isJsonObject()} + * first. + * + * @return get this element as a {@link JsonObject}. + * @throws IllegalStateException if the element is of another type. + */ + public JsonObject getAsJsonObject() { + if (isJsonObject()) { + return (JsonObject) this; + } + throw new IllegalStateException("Not a JSON Object: " + this); + } + + /** + * convenience method to get this element as a {@link JsonArray}. If the element is of some + * other type, a {@link IllegalStateException} will result. Hence it is best to use this method + * after ensuring that this element is of the desired type by calling {@link #isJsonArray()} + * first. + * + * @return get this element as a {@link JsonArray}. + * @throws IllegalStateException if the element is of another type. + */ + public JsonArray getAsJsonArray() { + if (isJsonArray()) { + return (JsonArray) this; + } + throw new IllegalStateException("This is not a JSON Array."); + } + + /** + * convenience method to get this element as a {@link JsonPrimitive}. If the element is of some + * other type, a {@link IllegalStateException} will result. Hence it is best to use this method + * after ensuring that this element is of the desired type by calling {@link #isJsonPrimitive()} + * first. + * + * @return get this element as a {@link JsonPrimitive}. + * @throws IllegalStateException if the element is of another type. + */ + public JsonPrimitive getAsJsonPrimitive() { + if (isJsonPrimitive()) { + return (JsonPrimitive) this; + } + throw new IllegalStateException("This is not a JSON Primitive."); + } + + /** + * convenience method to get this element as a {@link JsonNull}. If the element is of some + * other type, a {@link IllegalStateException} will result. Hence it is best to use this method + * after ensuring that this element is of the desired type by calling {@link #isJsonNull()} + * first. + * + * @return get this element as a {@link JsonNull}. + * @throws IllegalStateException if the element is of another type. + * @since 1.2 + */ + public JsonNull getAsJsonNull() { + if (isJsonNull()) { + return (JsonNull) this; + } + throw new IllegalStateException("This is not a JSON Null."); + } + + /** + * convenience method to get this element as a boolean value. + * + * @return get this element as a primitive boolean value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * boolean value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public boolean getAsBoolean() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a {@link Boolean} value. + * + * @return get this element as a {@link Boolean} value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * boolean value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + Boolean getAsBooleanWrapper() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a {@link Number}. + * + * @return get this element as a {@link Number}. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * number. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public Number getAsNumber() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a string value. + * + * @return get this element as a string value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * string value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public String getAsString() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive double value. + * + * @return get this element as a primitive double value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * double value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public double getAsDouble() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive float value. + * + * @return get this element as a primitive float value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * float value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public float getAsFloat() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive long value. + * + * @return get this element as a primitive long value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * long value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public long getAsLong() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive integer value. + * + * @return get this element as a primitive integer value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * integer value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public int getAsInt() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive byte value. + * + * @return get this element as a primitive byte value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * byte value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + * @since 1.3 + */ + public byte getAsByte() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive character value. + * + * @return get this element as a primitive char value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * char value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + * @since 1.3 + */ + public char getAsCharacter() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a {@link BigDecimal}. + * + * @return get this element as a {@link BigDecimal}. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive}. + * * @throws NumberFormatException if the element is not a valid {@link BigDecimal}. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + * @since 1.2 + */ + public BigDecimal getAsBigDecimal() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a {@link BigInteger}. + * + * @return get this element as a {@link BigInteger}. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive}. + * @throws NumberFormatException if the element is not a valid {@link BigInteger}. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + * @since 1.2 + */ + public BigInteger getAsBigInteger() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * convenience method to get this element as a primitive short value. + * + * @return get this element as a primitive short value. + * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid + * short value. + * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains + * more than a single element. + */ + public short getAsShort() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /** + * Returns a String representation of this element. + */ + @Override + public String toString() { + try { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setLenient(true); + Streams.write(this, jsonWriter); + return stringWriter.toString(); + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/gson/src/main/java/com/google/gson/JsonIOException.java b/gson/src/main/java/com/google/gson/JsonIOException.java new file mode 100644 index 00000000..dfeccd8e --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonIOException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +/** + * This exception is raised when Gson was unable to read an input stream + * or write to one. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public final class JsonIOException extends JsonParseException { + private static final long serialVersionUID = 1L; + + public JsonIOException(String msg) { + super(msg); + } + + public JsonIOException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Creates exception with the specified cause. Consider using + * {@link #JsonIOException(String, Throwable)} instead if you can describe what happened. + * + * @param cause root exception that caused this exception to be thrown. + */ + public JsonIOException(Throwable cause) { + super(cause); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonNull.java b/gson/src/main/java/com/google/gson/JsonNull.java new file mode 100755 index 00000000..56883369 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonNull.java @@ -0,0 +1,63 @@ +/*
+ * Copyright (C) 2008 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 com.google.gson;
+
+/**
+ * A class representing a Json {@code null} value.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.2
+ */
+public final class JsonNull extends JsonElement {
+ /**
+ * singleton for JsonNull
+ *
+ * @since 1.8
+ */
+ public static final JsonNull INSTANCE = new JsonNull();
+
+ /**
+ * Creates a new JsonNull object.
+ * Deprecated since Gson version 1.8. Use {@link #INSTANCE} instead
+ */
+ @Deprecated
+ public JsonNull() {
+ // Do nothing
+ }
+
+ @Override
+ JsonNull deepCopy() {
+ return INSTANCE;
+ }
+
+ /**
+ * All instances of JsonNull have the same hash code since they are indistinguishable
+ */
+ @Override
+ public int hashCode() {
+ return JsonNull.class.hashCode();
+ }
+
+ /**
+ * All instances of JsonNull are the same
+ */
+ @Override
+ public boolean equals(Object other) {
+ return this == other || other instanceof JsonNull;
+ }
+}
diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java new file mode 100644 index 00000000..78c7a177 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import com.google.gson.internal.LinkedTreeMap; + +import java.util.Map; +import java.util.Set; + +/** + * A class representing an object type in Json. An object consists of name-value pairs where names + * are strings, and values are any other type of {@link JsonElement}. This allows for a creating a + * tree of JsonElements. The member elements of this object are maintained in order they were added. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public final class JsonObject extends JsonElement { + private final LinkedTreeMap<String, JsonElement> members = + new LinkedTreeMap<String, JsonElement>(); + + @Override + JsonObject deepCopy() { + JsonObject result = new JsonObject(); + for (Map.Entry<String, JsonElement> entry : members.entrySet()) { + result.add(entry.getKey(), entry.getValue().deepCopy()); + } + return result; + } + + /** + * Adds a member, which is a name-value pair, to self. The name must be a String, but the value + * can be an arbitrary JsonElement, thereby allowing you to build a full tree of JsonElements + * rooted at this node. + * + * @param property name of the member. + * @param value the member object. + */ + public void add(String property, JsonElement value) { + if (value == null) { + value = JsonNull.INSTANCE; + } + members.put(property, value); + } + + /** + * Removes the {@code property} from this {@link JsonObject}. + * + * @param property name of the member that should be removed. + * @return the {@link JsonElement} object that is being removed. + * @since 1.3 + */ + public JsonElement remove(String property) { + return members.remove(property); + } + + /** + * Convenience method to add a primitive member. The specified value is converted to a + * JsonPrimitive of String. + * + * @param property name of the member. + * @param value the string value associated with the member. + */ + public void addProperty(String property, String value) { + add(property, createJsonElement(value)); + } + + /** + * Convenience method to add a primitive member. The specified value is converted to a + * JsonPrimitive of Number. + * + * @param property name of the member. + * @param value the number value associated with the member. + */ + public void addProperty(String property, Number value) { + add(property, createJsonElement(value)); + } + + /** + * Convenience method to add a boolean member. The specified value is converted to a + * JsonPrimitive of Boolean. + * + * @param property name of the member. + * @param value the number value associated with the member. + */ + public void addProperty(String property, Boolean value) { + add(property, createJsonElement(value)); + } + + /** + * Convenience method to add a char member. The specified value is converted to a + * JsonPrimitive of Character. + * + * @param property name of the member. + * @param value the number value associated with the member. + */ + public void addProperty(String property, Character value) { + add(property, createJsonElement(value)); + } + + /** + * Creates the proper {@link JsonElement} object from the given {@code value} object. + * + * @param value the object to generate the {@link JsonElement} for + * @return a {@link JsonPrimitive} if the {@code value} is not null, otherwise a {@link JsonNull} + */ + private JsonElement createJsonElement(Object value) { + return value == null ? JsonNull.INSTANCE : new JsonPrimitive(value); + } + + /** + * Returns a set of members of this object. The set is ordered, and the order is in which the + * elements were added. + * + * @return a set of members of this object. + */ + public Set<Map.Entry<String, JsonElement>> entrySet() { + return members.entrySet(); + } + + /** + * Convenience method to check if a member with the specified name is present in this object. + * + * @param memberName name of the member that is being checked for presence. + * @return true if there is a member with the specified name, false otherwise. + */ + public boolean has(String memberName) { + return members.containsKey(memberName); + } + + /** + * Returns the member with the specified name. + * + * @param memberName name of the member that is being requested. + * @return the member matching the name. Null if no such member exists. + */ + public JsonElement get(String memberName) { + return members.get(memberName); + } + + /** + * Convenience method to get the specified member as a JsonPrimitive element. + * + * @param memberName name of the member being requested. + * @return the JsonPrimitive corresponding to the specified member. + */ + public JsonPrimitive getAsJsonPrimitive(String memberName) { + return (JsonPrimitive) members.get(memberName); + } + + /** + * Convenience method to get the specified member as a JsonArray. + * + * @param memberName name of the member being requested. + * @return the JsonArray corresponding to the specified member. + */ + public JsonArray getAsJsonArray(String memberName) { + return (JsonArray) members.get(memberName); + } + + /** + * Convenience method to get the specified member as a JsonObject. + * + * @param memberName name of the member being requested. + * @return the JsonObject corresponding to the specified member. + */ + public JsonObject getAsJsonObject(String memberName) { + return (JsonObject) members.get(memberName); + } + + @Override + public boolean equals(Object o) { + return (o == this) || (o instanceof JsonObject + && ((JsonObject) o).members.equals(members)); + } + + @Override + public int hashCode() { + return members.hashCode(); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonParseException.java b/gson/src/main/java/com/google/gson/JsonParseException.java new file mode 100644 index 00000000..084f6612 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonParseException.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +/** + * This exception is raised if there is a serious issue that occurs during parsing of a Json + * string. One of the main usages for this class is for the Gson infrastructure. If the incoming + * Json is bad/malicious, an instance of this exception is raised. + * + * <p>This exception is a {@link RuntimeException} because it is exposed to the client. Using a + * {@link RuntimeException} avoids bad coding practices on the client side where they catch the + * exception and do nothing. It is often the case that you want to blow up if there is a parsing + * error (i.e. often clients do not know how to recover from a {@link JsonParseException}.</p> + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class JsonParseException extends RuntimeException { + static final long serialVersionUID = -4086729973971783390L; + + /** + * Creates exception with the specified message. If you are wrapping another exception, consider + * using {@link #JsonParseException(String, Throwable)} instead. + * + * @param msg error message describing a possible cause of this exception. + */ + public JsonParseException(String msg) { + super(msg); + } + + /** + * Creates exception with the specified message and cause. + * + * @param msg error message describing what happened. + * @param cause root exception that caused this exception to be thrown. + */ + public JsonParseException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Creates exception with the specified cause. Consider using + * {@link #JsonParseException(String, Throwable)} instead if you can describe what happened. + * + * @param cause root exception that caused this exception to be thrown. + */ + public JsonParseException(Throwable cause) { + super(cause); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java new file mode 100755 index 00000000..a8ae337b --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -0,0 +1,93 @@ +/*
+ * Copyright (C) 2009 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 com.google.gson;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
+
+/**
+ * A parser to parse Json into a parse tree of {@link JsonElement}s
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.3
+ */
+public final class JsonParser {
+
+ /**
+ * Parses the specified JSON string into a parse tree
+ *
+ * @param json JSON text
+ * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
+ * @throws JsonParseException if the specified text is not valid JSON
+ * @since 1.3
+ */
+ public JsonElement parse(String json) throws JsonSyntaxException {
+ return parse(new StringReader(json));
+ }
+
+ /**
+ * Parses the specified JSON string into a parse tree
+ *
+ * @param json JSON text
+ * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
+ * @throws JsonParseException if the specified text is not valid JSON
+ * @since 1.3
+ */
+ public JsonElement parse(Reader json) throws JsonIOException, JsonSyntaxException {
+ try {
+ JsonReader jsonReader = new JsonReader(json);
+ JsonElement element = parse(jsonReader);
+ if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) {
+ throw new JsonSyntaxException("Did not consume the entire document.");
+ }
+ return element;
+ } catch (MalformedJsonException e) {
+ throw new JsonSyntaxException(e);
+ } catch (IOException e) {
+ throw new JsonIOException(e);
+ } catch (NumberFormatException e) {
+ throw new JsonSyntaxException(e);
+ }
+ }
+
+ /**
+ * Returns the next value from the JSON stream as a parse tree.
+ *
+ * @throws JsonParseException if there is an IOException or if the specified
+ * text is not valid JSON
+ * @since 1.6
+ */
+ public JsonElement parse(JsonReader json) throws JsonIOException, JsonSyntaxException {
+ boolean lenient = json.isLenient();
+ json.setLenient(true);
+ try {
+ return Streams.parse(json);
+ } catch (StackOverflowError e) {
+ throw new JsonParseException("Failed parsing JSON source: " + json + " to Json", e);
+ } catch (OutOfMemoryError e) {
+ throw new JsonParseException("Failed parsing JSON source: " + json + " to Json", e);
+ } finally {
+ json.setLenient(lenient);
+ }
+ }
+}
diff --git a/gson/src/main/java/com/google/gson/JsonPrimitive.java b/gson/src/main/java/com/google/gson/JsonPrimitive.java new file mode 100644 index 00000000..e2443d43 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonPrimitive.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import com.google.gson.internal.$Gson$Preconditions; +import com.google.gson.internal.LazilyParsedNumber; + +/** + * A class representing a Json primitive value. A primitive value + * is either a String, a Java primitive, or a Java primitive + * wrapper type. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public final class JsonPrimitive extends JsonElement { + + private static final Class<?>[] PRIMITIVE_TYPES = { int.class, long.class, short.class, + float.class, double.class, byte.class, boolean.class, char.class, Integer.class, Long.class, + Short.class, Float.class, Double.class, Byte.class, Boolean.class, Character.class }; + + private Object value; + + /** + * Create a primitive containing a boolean value. + * + * @param bool the value to create the primitive with. + */ + public JsonPrimitive(Boolean bool) { + setValue(bool); + } + + /** + * Create a primitive containing a {@link Number}. + * + * @param number the value to create the primitive with. + */ + public JsonPrimitive(Number number) { + setValue(number); + } + + /** + * Create a primitive containing a String value. + * + * @param string the value to create the primitive with. + */ + public JsonPrimitive(String string) { + setValue(string); + } + + /** + * Create a primitive containing a character. The character is turned into a one character String + * since Json only supports String. + * + * @param c the value to create the primitive with. + */ + public JsonPrimitive(Character c) { + setValue(c); + } + + /** + * Create a primitive using the specified Object. It must be an instance of {@link Number}, a + * Java primitive type, or a String. + * + * @param primitive the value to create the primitive with. + */ + JsonPrimitive(Object primitive) { + setValue(primitive); + } + + @Override + JsonPrimitive deepCopy() { + return this; + } + + void setValue(Object primitive) { + if (primitive instanceof Character) { + // convert characters to strings since in JSON, characters are represented as a single + // character string + char c = ((Character) primitive).charValue(); + this.value = String.valueOf(c); + } else { + $Gson$Preconditions.checkArgument(primitive instanceof Number + || isPrimitiveOrString(primitive)); + this.value = primitive; + } + } + + /** + * Check whether this primitive contains a boolean value. + * + * @return true if this primitive contains a boolean value, false otherwise. + */ + public boolean isBoolean() { + return value instanceof Boolean; + } + + /** + * convenience method to get this element as a {@link Boolean}. + * + * @return get this element as a {@link Boolean}. + */ + @Override + Boolean getAsBooleanWrapper() { + return (Boolean) value; + } + + /** + * convenience method to get this element as a boolean value. + * + * @return get this element as a primitive boolean value. + */ + @Override + public boolean getAsBoolean() { + if (isBoolean()) { + return getAsBooleanWrapper().booleanValue(); + } else { + // Check to see if the value as a String is "true" in any case. + return Boolean.parseBoolean(getAsString()); + } + } + + /** + * Check whether this primitive contains a Number. + * + * @return true if this primitive contains a Number, false otherwise. + */ + public boolean isNumber() { + return value instanceof Number; + } + + /** + * convenience method to get this element as a Number. + * + * @return get this element as a Number. + * @throws NumberFormatException if the value contained is not a valid Number. + */ + @Override + public Number getAsNumber() { + return value instanceof String ? new LazilyParsedNumber((String) value) : (Number) value; + } + + /** + * Check whether this primitive contains a String value. + * + * @return true if this primitive contains a String value, false otherwise. + */ + public boolean isString() { + return value instanceof String; + } + + /** + * convenience method to get this element as a String. + * + * @return get this element as a String. + */ + @Override + public String getAsString() { + if (isNumber()) { + return getAsNumber().toString(); + } else if (isBoolean()) { + return getAsBooleanWrapper().toString(); + } else { + return (String) value; + } + } + + /** + * convenience method to get this element as a primitive double. + * + * @return get this element as a primitive double. + * @throws NumberFormatException if the value contained is not a valid double. + */ + @Override + public double getAsDouble() { + return isNumber() ? getAsNumber().doubleValue() : Double.parseDouble(getAsString()); + } + + /** + * convenience method to get this element as a {@link BigDecimal}. + * + * @return get this element as a {@link BigDecimal}. + * @throws NumberFormatException if the value contained is not a valid {@link BigDecimal}. + */ + @Override + public BigDecimal getAsBigDecimal() { + return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString()); + } + + /** + * convenience method to get this element as a {@link BigInteger}. + * + * @return get this element as a {@link BigInteger}. + * @throws NumberFormatException if the value contained is not a valid {@link BigInteger}. + */ + @Override + public BigInteger getAsBigInteger() { + return value instanceof BigInteger ? + (BigInteger) value : new BigInteger(value.toString()); + } + + /** + * convenience method to get this element as a float. + * + * @return get this element as a float. + * @throws NumberFormatException if the value contained is not a valid float. + */ + @Override + public float getAsFloat() { + return isNumber() ? getAsNumber().floatValue() : Float.parseFloat(getAsString()); + } + + /** + * convenience method to get this element as a primitive long. + * + * @return get this element as a primitive long. + * @throws NumberFormatException if the value contained is not a valid long. + */ + @Override + public long getAsLong() { + return isNumber() ? getAsNumber().longValue() : Long.parseLong(getAsString()); + } + + /** + * convenience method to get this element as a primitive short. + * + * @return get this element as a primitive short. + * @throws NumberFormatException if the value contained is not a valid short value. + */ + @Override + public short getAsShort() { + return isNumber() ? getAsNumber().shortValue() : Short.parseShort(getAsString()); + } + + /** + * convenience method to get this element as a primitive integer. + * + * @return get this element as a primitive integer. + * @throws NumberFormatException if the value contained is not a valid integer. + */ + @Override + public int getAsInt() { + return isNumber() ? getAsNumber().intValue() : Integer.parseInt(getAsString()); + } + + @Override + public byte getAsByte() { + return isNumber() ? getAsNumber().byteValue() : Byte.parseByte(getAsString()); + } + + @Override + public char getAsCharacter() { + return getAsString().charAt(0); + } + + private static boolean isPrimitiveOrString(Object target) { + if (target instanceof String) { + return true; + } + + Class<?> classOfPrimitive = target.getClass(); + for (Class<?> standardPrimitive : PRIMITIVE_TYPES) { + if (standardPrimitive.isAssignableFrom(classOfPrimitive)) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + if (value == null) { + return 31; + } + // Using recommended hashing algorithm from Effective Java for longs and doubles + if (isIntegral(this)) { + long value = getAsNumber().longValue(); + return (int) (value ^ (value >>> 32)); + } + if (value instanceof Number) { + long value = Double.doubleToLongBits(getAsNumber().doubleValue()); + return (int) (value ^ (value >>> 32)); + } + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + JsonPrimitive other = (JsonPrimitive)obj; + if (value == null) { + return other.value == null; + } + if (isIntegral(this) && isIntegral(other)) { + return getAsNumber().longValue() == other.getAsNumber().longValue(); + } + if (value instanceof Number && other.value instanceof Number) { + double a = getAsNumber().doubleValue(); + // Java standard types other than double return true for two NaN. So, need + // special handling for double. + double b = other.getAsNumber().doubleValue(); + return a == b || (Double.isNaN(a) && Double.isNaN(b)); + } + return value.equals(other.value); + } + + /** + * Returns true if the specified number is an integral type + * (Long, Integer, Short, Byte, BigInteger) + */ + private static boolean isIntegral(JsonPrimitive primitive) { + if (primitive.value instanceof Number) { + Number number = (Number) primitive.value; + return number instanceof BigInteger || number instanceof Long || number instanceof Integer + || number instanceof Short || number instanceof Byte; + } + return false; + } +} diff --git a/gson/src/main/java/com/google/gson/JsonSerializationContext.java b/gson/src/main/java/com/google/gson/JsonSerializationContext.java new file mode 100644 index 00000000..ca3ec4f9 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonSerializationContext.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; + +/** + * Context for serialization that is passed to a custom serializer during invocation of its + * {@link JsonSerializer#serialize(Object, Type, JsonSerializationContext)} method. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public interface JsonSerializationContext { + + /** + * Invokes default serialization on the specified object. + * + * @param src the object that needs to be serialized. + * @return a tree of {@link JsonElement}s corresponding to the serialized form of {@code src}. + */ + public JsonElement serialize(Object src); + + /** + * Invokes default serialization on the specified object passing the specific type information. + * It should never be invoked on the element received as a parameter of the + * {@link JsonSerializer#serialize(Object, Type, JsonSerializationContext)} method. Doing + * so will result in an infinite loop since Gson will in-turn call the custom serializer again. + * + * @param src the object that needs to be serialized. + * @param typeOfSrc the actual genericized type of src object. + * @return a tree of {@link JsonElement}s corresponding to the serialized form of {@code src}. + */ + public JsonElement serialize(Object src, Type typeOfSrc); +} diff --git a/gson/src/main/java/com/google/gson/JsonSerializer.java b/gson/src/main/java/com/google/gson/JsonSerializer.java new file mode 100644 index 00000000..a6050033 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonSerializer.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 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 com.google.gson; + +import java.lang.reflect.Type; + +/** + * Interface representing a custom serializer for Json. You should write a custom serializer, if + * you are not happy with the default serialization done by Gson. You will also need to register + * this serializer through {@link com.google.gson.GsonBuilder#registerTypeAdapter(Type, Object)}. + * + * <p>Let us look at example where defining a serializer will be useful. The {@code Id} class + * defined below has two fields: {@code clazz} and {@code value}.</p> + * + * <p><pre> + * public class Id<T> { + * private final Class<T> clazz; + * private final long value; + * + * public Id(Class<T> clazz, long value) { + * this.clazz = clazz; + * this.value = value; + * } + * + * public long getValue() { + * return value; + * } + * } + * </pre></p> + * + * <p>The default serialization of {@code Id(com.foo.MyObject.class, 20L)} will be + * <code>{"clazz":com.foo.MyObject,"value":20}</code>. Suppose, you just want the output to be + * the value instead, which is {@code 20} in this case. You can achieve that by writing a custom + * serializer:</p> + * + * <p><pre> + * class IdSerializer implements JsonSerializer<Id>() { + * public JsonElement serialize(Id id, Type typeOfId, JsonSerializationContext context) { + * return new JsonPrimitive(id.getValue()); + * } + * } + * </pre></p> + * + * <p>You will also need to register {@code IdSerializer} with Gson as follows:</p> + * <pre> + * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdSerializer()).create(); + * </pre> + * + * <p>New applications should prefer {@link TypeAdapter}, whose streaming API + * is more efficient than this interface's tree API. + * + * @author Inderjeet Singh + * @author Joel Leitch + * + * @param <T> type for which the serializer is being registered. It is possible that a serializer + * may be asked to serialize a specific generic type of the T. + */ +public interface JsonSerializer<T> { + + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + * + * <p>In the implementation of this call-back method, you should consider invoking + * {@link JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code src} object. However, you should never invoke it on the + * {@code src} object itself since that will cause an infinite loop (Gson will call your + * call-back method again).</p> + * + * @param src the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @return a JsonElement corresponding to the specified object. + */ + public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context); +} diff --git a/gson/src/main/java/com/google/gson/JsonStreamParser.java b/gson/src/main/java/com/google/gson/JsonStreamParser.java new file mode 100644 index 00000000..f0438db3 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonStreamParser.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2009 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 com.google.gson; + +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.MalformedJsonException; + +/** + * A streaming parser that allows reading of multiple {@link JsonElement}s from the specified reader + * asynchronously. + * + * <p>This class is conditionally thread-safe (see Item 70, Effective Java second edition). To + * properly use this class across multiple threads, you will need to add some external + * synchronization. For example: + * + * <pre> + * JsonStreamParser parser = new JsonStreamParser("['first'] {'second':10} 'third'"); + * JsonElement element; + * synchronized (parser) { // synchronize on an object shared by threads + * if (parser.hasNext()) { + * element = parser.next(); + * } + * } + * </pre> + * + * @author Inderjeet Singh + * @author Joel Leitch + * @since 1.4 + */ +public final class JsonStreamParser implements Iterator<JsonElement> { + private final JsonReader parser; + private final Object lock; + + /** + * @param json The string containing JSON elements concatenated to each other. + * @since 1.4 + */ + public JsonStreamParser(String json) { + this(new StringReader(json)); + } + + /** + * @param reader The data stream containing JSON elements concatenated to each other. + * @since 1.4 + */ + public JsonStreamParser(Reader reader) { + parser = new JsonReader(reader); + parser.setLenient(true); + lock = new Object(); + } + + /** + * Returns the next available {@link JsonElement} on the reader. Null if none available. + * + * @return the next available {@link JsonElement} on the reader. Null if none available. + * @throws JsonParseException if the incoming stream is malformed JSON. + * @since 1.4 + */ + public JsonElement next() throws JsonParseException { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + try { + return Streams.parse(parser); + } catch (StackOverflowError e) { + throw new JsonParseException("Failed parsing JSON source to Json", e); + } catch (OutOfMemoryError e) { + throw new JsonParseException("Failed parsing JSON source to Json", e); + } catch (JsonParseException e) { + throw e.getCause() instanceof EOFException ? new NoSuchElementException() : e; + } + } + + /** + * Returns true if a {@link JsonElement} is available on the input for consumption + * @return true if a {@link JsonElement} is available on the input, false otherwise + * @since 1.4 + */ + public boolean hasNext() { + synchronized (lock) { + try { + return parser.peek() != JsonToken.END_DOCUMENT; + } catch (MalformedJsonException e) { + throw new JsonSyntaxException(e); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + } + + /** + * This optional {@link Iterator} method is not relevant for stream parsing and hence is not + * implemented. + * @since 1.4 + */ + public void remove() { + throw new UnsupportedOperationException(); + } +} diff --git a/gson/src/main/java/com/google/gson/JsonSyntaxException.java b/gson/src/main/java/com/google/gson/JsonSyntaxException.java new file mode 100644 index 00000000..17c1d3d3 --- /dev/null +++ b/gson/src/main/java/com/google/gson/JsonSyntaxException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 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 com.google.gson; + +/** + * This exception is raised when Gson attempts to read (or write) a malformed + * JSON element. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public final class JsonSyntaxException extends JsonParseException { + + private static final long serialVersionUID = 1L; + + public JsonSyntaxException(String msg) { + super(msg); + } + + public JsonSyntaxException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Creates exception with the specified cause. Consider using + * {@link #JsonSyntaxException(String, Throwable)} instead if you can + * describe what actually happened. + * + * @param cause root exception that caused this exception to be thrown. + */ + public JsonSyntaxException(Throwable cause) { + super(cause); + } +} diff --git a/gson/src/main/java/com/google/gson/LongSerializationPolicy.java b/gson/src/main/java/com/google/gson/LongSerializationPolicy.java new file mode 100644 index 00000000..3d9a2da1 --- /dev/null +++ b/gson/src/main/java/com/google/gson/LongSerializationPolicy.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009 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 com.google.gson; + +/** + * Defines the expected format for a {@code long} or {@code Long} type when its serialized. + * + * @since 1.3 + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public enum LongSerializationPolicy { + /** + * This is the "default" serialization policy that will output a {@code long} object as a JSON + * number. For example, assume an object has a long field named "f" then the serialized output + * would be: + * {@code {"f":123}}. + */ + DEFAULT() { + public JsonElement serialize(Long value) { + return new JsonPrimitive(value); + } + }, + + /** + * Serializes a long value as a quoted string. For example, assume an object has a long field + * named "f" then the serialized output would be: + * {@code {"f":"123"}}. + */ + STRING() { + public JsonElement serialize(Long value) { + return new JsonPrimitive(String.valueOf(value)); + } + }; + + /** + * Serialize this {@code value} using this serialization policy. + * + * @param value the long value to be serialized into a {@link JsonElement} + * @return the serialized version of {@code value} + */ + public abstract JsonElement serialize(Long value); +} diff --git a/gson/src/main/java/com/google/gson/TreeTypeAdapter.java b/gson/src/main/java/com/google/gson/TreeTypeAdapter.java new file mode 100644 index 00000000..a05c1fe0 --- /dev/null +++ b/gson/src/main/java/com/google/gson/TreeTypeAdapter.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2011 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 com.google.gson; + +import com.google.gson.internal.$Gson$Preconditions; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +/** + * Adapts a Gson 1.x tree-style adapter as a streaming TypeAdapter. Since the + * tree adapter may be serialization-only or deserialization-only, this class + * has a facility to lookup a delegate type adapter on demand. + */ +final class TreeTypeAdapter<T> extends TypeAdapter<T> { + private final JsonSerializer<T> serializer; + private final JsonDeserializer<T> deserializer; + private final Gson gson; + private final TypeToken<T> typeToken; + private final TypeAdapterFactory skipPast; + + /** The delegate is lazily created because it may not be needed, and creating it may fail. */ + private TypeAdapter<T> delegate; + + private TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer, + Gson gson, TypeToken<T> typeToken, TypeAdapterFactory skipPast) { + this.serializer = serializer; + this.deserializer = deserializer; + this.gson = gson; + this.typeToken = typeToken; + this.skipPast = skipPast; + } + + @Override public T read(JsonReader in) throws IOException { + if (deserializer == null) { + return delegate().read(in); + } + JsonElement value = Streams.parse(in); + if (value.isJsonNull()) { + return null; + } + return deserializer.deserialize(value, typeToken.getType(), gson.deserializationContext); + } + + @Override public void write(JsonWriter out, T value) throws IOException { + if (serializer == null) { + delegate().write(out, value); + return; + } + if (value == null) { + out.nullValue(); + return; + } + JsonElement tree = serializer.serialize(value, typeToken.getType(), gson.serializationContext); + Streams.write(tree, out); + } + + private TypeAdapter<T> delegate() { + TypeAdapter<T> d = delegate; + return d != null + ? d + : (delegate = gson.getDelegateAdapter(skipPast, typeToken)); + } + + /** + * Returns a new factory that will match each type against {@code exactType}. + */ + public static TypeAdapterFactory newFactory(TypeToken<?> exactType, Object typeAdapter) { + return new SingleTypeFactory(typeAdapter, exactType, false, null); + } + + /** + * Returns a new factory that will match each type and its raw type against + * {@code exactType}. + */ + public static TypeAdapterFactory newFactoryWithMatchRawType( + TypeToken<?> exactType, Object typeAdapter) { + // only bother matching raw types if exact type is a raw type + boolean matchRawType = exactType.getType() == exactType.getRawType(); + return new SingleTypeFactory(typeAdapter, exactType, matchRawType, null); + } + + /** + * Returns a new factory that will match each type's raw type for assignability + * to {@code hierarchyType}. + */ + public static TypeAdapterFactory newTypeHierarchyFactory( + Class<?> hierarchyType, Object typeAdapter) { + return new SingleTypeFactory(typeAdapter, null, false, hierarchyType); + } + + private static class SingleTypeFactory implements TypeAdapterFactory { + private final TypeToken<?> exactType; + private final boolean matchRawType; + private final Class<?> hierarchyType; + private final JsonSerializer<?> serializer; + private final JsonDeserializer<?> deserializer; + + private SingleTypeFactory(Object typeAdapter, TypeToken<?> exactType, boolean matchRawType, + Class<?> hierarchyType) { + serializer = typeAdapter instanceof JsonSerializer + ? (JsonSerializer<?>) typeAdapter + : null; + deserializer = typeAdapter instanceof JsonDeserializer + ? (JsonDeserializer<?>) typeAdapter + : null; + $Gson$Preconditions.checkArgument(serializer != null || deserializer != null); + this.exactType = exactType; + this.matchRawType = matchRawType; + this.hierarchyType = hierarchyType; + } + + @SuppressWarnings("unchecked") // guarded by typeToken.equals() call + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + boolean matches = exactType != null + ? exactType.equals(type) || matchRawType && exactType.getType() == type.getRawType() + : hierarchyType.isAssignableFrom(type.getRawType()); + return matches + ? new TreeTypeAdapter<T>((JsonSerializer<T>) serializer, + (JsonDeserializer<T>) deserializer, gson, type, this) + : null; + } + } +} diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java new file mode 100644 index 00000000..4646d271 --- /dev/null +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2011 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 com.google.gson; + +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * Converts Java objects to and from JSON. + * + * <h3>Defining a type's JSON form</h3> + * By default Gson converts application classes to JSON using its built-in type + * adapters. If Gson's default JSON conversion isn't appropriate for a type, + * extend this class to customize the conversion. Here's an example of a type + * adapter for an (X,Y) coordinate point: <pre> {@code + * + * public class PointAdapter extends TypeAdapter<Point> { + * public Point read(JsonReader reader) throws IOException { + * if (reader.peek() == JsonToken.NULL) { + * reader.nextNull(); + * return null; + * } + * String xy = reader.nextString(); + * String[] parts = xy.split(","); + * int x = Integer.parseInt(parts[0]); + * int y = Integer.parseInt(parts[1]); + * return new Point(x, y); + * } + * public void write(JsonWriter writer, Point value) throws IOException { + * if (value == null) { + * writer.nullValue(); + * return; + * } + * String xy = value.getX() + "," + value.getY(); + * writer.value(xy); + * } + * }}</pre> + * With this type adapter installed, Gson will convert {@code Points} to JSON as + * strings like {@code "5,8"} rather than objects like {@code {"x":5,"y":8}}. In + * this case the type adapter binds a rich Java class to a compact JSON value. + * + * <p>The {@link #read(JsonReader) read()} method must read exactly one value + * and {@link #write(JsonWriter,Object) write()} must write exactly one value. + * For primitive types this is means readers should make exactly one call to + * {@code nextBoolean()}, {@code nextDouble()}, {@code nextInt()}, {@code + * nextLong()}, {@code nextString()} or {@code nextNull()}. Writers should make + * exactly one call to one of <code>value()</code> or <code>nullValue()</code>. + * For arrays, type adapters should start with a call to {@code beginArray()}, + * convert all elements, and finish with a call to {@code endArray()}. For + * objects, they should start with {@code beginObject()}, convert the object, + * and finish with {@code endObject()}. Failing to convert a value or converting + * too many values may cause the application to crash. + * + * <p>Type adapters should be prepared to read null from the stream and write it + * to the stream. Alternatively, they should use {@link #nullSafe()} method while + * registering the type adapter with Gson. If your {@code Gson} instance + * has been configured to {@link GsonBuilder#serializeNulls()}, these nulls will be + * written to the final document. Otherwise the value (and the corresponding name + * when writing to a JSON object) will be omitted automatically. In either case + * your type adapter must handle null. + * + * <p>To use a custom type adapter with Gson, you must <i>register</i> it with a + * {@link GsonBuilder}: <pre> {@code + * + * GsonBuilder builder = new GsonBuilder(); + * builder.registerTypeAdapter(Point.class, new PointAdapter()); + * // if PointAdapter didn't check for nulls in its read/write methods, you should instead use + * // builder.registerTypeAdapter(Point.class, new PointAdapter().nullSafe()); + * ... + * Gson gson = builder.create(); + * }</pre> + * + * @since 2.1 + */ +// non-Javadoc: +// +// <h3>JSON Conversion</h3> +// <p>A type adapter registered with Gson is automatically invoked while serializing +// or deserializing JSON. However, you can also use type adapters directly to serialize +// and deserialize JSON. Here is an example for deserialization: <pre> {@code +// +// String json = "{'origin':'0,0','points':['1,2','3,4']}"; +// TypeAdapter<Graph> graphAdapter = gson.getAdapter(Graph.class); +// Graph graph = graphAdapter.fromJson(json); +// }</pre> +// And an example for serialization: <pre> {@code +// +// Graph graph = new Graph(...); +// TypeAdapter<Graph> graphAdapter = gson.getAdapter(Graph.class); +// String json = graphAdapter.toJson(graph); +// }</pre> +// +// <p>Type adapters are <strong>type-specific</strong>. For example, a {@code +// TypeAdapter<Date>} can convert {@code Date} instances to JSON and JSON to +// instances of {@code Date}, but cannot convert any other types. +// +public abstract class TypeAdapter<T> { + + /** + * Writes one JSON value (an array, object, string, number, boolean or null) + * for {@code value}. + * + * @param value the Java object to write. May be null. + */ + public abstract void write(JsonWriter out, T value) throws IOException; + + /** + * Converts {@code value} to a JSON document and writes it to {@code out}. + * Unlike Gson's similar {@link Gson#toJson(JsonElement, Appendable) toJson} + * method, this write is strict. Create a {@link + * JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call + * {@link #write(com.google.gson.stream.JsonWriter, Object)} for lenient + * writing. + * + * @param value the Java object to convert. May be null. + * @since 2.2 + */ + public final void toJson(Writer out, T value) throws IOException { + JsonWriter writer = new JsonWriter(out); + write(writer, value); + } + + /** + * This wrapper method is used to make a type adapter null tolerant. In general, a + * type adapter is required to handle nulls in write and read methods. Here is how this + * is typically done:<br> + * <pre> {@code + * + * Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, + * new TypeAdapter<Foo>() { + * public Foo read(JsonReader in) throws IOException { + * if (in.peek() == JsonToken.NULL) { + * in.nextNull(); + * return null; + * } + * // read a Foo from in and return it + * } + * public void write(JsonWriter out, Foo src) throws IOException { + * if (src == null) { + * out.nullValue(); + * return; + * } + * // write src as JSON to out + * } + * }).create(); + * }</pre> + * You can avoid this boilerplate handling of nulls by wrapping your type adapter with + * this method. Here is how we will rewrite the above example: + * <pre> {@code + * + * Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, + * new TypeAdapter<Foo>() { + * public Foo read(JsonReader in) throws IOException { + * // read a Foo from in and return it + * } + * public void write(JsonWriter out, Foo src) throws IOException { + * // write src as JSON to out + * } + * }.nullSafe()).create(); + * }</pre> + * Note that we didn't need to check for nulls in our type adapter after we used nullSafe. + */ + public final TypeAdapter<T> nullSafe() { + return new TypeAdapter<T>() { + @Override public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + TypeAdapter.this.write(out, value); + } + } + @Override public T read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + return TypeAdapter.this.read(reader); + } + }; + } + + /** + * Converts {@code value} to a JSON document. Unlike Gson's similar {@link + * Gson#toJson(Object) toJson} method, this write is strict. Create a {@link + * JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call + * {@link #write(com.google.gson.stream.JsonWriter, Object)} for lenient + * writing. + * + * @param value the Java object to convert. May be null. + * @since 2.2 + */ + public final String toJson(T value) { + StringWriter stringWriter = new StringWriter(); + try { + toJson(stringWriter, value); + } catch (IOException e) { + throw new AssertionError(e); // No I/O writing to a StringWriter. + } + return stringWriter.toString(); + } + + /** + * Converts {@code value} to a JSON tree. + * + * @param value the Java object to convert. May be null. + * @return the converted JSON tree. May be {@link JsonNull}. + * @since 2.2 + */ + public final JsonElement toJsonTree(T value) { + try { + JsonTreeWriter jsonWriter = new JsonTreeWriter(); + write(jsonWriter, value); + return jsonWriter.get(); + } catch (IOException e) { + throw new JsonIOException(e); + } + } + + /** + * Reads one JSON value (an array, object, string, number, boolean or null) + * and converts it to a Java object. Returns the converted object. + * + * @return the converted Java object. May be null. + */ + public abstract T read(JsonReader in) throws IOException; + + /** + * Converts the JSON document in {@code in} to a Java object. Unlike Gson's + * similar {@link Gson#fromJson(java.io.Reader, Class) fromJson} method, this + * read is strict. Create a {@link JsonReader#setLenient(boolean) lenient} + * {@code JsonReader} and call {@link #read(JsonReader)} for lenient reading. + * + * @return the converted Java object. May be null. + * @since 2.2 + */ + public final T fromJson(Reader in) throws IOException { + JsonReader reader = new JsonReader(in); + return read(reader); + } + + /** + * Converts the JSON document in {@code json} to a Java object. Unlike Gson's + * similar {@link Gson#fromJson(String, Class) fromJson} method, this read is + * strict. Create a {@link JsonReader#setLenient(boolean) lenient} {@code + * JsonReader} and call {@link #read(JsonReader)} for lenient reading. + * + * @return the converted Java object. May be null. + * @since 2.2 + */ + public final T fromJson(String json) throws IOException { + return fromJson(new StringReader(json)); + } + + /** + * Converts {@code jsonTree} to a Java object. + * + * @param jsonTree the Java object to convert. May be {@link JsonNull}. + * @since 2.2 + */ + public final T fromJsonTree(JsonElement jsonTree) { + try { + JsonReader jsonReader = new JsonTreeReader(jsonTree); + return read(jsonReader); + } catch (IOException e) { + throw new JsonIOException(e); + } + } +} diff --git a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java new file mode 100644 index 00000000..e12a72dc --- /dev/null +++ b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2011 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 com.google.gson; + +import com.google.gson.reflect.TypeToken; + +/** + * Creates type adapters for set of related types. Type adapter factories are + * most useful when several types share similar structure in their JSON form. + * + * <h3>Example: Converting enums to lowercase</h3> + * In this example, we implement a factory that creates type adapters for all + * enums. The type adapters will write enums in lowercase, despite the fact + * that they're defined in {@code CONSTANT_CASE} in the corresponding Java + * model: <pre> {@code + * + * public class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory { + * public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + * Class<T> rawType = (Class<T>) type.getRawType(); + * if (!rawType.isEnum()) { + * return null; + * } + * + * final Map<String, T> lowercaseToConstant = new HashMap<String, T>(); + * for (T constant : rawType.getEnumConstants()) { + * lowercaseToConstant.put(toLowercase(constant), constant); + * } + * + * return new TypeAdapter<T>() { + * public void write(JsonWriter out, T value) throws IOException { + * if (value == null) { + * out.nullValue(); + * } else { + * out.value(toLowercase(value)); + * } + * } + * + * public T read(JsonReader reader) throws IOException { + * if (reader.peek() == JsonToken.NULL) { + * reader.nextNull(); + * return null; + * } else { + * return lowercaseToConstant.get(reader.nextString()); + * } + * } + * }; + * } + * + * private String toLowercase(Object o) { + * return o.toString().toLowerCase(Locale.US); + * } + * } + * }</pre> + * + * <p>Type adapter factories select which types they provide type adapters + * for. If a factory cannot support a given type, it must return null when + * that type is passed to {@link #create}. Factories should expect {@code + * create()} to be called on them for many types and should return null for + * most of those types. In the above example the factory returns null for + * calls to {@code create()} where {@code type} is not an enum. + * + * <p>A factory is typically called once per type, but the returned type + * adapter may be used many times. It is most efficient to do expensive work + * like reflection in {@code create()} so that the type adapter's {@code + * read()} and {@code write()} methods can be very fast. In this example the + * mapping from lowercase name to enum value is computed eagerly. + * + * <p>As with type adapters, factories must be <i>registered</i> with a {@link + * com.google.gson.GsonBuilder} for them to take effect: <pre> {@code + * + * GsonBuilder builder = new GsonBuilder(); + * builder.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory()); + * ... + * Gson gson = builder.create(); + * }</pre> + * If multiple factories support the same type, the factory registered earlier + * takes precedence. + * + * <h3>Example: composing other type adapters</h3> + * In this example we implement a factory for Guava's {@code Multiset} + * collection type. The factory can be used to create type adapters for + * multisets of any element type: the type adapter for {@code + * Multiset<String>} is different from the type adapter for {@code + * Multiset<URL>}. + * + * <p>The type adapter <i>delegates</i> to another type adapter for the + * multiset elements. It figures out the element type by reflecting on the + * multiset's type token. A {@code Gson} is passed in to {@code create} for + * just this purpose: <pre> {@code + * + * public class MultisetTypeAdapterFactory implements TypeAdapterFactory { + * public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + * Type type = typeToken.getType(); + * if (typeToken.getRawType() != Multiset.class + * || !(type instanceof ParameterizedType)) { + * return null; + * } + * + * Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + * TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType)); + * return (TypeAdapter<T>) newMultisetAdapter(elementAdapter); + * } + * + * private <E> TypeAdapter<Multiset<E>> newMultisetAdapter( + * final TypeAdapter<E> elementAdapter) { + * return new TypeAdapter<Multiset<E>>() { + * public void write(JsonWriter out, Multiset<E> value) throws IOException { + * if (value == null) { + * out.nullValue(); + * return; + * } + * + * out.beginArray(); + * for (Multiset.Entry<E> entry : value.entrySet()) { + * out.value(entry.getCount()); + * elementAdapter.write(out, entry.getElement()); + * } + * out.endArray(); + * } + * + * public Multiset<E> read(JsonReader in) throws IOException { + * if (in.peek() == JsonToken.NULL) { + * in.nextNull(); + * return null; + * } + * + * Multiset<E> result = LinkedHashMultiset.create(); + * in.beginArray(); + * while (in.hasNext()) { + * int count = in.nextInt(); + * E element = elementAdapter.read(in); + * result.add(element, count); + * } + * in.endArray(); + * return result; + * } + * }; + * } + * } + * }</pre> + * Delegating from one type adapter to another is extremely powerful; it's + * the foundation of how Gson converts Java objects and collections. Whenever + * possible your factory should retrieve its delegate type adapter in the + * {@code create()} method; this ensures potentially-expensive type adapter + * creation happens only once. + * + * @since 2.1 + */ +public interface TypeAdapterFactory { + + /** + * Returns a type adapter for {@code type}, or null if this factory doesn't + * support {@code type}. + */ + <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type); +} diff --git a/gson/src/main/java/com/google/gson/annotations/Expose.java b/gson/src/main/java/com/google/gson/annotations/Expose.java new file mode 100644 index 00000000..1b9c70df --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/Expose.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008 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 com.google.gson.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates this member should be exposed for JSON + * serialization or deserialization. + * + * <p>This annotation has no effect unless you build {@link com.google.gson.Gson} + * with a {@link com.google.gson.GsonBuilder} and invoke + * {@link com.google.gson.GsonBuilder#excludeFieldsWithoutExposeAnnotation()} + * method.</p> + * + * <p>Here is an example of how this annotation is meant to be used: + * <p><pre> + * public class User { + * @Expose private String firstName; + * @Expose(serialize = false) private String lastName; + * @Expose (serialize = false, deserialize = false) private String emailAddress; + * private String password; + * } + * </pre></p> + * If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()} + * methods will use the {@code password} field along-with {@code firstName}, {@code lastName}, + * and {@code emailAddress} for serialization and deserialization. However, if you created Gson + * with {@code Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()} + * then the {@code toJson()} and {@code fromJson()} methods of Gson will exclude the + * {@code password} field. This is because the {@code password} field is not marked with the + * {@code @Expose} annotation. Gson will also exclude {@code lastName} and {@code emailAddress} + * from serialization since {@code serialize} is set to {@code false}. Similarly, Gson will + * exclude {@code emailAddress} from deserialization since {@code deserialize} is set to false. + * + * <p>Note that another way to achieve the same effect would have been to just mark the + * {@code password} field as {@code transient}, and Gson would have excluded it even with default + * settings. The {@code @Expose} annotation is useful in a style of programming where you want to + * explicitly specify all fields that should get considered for serialization or deserialization. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Expose { + + /** + * If {@code true}, the field marked with this annotation is written out in the JSON while + * serializing. If {@code false}, the field marked with this annotation is skipped from the + * serialized output. Defaults to {@code true}. + * @since 1.4 + */ + public boolean serialize() default true; + + /** + * If {@code true}, the field marked with this annotation is deserialized from the JSON. + * If {@code false}, the field marked with this annotation is skipped during deserialization. + * Defaults to {@code true}. + * @since 1.4 + */ + public boolean deserialize() default true; +} diff --git a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java new file mode 100644 index 00000000..2ee3e682 --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 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 com.google.gson.annotations; + +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates the Gson {@link TypeAdapter} to use with a class + * or field. + * + * <p>Here is an example of how this annotation is used:</p> + * <pre> + * @JsonAdapter(UserJsonAdapter.class) + * public class User { + * public final String firstName, lastName; + * private User(String firstName, String lastName) { + * this.firstName = firstName; + * this.lastName = lastName; + * } + * } + * public class UserJsonAdapter extends TypeAdapter<User> { + * @Override public void write(JsonWriter out, User user) throws IOException { + * // implement write: combine firstName and lastName into name + * out.beginObject(); + * out.name("name"); + * out.value(user.firstName + " " + user.lastName); + * out.endObject(); + * // implement the write method + * } + * @Override public User read(JsonReader in) throws IOException { + * // implement read: split name into firstName and lastName + * in.beginObject(); + * in.nextName(); + * String[] nameParts = in.nextString().split(" "); + * in.endObject(); + * return new User(nameParts[0], nameParts[1]); + * } + * } + * </pre> + * + * Since User class specified UserJsonAdapter.class in @JsonAdapter annotation, it + * will automatically be invoked to serialize/deserialize User instances. <br> + * + * <p> Here is an example of how to apply this annotation to a field. + * <pre> + * private static final class Gadget { + * @JsonAdapter(UserJsonAdapter2.class) + * final User user; + * Gadget(User user) { + * this.user = user; + * } + * } + * </pre> + * + * It's possible to specify different type adapters on a field, that + * field's type, and in the {@link com.google.gson.GsonBuilder}. Field + * annotations take precedence over {@code GsonBuilder}-registered type + * adapters, which in turn take precedence over annotated types. + * + * <p>The class referenced by this annotation must be either a {@link + * TypeAdapter} or a {@link TypeAdapterFactory}. Using the factory interface + * makes it possible to delegate to the enclosing {@code Gson} instance. + * + * @since 2.3 + * + * @author Inderjeet Singh + * @author Joel Leitch + * @author Jesse Wilson + */ +// Note that the above example is taken from AdaptAnnotationTest. +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD}) +public @interface JsonAdapter { + + /** Either a {@link TypeAdapter} or {@link TypeAdapterFactory}. */ + Class<?> value(); + +} diff --git a/gson/src/main/java/com/google/gson/annotations/SerializedName.java b/gson/src/main/java/com/google/gson/annotations/SerializedName.java new file mode 100644 index 00000000..06c0a14c --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/SerializedName.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 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 com.google.gson.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates this member should be serialized to JSON with + * the provided name value as its field name. + * + * <p>This annotation will override any {@link com.google.gson.FieldNamingPolicy}, including + * the default field naming policy, that may have been set on the {@link com.google.gson.Gson} + * instance. A different naming policy can set using the {@code GsonBuilder} class. See + * {@link com.google.gson.GsonBuilder#setFieldNamingPolicy(com.google.gson.FieldNamingPolicy)} + * for more information.</p> + * + * <p>Here is an example of how this annotation is meant to be used:</p> + * <pre> + * public class MyClass { + * @SerializedName("name") String a; + * @SerializedName(value="name1", alternate={"name2", "name3"}) String b; + * String c; + * + * public MyClass(String a, String b, String c) { + * this.a = a; + * this.b = b; + * this.c = c; + * } + * } + * </pre> + * + * <p>The following shows the output that is generated when serializing an instance of the + * above example class:</p> + * <pre> + * MyClass target = new MyClass("v1", "v2", "v3"); + * Gson gson = new Gson(); + * String json = gson.toJson(target); + * System.out.println(json); + * + * ===== OUTPUT ===== + * {"name":"v1","name1":"v2","c":"v3"} + * </pre> + * + * <p>NOTE: The value you specify in this annotation must be a valid JSON field name.</p> + * While deserializing, all values specified in the annotation will be deserialized into the field. + * For example: + * <pre> + * MyClass target = gson.fromJson("{'name1':'v1'}", MyClass.class); + * assertEquals("v1", target.b); + * target = gson.fromJson("{'name2':'v2'}", MyClass.class); + * assertEquals("v2", target.b); + * target = gson.fromJson("{'name3':'v3'}", MyClass.class); + * assertEquals("v3", target.b); + * </pre> + * Note that MyClass.b is now deserialized from either name1, name2 or name3. + * + * @see com.google.gson.FieldNamingPolicy + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface SerializedName { + + /** + * @return the desired names of the field when it is deserialized or serialized. All of the specified names will be deserialized from. + * The specified first name is what is used for serialization. + */ + String value(); + String[] alternate() default {}; +} diff --git a/gson/src/main/java/com/google/gson/annotations/Since.java b/gson/src/main/java/com/google/gson/annotations/Since.java new file mode 100644 index 00000000..541f154b --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/Since.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2008 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 com.google.gson.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates the version number since a member or a type has been present. + * This annotation is useful to manage versioning of your Json classes for a web-service. + * + * <p> + * This annotation has no effect unless you build {@link com.google.gson.Gson} with a + * {@link com.google.gson.GsonBuilder} and invoke + * {@link com.google.gson.GsonBuilder#setVersion(double)} method. + * + * <p>Here is an example of how this annotation is meant to be used:</p> + * <pre> + * public class User { + * private String firstName; + * private String lastName; + * @Since(1.0) private String emailAddress; + * @Since(1.0) private String password; + * @Since(1.1) private Address address; + * } + * </pre> + * + * <p>If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()} + * methods will use all the fields for serialization and deserialization. However, if you created + * Gson with {@code Gson gson = new GsonBuilder().setVersion(1.0).create()} then the + * {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code address} field + * since it's version number is set to {@code 1.1}.</p> + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface Since { + /** + * the value indicating a version number since this member + * or type has been present. + */ + double value(); +} diff --git a/gson/src/main/java/com/google/gson/annotations/Until.java b/gson/src/main/java/com/google/gson/annotations/Until.java new file mode 100644 index 00000000..4648b8a2 --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/Until.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2008 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 com.google.gson.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates the version number until a member or a type should be present. + * Basically, if Gson is created with a version number that exceeds the value stored in the + * {@code Until} annotation then the field will be ignored from the JSON output. This annotation + * is useful to manage versioning of your JSON classes for a web-service. + * + * <p> + * This annotation has no effect unless you build {@link com.google.gson.Gson} with a + * {@link com.google.gson.GsonBuilder} and invoke + * {@link com.google.gson.GsonBuilder#setVersion(double)} method. + * + * <p>Here is an example of how this annotation is meant to be used:</p> + * <pre> + * public class User { + * private String firstName; + * private String lastName; + * @Until(1.1) private String emailAddress; + * @Until(1.1) private String password; + * } + * </pre> + * + * <p>If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()} + * methods will use all the fields for serialization and deserialization. However, if you created + * Gson with {@code Gson gson = new GsonBuilder().setVersion(1.2).create()} then the + * {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code emailAddress} + * and {@code password} fields from the example above, because the version number passed to the + * GsonBuilder, {@code 1.2}, exceeds the version number set on the {@code Until} annotation, + * {@code 1.1}, for those fields. + * + * @author Inderjeet Singh + * @author Joel Leitch + * @since 1.3 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface Until { + + /** + * the value indicating a version number until this member + * or type should be ignored. + */ + double value(); +} diff --git a/gson/src/main/java/com/google/gson/annotations/package-info.java b/gson/src/main/java/com/google/gson/annotations/package-info.java new file mode 100644 index 00000000..1c461fd6 --- /dev/null +++ b/gson/src/main/java/com/google/gson/annotations/package-info.java @@ -0,0 +1,6 @@ +/** + * This package provides annotations that can be used with {@link com.google.gson.Gson}. + * + * @author Inderjeet Singh, Joel Leitch + */ +package com.google.gson.annotations;
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java b/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java new file mode 100644 index 00000000..f0e7d3fa --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java @@ -0,0 +1,49 @@ +/*
+ * Copyright (C) 2008 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 com.google.gson.internal;
+
+/**
+ * A simple utility class used to check method Preconditions.
+ *
+ * <pre>
+ * public long divideBy(long value) {
+ * Preconditions.checkArgument(value != 0);
+ * return this.value / value;
+ * }
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class $Gson$Preconditions {
+ private $Gson$Preconditions() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static <T> T checkNotNull(T obj) {
+ if (obj == null) {
+ throw new NullPointerException();
+ }
+ return obj;
+ }
+
+ public static void checkArgument(boolean condition) {
+ if (!condition) {
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java new file mode 100644 index 00000000..745d0719 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java @@ -0,0 +1,588 @@ +/** + * Copyright (C) 2008 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 com.google.gson.internal; + +import static com.google.gson.internal.$Gson$Preconditions.checkArgument; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Properties; + +/** + * Static methods for working with types. + * + * @author Bob Lee + * @author Jesse Wilson + */ +public final class $Gson$Types { + static final Type[] EMPTY_TYPE_ARRAY = new Type[] {}; + + private $Gson$Types() { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new parameterized type, applying {@code typeArguments} to + * {@code rawType} and enclosed by {@code ownerType}. + * + * @return a {@link java.io.Serializable serializable} parameterized type. + */ + public static ParameterizedType newParameterizedTypeWithOwner( + Type ownerType, Type rawType, Type... typeArguments) { + return new ParameterizedTypeImpl(ownerType, rawType, typeArguments); + } + + /** + * Returns an array type whose elements are all instances of + * {@code componentType}. + * + * @return a {@link java.io.Serializable serializable} generic array type. + */ + public static GenericArrayType arrayOf(Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + /** + * Returns a type that represents an unknown type that extends {@code bound}. + * For example, if {@code bound} is {@code CharSequence.class}, this returns + * {@code ? extends CharSequence}. If {@code bound} is {@code Object.class}, + * this returns {@code ?}, which is shorthand for {@code ? extends Object}. + */ + public static WildcardType subtypeOf(Type bound) { + return new WildcardTypeImpl(new Type[] { bound }, EMPTY_TYPE_ARRAY); + } + + /** + * Returns a type that represents an unknown supertype of {@code bound}. For + * example, if {@code bound} is {@code String.class}, this returns {@code ? + * super String}. + */ + public static WildcardType supertypeOf(Type bound) { + return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound }); + } + + /** + * Returns a type that is functionally equal but not necessarily equal + * according to {@link Object#equals(Object) Object.equals()}. The returned + * type is {@link java.io.Serializable}. + */ + public static Type canonicalize(Type type) { + if (type instanceof Class) { + Class<?> c = (Class<?>) type; + return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c; + + } else if (type instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) type; + return new ParameterizedTypeImpl(p.getOwnerType(), + p.getRawType(), p.getActualTypeArguments()); + + } else if (type instanceof GenericArrayType) { + GenericArrayType g = (GenericArrayType) type; + return new GenericArrayTypeImpl(g.getGenericComponentType()); + + } else if (type instanceof WildcardType) { + WildcardType w = (WildcardType) type; + return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); + + } else { + // type is either serializable as-is or unsupported + return type; + } + } + + public static Class<?> getRawType(Type type) { + if (type instanceof Class<?>) { + // type is a normal class. + return (Class<?>) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // I'm not exactly sure why getRawType() returns Type instead of Class. + // Neal isn't either but suspects some pathological case related + // to nested classes exists. + Type rawType = parameterizedType.getRawType(); + checkArgument(rawType instanceof Class); + return (Class<?>) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType)type).getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // we could use the variable's bounds, but that won't work if there are multiple. + // having a raw type that's more general than necessary is okay + return Object.class; + + } else if (type instanceof WildcardType) { + return getRawType(((WildcardType) type).getUpperBounds()[0]); + + } else { + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " + + "GenericArrayType, but <" + type + "> is of type " + className); + } + } + + static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Returns true if {@code a} and {@code b} are equal. + */ + public static boolean equals(Type a, Type b) { + if (a == b) { + // also handles (a == null && b == null) + return true; + + } else if (a instanceof Class) { + // Class already specifies equals(). + return a.equals(b); + + } else if (a instanceof ParameterizedType) { + if (!(b instanceof ParameterizedType)) { + return false; + } + + // TODO: save a .clone() call + ParameterizedType pa = (ParameterizedType) a; + ParameterizedType pb = (ParameterizedType) b; + return equal(pa.getOwnerType(), pb.getOwnerType()) + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); + + } else if (a instanceof GenericArrayType) { + if (!(b instanceof GenericArrayType)) { + return false; + } + + GenericArrayType ga = (GenericArrayType) a; + GenericArrayType gb = (GenericArrayType) b; + return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); + + } else if (a instanceof WildcardType) { + if (!(b instanceof WildcardType)) { + return false; + } + + WildcardType wa = (WildcardType) a; + WildcardType wb = (WildcardType) b; + return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + + } else if (a instanceof TypeVariable) { + if (!(b instanceof TypeVariable)) { + return false; + } + TypeVariable<?> va = (TypeVariable<?>) a; + TypeVariable<?> vb = (TypeVariable<?>) b; + return va.getGenericDeclaration() == vb.getGenericDeclaration() + && va.getName().equals(vb.getName()); + + } else { + // This isn't a type we support. Could be a generic array type, wildcard type, etc. + return false; + } + } + + private static int hashCodeOrZero(Object o) { + return o != null ? o.hashCode() : 0; + } + + public static String typeToString(Type type) { + return type instanceof Class ? ((Class<?>) type).getName() : type.toString(); + } + + /** + * Returns the generic supertype for {@code supertype}. For example, given a class {@code + * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the + * result when the supertype is {@code Collection.class} is {@code Collection<Integer>}. + */ + static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) { + if (toResolve == rawType) { + return context; + } + + // we skip searching through interfaces if unknown is an interface + if (toResolve.isInterface()) { + Class<?>[] interfaces = rawType.getInterfaces(); + for (int i = 0, length = interfaces.length; i < length; i++) { + if (interfaces[i] == toResolve) { + return rawType.getGenericInterfaces()[i]; + } else if (toResolve.isAssignableFrom(interfaces[i])) { + return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve); + } + } + } + + // check our supertypes + if (!rawType.isInterface()) { + while (rawType != Object.class) { + Class<?> rawSupertype = rawType.getSuperclass(); + if (rawSupertype == toResolve) { + return rawType.getGenericSuperclass(); + } else if (toResolve.isAssignableFrom(rawSupertype)) { + return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve); + } + rawType = rawSupertype; + } + } + + // we can't resolve this further + return toResolve; + } + + /** + * Returns the generic form of {@code supertype}. For example, if this is {@code + * ArrayList<String>}, this returns {@code Iterable<String>} given the input {@code + * Iterable.class}. + * + * @param supertype a superclass of, or interface implemented by, this. + */ + static Type getSupertype(Type context, Class<?> contextRawType, Class<?> supertype) { + checkArgument(supertype.isAssignableFrom(contextRawType)); + return resolve(context, contextRawType, + $Gson$Types.getGenericSupertype(context, contextRawType, supertype)); + } + + /** + * Returns the component type of this array type. + * @throws ClassCastException if this type is not an array. + */ + public static Type getArrayComponentType(Type array) { + return array instanceof GenericArrayType + ? ((GenericArrayType) array).getGenericComponentType() + : ((Class<?>) array).getComponentType(); + } + + /** + * Returns the element type of this collection type. + * @throws IllegalArgumentException if this type is not a collection. + */ + public static Type getCollectionElementType(Type context, Class<?> contextRawType) { + Type collectionType = getSupertype(context, contextRawType, Collection.class); + + if (collectionType instanceof WildcardType) { + collectionType = ((WildcardType)collectionType).getUpperBounds()[0]; + } + if (collectionType instanceof ParameterizedType) { + return ((ParameterizedType) collectionType).getActualTypeArguments()[0]; + } + return Object.class; + } + + /** + * Returns a two element array containing this map's key and value types in + * positions 0 and 1 respectively. + */ + public static Type[] getMapKeyAndValueTypes(Type context, Class<?> contextRawType) { + /* + * Work around a problem with the declaration of java.util.Properties. That + * class should extend Hashtable<String, String>, but it's declared to + * extend Hashtable<Object, Object>. + */ + if (context == Properties.class) { + return new Type[] { String.class, String.class }; // TODO: test subclasses of Properties! + } + + Type mapType = getSupertype(context, contextRawType, Map.class); + // TODO: strip wildcards? + if (mapType instanceof ParameterizedType) { + ParameterizedType mapParameterizedType = (ParameterizedType) mapType; + return mapParameterizedType.getActualTypeArguments(); + } + return new Type[] { Object.class, Object.class }; + } + + public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) { + // this implementation is made a little more complicated in an attempt to avoid object-creation + while (true) { + if (toResolve instanceof TypeVariable) { + TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve; + toResolve = resolveTypeVariable(context, contextRawType, typeVariable); + if (toResolve == typeVariable) { + return toResolve; + } + + } else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) { + Class<?> original = (Class<?>) toResolve; + Type componentType = original.getComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType + ? original + : arrayOf(newComponentType); + + } else if (toResolve instanceof GenericArrayType) { + GenericArrayType original = (GenericArrayType) toResolve; + Type componentType = original.getGenericComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType + ? original + : arrayOf(newComponentType); + + } else if (toResolve instanceof ParameterizedType) { + ParameterizedType original = (ParameterizedType) toResolve; + Type ownerType = original.getOwnerType(); + Type newOwnerType = resolve(context, contextRawType, ownerType); + boolean changed = newOwnerType != ownerType; + + Type[] args = original.getActualTypeArguments(); + for (int t = 0, length = args.length; t < length; t++) { + Type resolvedTypeArgument = resolve(context, contextRawType, args[t]); + if (resolvedTypeArgument != args[t]) { + if (!changed) { + args = args.clone(); + changed = true; + } + args[t] = resolvedTypeArgument; + } + } + + return changed + ? newParameterizedTypeWithOwner(newOwnerType, original.getRawType(), args) + : original; + + } else if (toResolve instanceof WildcardType) { + WildcardType original = (WildcardType) toResolve; + Type[] originalLowerBound = original.getLowerBounds(); + Type[] originalUpperBound = original.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]); + if (lowerBound != originalLowerBound[0]) { + return supertypeOf(lowerBound); + } + } else if (originalUpperBound.length == 1) { + Type upperBound = resolve(context, contextRawType, originalUpperBound[0]); + if (upperBound != originalUpperBound[0]) { + return subtypeOf(upperBound); + } + } + return original; + + } else { + return toResolve; + } + } + } + + static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) { + Class<?> declaredByRaw = declaringClassOf(unknown); + + // we can't reduce this further + if (declaredByRaw == null) { + return unknown; + } + + Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); + if (declaredBy instanceof ParameterizedType) { + int index = indexOf(declaredByRaw.getTypeParameters(), unknown); + return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; + } + + return unknown; + } + + private static int indexOf(Object[] array, Object toFind) { + for (int i = 0; i < array.length; i++) { + if (toFind.equals(array[i])) { + return i; + } + } + throw new NoSuchElementException(); + } + + /** + * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by + * a class. + */ + private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class + ? (Class<?>) genericDeclaration + : null; + } + + private static void checkNotPrimitive(Type type) { + checkArgument(!(type instanceof Class<?>) || !((Class<?>) type).isPrimitive()); + } + + private static final class ParameterizedTypeImpl implements ParameterizedType, Serializable { + private final Type ownerType; + private final Type rawType; + private final Type[] typeArguments; + + public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { + // require an owner type if the raw type needs it + if (rawType instanceof Class<?>) { + Class<?> rawTypeAsClass = (Class<?>) rawType; + boolean isStaticOrTopLevelClass = Modifier.isStatic(rawTypeAsClass.getModifiers()) + || rawTypeAsClass.getEnclosingClass() == null; + checkArgument(ownerType != null || isStaticOrTopLevelClass); + } + + this.ownerType = ownerType == null ? null : canonicalize(ownerType); + this.rawType = canonicalize(rawType); + this.typeArguments = typeArguments.clone(); + for (int t = 0; t < this.typeArguments.length; t++) { + checkNotNull(this.typeArguments[t]); + checkNotPrimitive(this.typeArguments[t]); + this.typeArguments[t] = canonicalize(this.typeArguments[t]); + } + } + + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + public Type getRawType() { + return rawType; + } + + public Type getOwnerType() { + return ownerType; + } + + @Override public boolean equals(Object other) { + return other instanceof ParameterizedType + && $Gson$Types.equals(this, (ParameterizedType) other); + } + + @Override public int hashCode() { + return Arrays.hashCode(typeArguments) + ^ rawType.hashCode() + ^ hashCodeOrZero(ownerType); + } + + @Override public String toString() { + StringBuilder stringBuilder = new StringBuilder(30 * (typeArguments.length + 1)); + stringBuilder.append(typeToString(rawType)); + + if (typeArguments.length == 0) { + return stringBuilder.toString(); + } + + stringBuilder.append("<").append(typeToString(typeArguments[0])); + for (int i = 1; i < typeArguments.length; i++) { + stringBuilder.append(", ").append(typeToString(typeArguments[i])); + } + return stringBuilder.append(">").toString(); + } + + private static final long serialVersionUID = 0; + } + + private static final class GenericArrayTypeImpl implements GenericArrayType, Serializable { + private final Type componentType; + + public GenericArrayTypeImpl(Type componentType) { + this.componentType = canonicalize(componentType); + } + + public Type getGenericComponentType() { + return componentType; + } + + @Override public boolean equals(Object o) { + return o instanceof GenericArrayType + && $Gson$Types.equals(this, (GenericArrayType) o); + } + + @Override public int hashCode() { + return componentType.hashCode(); + } + + @Override public String toString() { + return typeToString(componentType) + "[]"; + } + + private static final long serialVersionUID = 0; + } + + /** + * The WildcardType interface supports multiple upper bounds and multiple + * lower bounds. We only support what the Java 6 language needs - at most one + * bound. If a lower bound is set, the upper bound must be Object.class. + */ + private static final class WildcardTypeImpl implements WildcardType, Serializable { + private final Type upperBound; + private final Type lowerBound; + + public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + checkArgument(lowerBounds.length <= 1); + checkArgument(upperBounds.length == 1); + + if (lowerBounds.length == 1) { + checkNotNull(lowerBounds[0]); + checkNotPrimitive(lowerBounds[0]); + checkArgument(upperBounds[0] == Object.class); + this.lowerBound = canonicalize(lowerBounds[0]); + this.upperBound = Object.class; + + } else { + checkNotNull(upperBounds[0]); + checkNotPrimitive(upperBounds[0]); + this.lowerBound = null; + this.upperBound = canonicalize(upperBounds[0]); + } + } + + public Type[] getUpperBounds() { + return new Type[] { upperBound }; + } + + public Type[] getLowerBounds() { + return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY; + } + + @Override public boolean equals(Object other) { + return other instanceof WildcardType + && $Gson$Types.equals(this, (WildcardType) other); + } + + @Override public int hashCode() { + // this equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()); + return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) + ^ (31 + upperBound.hashCode()); + } + + @Override public String toString() { + if (lowerBound != null) { + return "? super " + typeToString(lowerBound); + } else if (upperBound == Object.class) { + return "?"; + } else { + return "? extends " + typeToString(upperBound); + } + } + + private static final long serialVersionUID = 0; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java new file mode 100644 index 00000000..62bae91c --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal; + +import com.google.gson.InstanceCreator; +import com.google.gson.JsonIOException; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Returns a function that can construct an instance of a requested type. + */ +public final class ConstructorConstructor { + private final Map<Type, InstanceCreator<?>> instanceCreators; + + public ConstructorConstructor(Map<Type, InstanceCreator<?>> instanceCreators) { + this.instanceCreators = instanceCreators; + } + + public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) { + final Type type = typeToken.getType(); + final Class<? super T> rawType = typeToken.getRawType(); + + // first try an instance creator + + @SuppressWarnings("unchecked") // types must agree + final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type); + if (typeCreator != null) { + return new ObjectConstructor<T>() { + public T construct() { + return typeCreator.createInstance(type); + } + }; + } + + // Next try raw type match for instance creators + @SuppressWarnings("unchecked") // types must agree + final InstanceCreator<T> rawTypeCreator = + (InstanceCreator<T>) instanceCreators.get(rawType); + if (rawTypeCreator != null) { + return new ObjectConstructor<T>() { + public T construct() { + return rawTypeCreator.createInstance(type); + } + }; + } + + ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType); + if (defaultConstructor != null) { + return defaultConstructor; + } + + ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType); + if (defaultImplementation != null) { + return defaultImplementation; + } + + // finally try unsafe + return newUnsafeAllocator(type, rawType); + } + + private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) { + try { + final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + return new ObjectConstructor<T>() { + @SuppressWarnings("unchecked") // T is the same raw type as is requested + public T construct() { + try { + Object[] args = null; + return (T) constructor.newInstance(args); + } catch (InstantiationException e) { + // TODO: JsonParseException ? + throw new RuntimeException("Failed to invoke " + constructor + " with no args", e); + } catch (InvocationTargetException e) { + // TODO: don't wrap if cause is unchecked! + // TODO: JsonParseException ? + throw new RuntimeException("Failed to invoke " + constructor + " with no args", + e.getTargetException()); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + }; + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Constructors for common interface types like Map and List and their + * subtypes. + */ + @SuppressWarnings("unchecked") // use runtime checks to guarantee that 'T' is what it is + private <T> ObjectConstructor<T> newDefaultImplementationConstructor( + final Type type, Class<? super T> rawType) { + if (Collection.class.isAssignableFrom(rawType)) { + if (SortedSet.class.isAssignableFrom(rawType)) { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new TreeSet<Object>(); + } + }; + } else if (EnumSet.class.isAssignableFrom(rawType)) { + return new ObjectConstructor<T>() { + @SuppressWarnings("rawtypes") + public T construct() { + if (type instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (elementType instanceof Class) { + return (T) EnumSet.noneOf((Class)elementType); + } else { + throw new JsonIOException("Invalid EnumSet type: " + type.toString()); + } + } else { + throw new JsonIOException("Invalid EnumSet type: " + type.toString()); + } + } + }; + } else if (Set.class.isAssignableFrom(rawType)) { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new LinkedHashSet<Object>(); + } + }; + } else if (Queue.class.isAssignableFrom(rawType)) { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new LinkedList<Object>(); + } + }; + } else { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new ArrayList<Object>(); + } + }; + } + } + + if (Map.class.isAssignableFrom(rawType)) { + if (SortedMap.class.isAssignableFrom(rawType)) { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new TreeMap<Object, Object>(); + } + }; + } else if (type instanceof ParameterizedType && !(String.class.isAssignableFrom( + TypeToken.get(((ParameterizedType) type).getActualTypeArguments()[0]).getRawType()))) { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new LinkedHashMap<Object, Object>(); + } + }; + } else { + return new ObjectConstructor<T>() { + public T construct() { + return (T) new LinkedTreeMap<String, Object>(); + } + }; + } + } + + return null; + } + + private <T> ObjectConstructor<T> newUnsafeAllocator( + final Type type, final Class<? super T> rawType) { + return new ObjectConstructor<T>() { + private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); + @SuppressWarnings("unchecked") + public T construct() { + try { + Object newInstance = unsafeAllocator.newInstance(rawType); + return (T) newInstance; + } catch (Exception e) { + throw new RuntimeException(("Unable to invoke no-args constructor for " + type + ". " + + "Register an InstanceCreator with Gson for this type may fix this problem."), e); + } + } + }; + } + + @Override public String toString() { + return instanceCreators.toString(); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/Excluder.java b/gson/src/main/java/com/google/gson/internal/Excluder.java new file mode 100644 index 00000000..1c71e3e7 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/Excluder.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2008 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 com.google.gson.internal; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.Since; +import com.google.gson.annotations.Until; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class selects which fields and types to omit. It is configurable, + * supporting version attributes {@link Since} and {@link Until}, modifiers, + * synthetic fields, anonymous and local classes, inner classes, and fields with + * the {@link Expose} annotation. + * + * <p>This class is a type adapter factory; types that are excluded will be + * adapted to null. It may delegate to another type adapter if only one + * direction is excluded. + * + * @author Joel Leitch + * @author Jesse Wilson + */ +public final class Excluder implements TypeAdapterFactory, Cloneable { + private static final double IGNORE_VERSIONS = -1.0d; + public static final Excluder DEFAULT = new Excluder(); + + private double version = IGNORE_VERSIONS; + private int modifiers = Modifier.TRANSIENT | Modifier.STATIC; + private boolean serializeInnerClasses = true; + private boolean requireExpose; + private List<ExclusionStrategy> serializationStrategies = Collections.emptyList(); + private List<ExclusionStrategy> deserializationStrategies = Collections.emptyList(); + + @Override protected Excluder clone() { + try { + return (Excluder) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + + public Excluder withVersion(double ignoreVersionsAfter) { + Excluder result = clone(); + result.version = ignoreVersionsAfter; + return result; + } + + public Excluder withModifiers(int... modifiers) { + Excluder result = clone(); + result.modifiers = 0; + for (int modifier : modifiers) { + result.modifiers |= modifier; + } + return result; + } + + public Excluder disableInnerClassSerialization() { + Excluder result = clone(); + result.serializeInnerClasses = false; + return result; + } + + public Excluder excludeFieldsWithoutExposeAnnotation() { + Excluder result = clone(); + result.requireExpose = true; + return result; + } + + public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, + boolean serialization, boolean deserialization) { + Excluder result = clone(); + if (serialization) { + result.serializationStrategies = new ArrayList<ExclusionStrategy>(serializationStrategies); + result.serializationStrategies.add(exclusionStrategy); + } + if (deserialization) { + result.deserializationStrategies + = new ArrayList<ExclusionStrategy>(deserializationStrategies); + result.deserializationStrategies.add(exclusionStrategy); + } + return result; + } + + public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) { + Class<?> rawType = type.getRawType(); + final boolean skipSerialize = excludeClass(rawType, true); + final boolean skipDeserialize = excludeClass(rawType, false); + + if (!skipSerialize && !skipDeserialize) { + return null; + } + + return new TypeAdapter<T>() { + /** The delegate is lazily created because it may not be needed, and creating it may fail. */ + private TypeAdapter<T> delegate; + + @Override public T read(JsonReader in) throws IOException { + if (skipDeserialize) { + in.skipValue(); + return null; + } + return delegate().read(in); + } + + @Override public void write(JsonWriter out, T value) throws IOException { + if (skipSerialize) { + out.nullValue(); + return; + } + delegate().write(out, value); + } + + private TypeAdapter<T> delegate() { + TypeAdapter<T> d = delegate; + return d != null + ? d + : (delegate = gson.getDelegateAdapter(Excluder.this, type)); + } + }; + } + + public boolean excludeField(Field field, boolean serialize) { + if ((modifiers & field.getModifiers()) != 0) { + return true; + } + + if (version != Excluder.IGNORE_VERSIONS + && !isValidVersion(field.getAnnotation(Since.class), field.getAnnotation(Until.class))) { + return true; + } + + if (field.isSynthetic()) { + return true; + } + + if (requireExpose) { + Expose annotation = field.getAnnotation(Expose.class); + if (annotation == null || (serialize ? !annotation.serialize() : !annotation.deserialize())) { + return true; + } + } + + if (!serializeInnerClasses && isInnerClass(field.getType())) { + return true; + } + + if (isAnonymousOrLocal(field.getType())) { + return true; + } + + List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies; + if (!list.isEmpty()) { + FieldAttributes fieldAttributes = new FieldAttributes(field); + for (ExclusionStrategy exclusionStrategy : list) { + if (exclusionStrategy.shouldSkipField(fieldAttributes)) { + return true; + } + } + } + + return false; + } + + public boolean excludeClass(Class<?> clazz, boolean serialize) { + if (version != Excluder.IGNORE_VERSIONS + && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) { + return true; + } + + if (!serializeInnerClasses && isInnerClass(clazz)) { + return true; + } + + if (isAnonymousOrLocal(clazz)) { + return true; + } + + List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies; + for (ExclusionStrategy exclusionStrategy : list) { + if (exclusionStrategy.shouldSkipClass(clazz)) { + return true; + } + } + + return false; + } + + private boolean isAnonymousOrLocal(Class<?> clazz) { + return !Enum.class.isAssignableFrom(clazz) + && (clazz.isAnonymousClass() || clazz.isLocalClass()); + } + + private boolean isInnerClass(Class<?> clazz) { + return clazz.isMemberClass() && !isStatic(clazz); + } + + private boolean isStatic(Class<?> clazz) { + return (clazz.getModifiers() & Modifier.STATIC) != 0; + } + + private boolean isValidVersion(Since since, Until until) { + return isValidSince(since) && isValidUntil(until); + } + + private boolean isValidSince(Since annotation) { + if (annotation != null) { + double annotationVersion = annotation.value(); + if (annotationVersion > version) { + return false; + } + } + return true; + } + + private boolean isValidUntil(Until annotation) { + if (annotation != null) { + double annotationVersion = annotation.value(); + if (annotationVersion <= version) { + return false; + } + } + return true; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java b/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java new file mode 100644 index 00000000..bbd47204 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal; + +import com.google.gson.stream.JsonReader; +import java.io.IOException; + +/** + * Internal-only APIs of JsonReader available only to other classes in Gson. + */ +public abstract class JsonReaderInternalAccess { + public static JsonReaderInternalAccess INSTANCE; + + /** + * Changes the type of the current property name token to a string value. + */ + public abstract void promoteNameToValue(JsonReader reader) throws IOException; +} diff --git a/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java b/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java new file mode 100644 index 00000000..3669af7b --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal; + +import java.io.ObjectStreamException; +import java.math.BigDecimal; + +/** + * This class holds a number value that is lazily converted to a specific number type + * + * @author Inderjeet Singh + */ +public final class LazilyParsedNumber extends Number { + private final String value; + + /** @param value must not be null */ + public LazilyParsedNumber(String value) { + this.value = value; + } + + @Override + public int intValue() { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + try { + return (int) Long.parseLong(value); + } catch (NumberFormatException nfe) { + return new BigDecimal(value).intValue(); + } + } + } + + @Override + public long longValue() { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return new BigDecimal(value).longValue(); + } + } + + @Override + public float floatValue() { + return Float.parseFloat(value); + } + + @Override + public double doubleValue() { + return Double.parseDouble(value); + } + + @Override + public String toString() { + return value; + } + + /** + * If somebody is unlucky enough to have to serialize one of these, serialize + * it as a BigDecimal so that they won't need Gson on the other side to + * deserialize it. + */ + private Object writeReplace() throws ObjectStreamException { + return new BigDecimal(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof LazilyParsedNumber) { + LazilyParsedNumber other = (LazilyParsedNumber) obj; + return value == other.value || value.equals(other.value); + } + return false; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/LinkedHashTreeMap.java b/gson/src/main/java/com/google/gson/internal/LinkedHashTreeMap.java new file mode 100644 index 00000000..e251ec2f --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/LinkedHashTreeMap.java @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2012 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 com.google.gson.internal; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses + * insertion order for iteration order. Comparison order is only used as an + * optimization for efficient insertion and removal. + * + * <p>This implementation was derived from Android 4.1's TreeMap and + * LinkedHashMap classes. + */ +public final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Serializable { + @SuppressWarnings({ "unchecked", "rawtypes" }) // to avoid Comparable<Comparable<Comparable<...>>> + private static final Comparator<Comparable> NATURAL_ORDER = new Comparator<Comparable>() { + public int compare(Comparable a, Comparable b) { + return a.compareTo(b); + } + }; + + Comparator<? super K> comparator; + Node<K, V>[] table; + final Node<K, V> header; + int size = 0; + int modCount = 0; + int threshold; + + /** + * Create a natural order, empty tree map whose keys must be mutually + * comparable and non-null. + */ + @SuppressWarnings("unchecked") // unsafe! this assumes K is comparable + public LinkedHashTreeMap() { + this((Comparator<? super K>) NATURAL_ORDER); + } + + /** + * Create a tree map ordered by {@code comparator}. This map's keys may only + * be null if {@code comparator} permits. + * + * @param comparator the comparator to order elements with, or {@code null} to + * use the natural ordering. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable + public LinkedHashTreeMap(Comparator<? super K> comparator) { + this.comparator = comparator != null + ? comparator + : (Comparator) NATURAL_ORDER; + this.header = new Node<K, V>(); + this.table = new Node[16]; // TODO: sizing/resizing policies + this.threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity + } + + @Override public int size() { + return size; + } + + @Override public V get(Object key) { + Node<K, V> node = findByObject(key); + return node != null ? node.value : null; + } + + @Override public boolean containsKey(Object key) { + return findByObject(key) != null; + } + + @Override public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("key == null"); + } + Node<K, V> created = find(key, true); + V result = created.value; + created.value = value; + return result; + } + + @Override public void clear() { + Arrays.fill(table, null); + size = 0; + modCount++; + + // Clear all links to help GC + Node<K, V> header = this.header; + for (Node<K, V> e = header.next; e != header; ) { + Node<K, V> next = e.next; + e.next = e.prev = null; + e = next; + } + + header.next = header.prev = header; + } + + @Override public V remove(Object key) { + Node<K, V> node = removeInternalByKey(key); + return node != null ? node.value : null; + } + + /** + * Returns the node at or adjacent to the given key, creating it if requested. + * + * @throws ClassCastException if {@code key} and the tree's keys aren't + * mutually comparable. + */ + Node<K, V> find(K key, boolean create) { + Comparator<? super K> comparator = this.comparator; + Node<K, V>[] table = this.table; + int hash = secondaryHash(key.hashCode()); + int index = hash & (table.length - 1); + Node<K, V> nearest = table[index]; + int comparison = 0; + + if (nearest != null) { + // Micro-optimization: avoid polymorphic calls to Comparator.compare(). + @SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble. + Comparable<Object> comparableKey = (comparator == NATURAL_ORDER) + ? (Comparable<Object>) key + : null; + + while (true) { + comparison = (comparableKey != null) + ? comparableKey.compareTo(nearest.key) + : comparator.compare(key, nearest.key); + + // We found the requested key. + if (comparison == 0) { + return nearest; + } + + // If it exists, the key is in a subtree. Go deeper. + Node<K, V> child = (comparison < 0) ? nearest.left : nearest.right; + if (child == null) { + break; + } + + nearest = child; + } + } + + // The key doesn't exist in this tree. + if (!create) { + return null; + } + + // Create the node and add it to the tree or the table. + Node<K, V> header = this.header; + Node<K, V> created; + if (nearest == null) { + // Check that the value is comparable if we didn't do any comparisons. + if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) { + throw new ClassCastException(key.getClass().getName() + " is not Comparable"); + } + created = new Node<K, V>(nearest, key, hash, header, header.prev); + table[index] = created; + } else { + created = new Node<K, V>(nearest, key, hash, header, header.prev); + if (comparison < 0) { // nearest.key is higher + nearest.left = created; + } else { // comparison > 0, nearest.key is lower + nearest.right = created; + } + rebalance(nearest, true); + } + + if (size++ > threshold) { + doubleCapacity(); + } + modCount++; + + return created; + } + + @SuppressWarnings("unchecked") + Node<K, V> findByObject(Object key) { + try { + return key != null ? find((K) key, false) : null; + } catch (ClassCastException e) { + return null; + } + } + + /** + * Returns this map's entry that has the same key and value as {@code + * entry}, or null if this map has no such entry. + * + * <p>This method uses the comparator for key equality rather than {@code + * equals}. If this map's comparator isn't consistent with equals (such as + * {@code String.CASE_INSENSITIVE_ORDER}), then {@code remove()} and {@code + * contains()} will violate the collections API. + */ + Node<K, V> findByEntry(Entry<?, ?> entry) { + Node<K, V> mine = findByObject(entry.getKey()); + boolean valuesEqual = mine != null && equal(mine.value, entry.getValue()); + return valuesEqual ? mine : null; + } + + private boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Applies a supplemental hash function to a given hashCode, which defends + * against poor quality hash functions. This is critical because HashMap + * uses power-of-two length hash tables, that otherwise encounter collisions + * for hashCodes that do not differ in lower or upper bits. + */ + private static int secondaryHash(int h) { + // Doug Lea's supplemental hash function + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); + } + + /** + * Removes {@code node} from this tree, rearranging the tree's structure as + * necessary. + * + * @param unlink true to also unlink this node from the iteration linked list. + */ + void removeInternal(Node<K, V> node, boolean unlink) { + if (unlink) { + node.prev.next = node.next; + node.next.prev = node.prev; + node.next = node.prev = null; // Help the GC (for performance) + } + + Node<K, V> left = node.left; + Node<K, V> right = node.right; + Node<K, V> originalParent = node.parent; + if (left != null && right != null) { + + /* + * To remove a node with both left and right subtrees, move an + * adjacent node from one of those subtrees into this node's place. + * + * Removing the adjacent node may change this node's subtrees. This + * node may no longer have two subtrees once the adjacent node is + * gone! + */ + + Node<K, V> adjacent = (left.height > right.height) ? left.last() : right.first(); + removeInternal(adjacent, false); // takes care of rebalance and size-- + + int leftHeight = 0; + left = node.left; + if (left != null) { + leftHeight = left.height; + adjacent.left = left; + left.parent = adjacent; + node.left = null; + } + int rightHeight = 0; + right = node.right; + if (right != null) { + rightHeight = right.height; + adjacent.right = right; + right.parent = adjacent; + node.right = null; + } + adjacent.height = Math.max(leftHeight, rightHeight) + 1; + replaceInParent(node, adjacent); + return; + } else if (left != null) { + replaceInParent(node, left); + node.left = null; + } else if (right != null) { + replaceInParent(node, right); + node.right = null; + } else { + replaceInParent(node, null); + } + + rebalance(originalParent, false); + size--; + modCount++; + } + + Node<K, V> removeInternalByKey(Object key) { + Node<K, V> node = findByObject(key); + if (node != null) { + removeInternal(node, true); + } + return node; + } + + private void replaceInParent(Node<K, V> node, Node<K, V> replacement) { + Node<K, V> parent = node.parent; + node.parent = null; + if (replacement != null) { + replacement.parent = parent; + } + + if (parent != null) { + if (parent.left == node) { + parent.left = replacement; + } else { + assert (parent.right == node); + parent.right = replacement; + } + } else { + int index = node.hash & (table.length - 1); + table[index] = replacement; + } + } + + /** + * Rebalances the tree by making any AVL rotations necessary between the + * newly-unbalanced node and the tree's root. + * + * @param insert true if the node was unbalanced by an insert; false if it + * was by a removal. + */ + private void rebalance(Node<K, V> unbalanced, boolean insert) { + for (Node<K, V> node = unbalanced; node != null; node = node.parent) { + Node<K, V> left = node.left; + Node<K, V> right = node.right; + int leftHeight = left != null ? left.height : 0; + int rightHeight = right != null ? right.height : 0; + + int delta = leftHeight - rightHeight; + if (delta == -2) { + Node<K, V> rightLeft = right.left; + Node<K, V> rightRight = right.right; + int rightRightHeight = rightRight != null ? rightRight.height : 0; + int rightLeftHeight = rightLeft != null ? rightLeft.height : 0; + + int rightDelta = rightLeftHeight - rightRightHeight; + if (rightDelta == -1 || (rightDelta == 0 && !insert)) { + rotateLeft(node); // AVL right right + } else { + assert (rightDelta == 1); + rotateRight(right); // AVL right left + rotateLeft(node); + } + if (insert) { + break; // no further rotations will be necessary + } + + } else if (delta == 2) { + Node<K, V> leftLeft = left.left; + Node<K, V> leftRight = left.right; + int leftRightHeight = leftRight != null ? leftRight.height : 0; + int leftLeftHeight = leftLeft != null ? leftLeft.height : 0; + + int leftDelta = leftLeftHeight - leftRightHeight; + if (leftDelta == 1 || (leftDelta == 0 && !insert)) { + rotateRight(node); // AVL left left + } else { + assert (leftDelta == -1); + rotateLeft(left); // AVL left right + rotateRight(node); + } + if (insert) { + break; // no further rotations will be necessary + } + + } else if (delta == 0) { + node.height = leftHeight + 1; // leftHeight == rightHeight + if (insert) { + break; // the insert caused balance, so rebalancing is done! + } + + } else { + assert (delta == -1 || delta == 1); + node.height = Math.max(leftHeight, rightHeight) + 1; + if (!insert) { + break; // the height hasn't changed, so rebalancing is done! + } + } + } + } + + /** + * Rotates the subtree so that its root's right child is the new root. + */ + private void rotateLeft(Node<K, V> root) { + Node<K, V> left = root.left; + Node<K, V> pivot = root.right; + Node<K, V> pivotLeft = pivot.left; + Node<K, V> pivotRight = pivot.right; + + // move the pivot's left child to the root's right + root.right = pivotLeft; + if (pivotLeft != null) { + pivotLeft.parent = root; + } + + replaceInParent(root, pivot); + + // move the root to the pivot's left + pivot.left = root; + root.parent = pivot; + + // fix heights + root.height = Math.max(left != null ? left.height : 0, + pivotLeft != null ? pivotLeft.height : 0) + 1; + pivot.height = Math.max(root.height, + pivotRight != null ? pivotRight.height : 0) + 1; + } + + /** + * Rotates the subtree so that its root's left child is the new root. + */ + private void rotateRight(Node<K, V> root) { + Node<K, V> pivot = root.left; + Node<K, V> right = root.right; + Node<K, V> pivotLeft = pivot.left; + Node<K, V> pivotRight = pivot.right; + + // move the pivot's right child to the root's left + root.left = pivotRight; + if (pivotRight != null) { + pivotRight.parent = root; + } + + replaceInParent(root, pivot); + + // move the root to the pivot's right + pivot.right = root; + root.parent = pivot; + + // fixup heights + root.height = Math.max(right != null ? right.height : 0, + pivotRight != null ? pivotRight.height : 0) + 1; + pivot.height = Math.max(root.height, + pivotLeft != null ? pivotLeft.height : 0) + 1; + } + + private EntrySet entrySet; + private KeySet keySet; + + @Override public Set<Entry<K, V>> entrySet() { + EntrySet result = entrySet; + return result != null ? result : (entrySet = new EntrySet()); + } + + @Override public Set<K> keySet() { + KeySet result = keySet; + return result != null ? result : (keySet = new KeySet()); + } + + static final class Node<K, V> implements Entry<K, V> { + Node<K, V> parent; + Node<K, V> left; + Node<K, V> right; + Node<K, V> next; + Node<K, V> prev; + final K key; + final int hash; + V value; + int height; + + /** Create the header entry */ + Node() { + key = null; + hash = -1; + next = prev = this; + } + + /** Create a regular entry */ + Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) { + this.parent = parent; + this.key = key; + this.hash = hash; + this.height = 1; + this.next = next; + this.prev = prev; + prev.next = this; + next.prev = this; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V oldValue = this.value; + this.value = value; + return oldValue; + } + + @SuppressWarnings("rawtypes") + @Override public boolean equals(Object o) { + if (o instanceof Entry) { + Entry other = (Entry) o; + return (key == null ? other.getKey() == null : key.equals(other.getKey())) + && (value == null ? other.getValue() == null : value.equals(other.getValue())); + } + return false; + } + + @Override public int hashCode() { + return (key == null ? 0 : key.hashCode()) + ^ (value == null ? 0 : value.hashCode()); + } + + @Override public String toString() { + return key + "=" + value; + } + + /** + * Returns the first node in this subtree. + */ + public Node<K, V> first() { + Node<K, V> node = this; + Node<K, V> child = node.left; + while (child != null) { + node = child; + child = node.left; + } + return node; + } + + /** + * Returns the last node in this subtree. + */ + public Node<K, V> last() { + Node<K, V> node = this; + Node<K, V> child = node.right; + while (child != null) { + node = child; + child = node.right; + } + return node; + } + } + + private void doubleCapacity() { + table = doubleCapacity(table); + threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity + } + + /** + * Returns a new array containing the same nodes as {@code oldTable}, but with + * twice as many trees, each of (approximately) half the previous size. + */ + static <K, V> Node<K, V>[] doubleCapacity(Node<K, V>[] oldTable) { + // TODO: don't do anything if we're already at MAX_CAPACITY + int oldCapacity = oldTable.length; + @SuppressWarnings("unchecked") // Arrays and generics don't get along. + Node<K, V>[] newTable = new Node[oldCapacity * 2]; + AvlIterator<K, V> iterator = new AvlIterator<K, V>(); + AvlBuilder<K, V> leftBuilder = new AvlBuilder<K, V>(); + AvlBuilder<K, V> rightBuilder = new AvlBuilder<K, V>(); + + // Split each tree into two trees. + for (int i = 0; i < oldCapacity; i++) { + Node<K, V> root = oldTable[i]; + if (root == null) { + continue; + } + + // Compute the sizes of the left and right trees. + iterator.reset(root); + int leftSize = 0; + int rightSize = 0; + for (Node<K, V> node; (node = iterator.next()) != null; ) { + if ((node.hash & oldCapacity) == 0) { + leftSize++; + } else { + rightSize++; + } + } + + // Split the tree into two. + leftBuilder.reset(leftSize); + rightBuilder.reset(rightSize); + iterator.reset(root); + for (Node<K, V> node; (node = iterator.next()) != null; ) { + if ((node.hash & oldCapacity) == 0) { + leftBuilder.add(node); + } else { + rightBuilder.add(node); + } + } + + // Populate the enlarged array with these new roots. + newTable[i] = leftSize > 0 ? leftBuilder.root() : null; + newTable[i + oldCapacity] = rightSize > 0 ? rightBuilder.root() : null; + } + return newTable; + } + + /** + * Walks an AVL tree in iteration order. Once a node has been returned, its + * left, right and parent links are <strong>no longer used</strong>. For this + * reason it is safe to transform these links as you walk a tree. + * + * <p><strong>Warning:</strong> this iterator is destructive. It clears the + * parent node of all nodes in the tree. It is an error to make a partial + * iteration of a tree. + */ + static class AvlIterator<K, V> { + /** This stack is a singly linked list, linked by the 'parent' field. */ + private Node<K, V> stackTop; + + void reset(Node<K, V> root) { + Node<K, V> stackTop = null; + for (Node<K, V> n = root; n != null; n = n.left) { + n.parent = stackTop; + stackTop = n; // Stack push. + } + this.stackTop = stackTop; + } + + public Node<K, V> next() { + Node<K, V> stackTop = this.stackTop; + if (stackTop == null) { + return null; + } + Node<K, V> result = stackTop; + stackTop = result.parent; + result.parent = null; + for (Node<K, V> n = result.right; n != null; n = n.left) { + n.parent = stackTop; + stackTop = n; // Stack push. + } + this.stackTop = stackTop; + return result; + } + } + + /** + * Builds AVL trees of a predetermined size by accepting nodes of increasing + * value. To use: + * <ol> + * <li>Call {@link #reset} to initialize the target size <i>size</i>. + * <li>Call {@link #add} <i>size</i> times with increasing values. + * <li>Call {@link #root} to get the root of the balanced tree. + * </ol> + * + * <p>The returned tree will satisfy the AVL constraint: for every node + * <i>N</i>, the height of <i>N.left</i> and <i>N.right</i> is different by at + * most 1. It accomplishes this by omitting deepest-level leaf nodes when + * building trees whose size isn't a power of 2 minus 1. + * + * <p>Unlike rebuilding a tree from scratch, this approach requires no value + * comparisons. Using this class to create a tree of size <i>S</i> is + * {@code O(S)}. + */ + final static class AvlBuilder<K, V> { + /** This stack is a singly linked list, linked by the 'parent' field. */ + private Node<K, V> stack; + private int leavesToSkip; + private int leavesSkipped; + private int size; + + void reset(int targetSize) { + // compute the target tree size. This is a power of 2 minus one, like 15 or 31. + int treeCapacity = Integer.highestOneBit(targetSize) * 2 - 1; + leavesToSkip = treeCapacity - targetSize; + size = 0; + leavesSkipped = 0; + stack = null; + } + + void add(Node<K, V> node) { + node.left = node.parent = node.right = null; + node.height = 1; + + // Skip a leaf if necessary. + if (leavesToSkip > 0 && (size & 1) == 0) { + size++; + leavesToSkip--; + leavesSkipped++; + } + + node.parent = stack; + stack = node; // Stack push. + size++; + + // Skip a leaf if necessary. + if (leavesToSkip > 0 && (size & 1) == 0) { + size++; + leavesToSkip--; + leavesSkipped++; + } + + /* + * Combine 3 nodes into subtrees whenever the size is one less than a + * multiple of 4. For example we combine the nodes A, B, C into a + * 3-element tree with B as the root. + * + * Combine two subtrees and a spare single value whenever the size is one + * less than a multiple of 8. For example at 8 we may combine subtrees + * (A B C) and (E F G) with D as the root to form ((A B C) D (E F G)). + * + * Just as we combine single nodes when size nears a multiple of 4, and + * 3-element trees when size nears a multiple of 8, we combine subtrees of + * size (N-1) whenever the total size is 2N-1 whenever N is a power of 2. + */ + for (int scale = 4; (size & scale - 1) == scale - 1; scale *= 2) { + if (leavesSkipped == 0) { + // Pop right, center and left, then make center the top of the stack. + Node<K, V> right = stack; + Node<K, V> center = right.parent; + Node<K, V> left = center.parent; + center.parent = left.parent; + stack = center; + // Construct a tree. + center.left = left; + center.right = right; + center.height = right.height + 1; + left.parent = center; + right.parent = center; + } else if (leavesSkipped == 1) { + // Pop right and center, then make center the top of the stack. + Node<K, V> right = stack; + Node<K, V> center = right.parent; + stack = center; + // Construct a tree with no left child. + center.right = right; + center.height = right.height + 1; + right.parent = center; + leavesSkipped = 0; + } else if (leavesSkipped == 2) { + leavesSkipped = 0; + } + } + } + + Node<K, V> root() { + Node<K, V> stackTop = this.stack; + if (stackTop.parent != null) { + throw new IllegalStateException(); + } + return stackTop; + } + } + + private abstract class LinkedTreeMapIterator<T> implements Iterator<T> { + Node<K, V> next = header.next; + Node<K, V> lastReturned = null; + int expectedModCount = modCount; + + public final boolean hasNext() { + return next != header; + } + + final Node<K, V> nextNode() { + Node<K, V> e = next; + if (e == header) { + throw new NoSuchElementException(); + } + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + next = e.next; + return lastReturned = e; + } + + public final void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + removeInternal(lastReturned, true); + lastReturned = null; + expectedModCount = modCount; + } + } + + final class EntrySet extends AbstractSet<Entry<K, V>> { + @Override public int size() { + return size; + } + + @Override public Iterator<Entry<K, V>> iterator() { + return new LinkedTreeMapIterator<Entry<K, V>>() { + public Entry<K, V> next() { + return nextNode(); + } + }; + } + + @Override public boolean contains(Object o) { + return o instanceof Entry && findByEntry((Entry<?, ?>) o) != null; + } + + @Override public boolean remove(Object o) { + if (!(o instanceof Entry)) { + return false; + } + + Node<K, V> node = findByEntry((Entry<?, ?>) o); + if (node == null) { + return false; + } + removeInternal(node, true); + return true; + } + + @Override public void clear() { + LinkedHashTreeMap.this.clear(); + } + } + + final class KeySet extends AbstractSet<K> { + @Override public int size() { + return size; + } + + @Override public Iterator<K> iterator() { + return new LinkedTreeMapIterator<K>() { + public K next() { + return nextNode().key; + } + }; + } + + @Override public boolean contains(Object o) { + return containsKey(o); + } + + @Override public boolean remove(Object key) { + return removeInternalByKey(key) != null; + } + + @Override public void clear() { + LinkedHashTreeMap.this.clear(); + } + } + + /** + * If somebody is unlucky enough to have to serialize one of these, serialize + * it as a LinkedHashMap so that they won't need Gson on the other side to + * deserialize it. Using serialization defeats our DoS defence, so most apps + * shouldn't use it. + */ + private Object writeReplace() throws ObjectStreamException { + return new LinkedHashMap<K, V>(this); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java new file mode 100644 index 00000000..c2c84802 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2012 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 com.google.gson.internal; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses + * insertion order for iteration order. Comparison order is only used as an + * optimization for efficient insertion and removal. + * + * <p>This implementation was derived from Android 4.1's TreeMap class. + */ +public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Serializable { + @SuppressWarnings({ "unchecked", "rawtypes" }) // to avoid Comparable<Comparable<Comparable<...>>> + private static final Comparator<Comparable> NATURAL_ORDER = new Comparator<Comparable>() { + public int compare(Comparable a, Comparable b) { + return a.compareTo(b); + } + }; + + Comparator<? super K> comparator; + Node<K, V> root; + int size = 0; + int modCount = 0; + + // Used to preserve iteration order + final Node<K, V> header = new Node<K, V>(); + + /** + * Create a natural order, empty tree map whose keys must be mutually + * comparable and non-null. + */ + @SuppressWarnings("unchecked") // unsafe! this assumes K is comparable + public LinkedTreeMap() { + this((Comparator<? super K>) NATURAL_ORDER); + } + + /** + * Create a tree map ordered by {@code comparator}. This map's keys may only + * be null if {@code comparator} permits. + * + * @param comparator the comparator to order elements with, or {@code null} to + * use the natural ordering. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable + public LinkedTreeMap(Comparator<? super K> comparator) { + this.comparator = comparator != null + ? comparator + : (Comparator) NATURAL_ORDER; + } + + @Override public int size() { + return size; + } + + @Override public V get(Object key) { + Node<K, V> node = findByObject(key); + return node != null ? node.value : null; + } + + @Override public boolean containsKey(Object key) { + return findByObject(key) != null; + } + + @Override public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("key == null"); + } + Node<K, V> created = find(key, true); + V result = created.value; + created.value = value; + return result; + } + + @Override public void clear() { + root = null; + size = 0; + modCount++; + + // Clear iteration order + Node<K, V> header = this.header; + header.next = header.prev = header; + } + + @Override public V remove(Object key) { + Node<K, V> node = removeInternalByKey(key); + return node != null ? node.value : null; + } + + /** + * Returns the node at or adjacent to the given key, creating it if requested. + * + * @throws ClassCastException if {@code key} and the tree's keys aren't + * mutually comparable. + */ + Node<K, V> find(K key, boolean create) { + Comparator<? super K> comparator = this.comparator; + Node<K, V> nearest = root; + int comparison = 0; + + if (nearest != null) { + // Micro-optimization: avoid polymorphic calls to Comparator.compare(). + @SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble. + Comparable<Object> comparableKey = (comparator == NATURAL_ORDER) + ? (Comparable<Object>) key + : null; + + while (true) { + comparison = (comparableKey != null) + ? comparableKey.compareTo(nearest.key) + : comparator.compare(key, nearest.key); + + // We found the requested key. + if (comparison == 0) { + return nearest; + } + + // If it exists, the key is in a subtree. Go deeper. + Node<K, V> child = (comparison < 0) ? nearest.left : nearest.right; + if (child == null) { + break; + } + + nearest = child; + } + } + + // The key doesn't exist in this tree. + if (!create) { + return null; + } + + // Create the node and add it to the tree or the table. + Node<K, V> header = this.header; + Node<K, V> created; + if (nearest == null) { + // Check that the value is comparable if we didn't do any comparisons. + if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) { + throw new ClassCastException(key.getClass().getName() + " is not Comparable"); + } + created = new Node<K, V>(nearest, key, header, header.prev); + root = created; + } else { + created = new Node<K, V>(nearest, key, header, header.prev); + if (comparison < 0) { // nearest.key is higher + nearest.left = created; + } else { // comparison > 0, nearest.key is lower + nearest.right = created; + } + rebalance(nearest, true); + } + size++; + modCount++; + + return created; + } + + @SuppressWarnings("unchecked") + Node<K, V> findByObject(Object key) { + try { + return key != null ? find((K) key, false) : null; + } catch (ClassCastException e) { + return null; + } + } + + /** + * Returns this map's entry that has the same key and value as {@code + * entry}, or null if this map has no such entry. + * + * <p>This method uses the comparator for key equality rather than {@code + * equals}. If this map's comparator isn't consistent with equals (such as + * {@code String.CASE_INSENSITIVE_ORDER}), then {@code remove()} and {@code + * contains()} will violate the collections API. + */ + Node<K, V> findByEntry(Entry<?, ?> entry) { + Node<K, V> mine = findByObject(entry.getKey()); + boolean valuesEqual = mine != null && equal(mine.value, entry.getValue()); + return valuesEqual ? mine : null; + } + + private boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Removes {@code node} from this tree, rearranging the tree's structure as + * necessary. + * + * @param unlink true to also unlink this node from the iteration linked list. + */ + void removeInternal(Node<K, V> node, boolean unlink) { + if (unlink) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + Node<K, V> left = node.left; + Node<K, V> right = node.right; + Node<K, V> originalParent = node.parent; + if (left != null && right != null) { + + /* + * To remove a node with both left and right subtrees, move an + * adjacent node from one of those subtrees into this node's place. + * + * Removing the adjacent node may change this node's subtrees. This + * node may no longer have two subtrees once the adjacent node is + * gone! + */ + + Node<K, V> adjacent = (left.height > right.height) ? left.last() : right.first(); + removeInternal(adjacent, false); // takes care of rebalance and size-- + + int leftHeight = 0; + left = node.left; + if (left != null) { + leftHeight = left.height; + adjacent.left = left; + left.parent = adjacent; + node.left = null; + } + + int rightHeight = 0; + right = node.right; + if (right != null) { + rightHeight = right.height; + adjacent.right = right; + right.parent = adjacent; + node.right = null; + } + + adjacent.height = Math.max(leftHeight, rightHeight) + 1; + replaceInParent(node, adjacent); + return; + } else if (left != null) { + replaceInParent(node, left); + node.left = null; + } else if (right != null) { + replaceInParent(node, right); + node.right = null; + } else { + replaceInParent(node, null); + } + + rebalance(originalParent, false); + size--; + modCount++; + } + + Node<K, V> removeInternalByKey(Object key) { + Node<K, V> node = findByObject(key); + if (node != null) { + removeInternal(node, true); + } + return node; + } + + private void replaceInParent(Node<K, V> node, Node<K, V> replacement) { + Node<K, V> parent = node.parent; + node.parent = null; + if (replacement != null) { + replacement.parent = parent; + } + + if (parent != null) { + if (parent.left == node) { + parent.left = replacement; + } else { + assert (parent.right == node); + parent.right = replacement; + } + } else { + root = replacement; + } + } + + /** + * Rebalances the tree by making any AVL rotations necessary between the + * newly-unbalanced node and the tree's root. + * + * @param insert true if the node was unbalanced by an insert; false if it + * was by a removal. + */ + private void rebalance(Node<K, V> unbalanced, boolean insert) { + for (Node<K, V> node = unbalanced; node != null; node = node.parent) { + Node<K, V> left = node.left; + Node<K, V> right = node.right; + int leftHeight = left != null ? left.height : 0; + int rightHeight = right != null ? right.height : 0; + + int delta = leftHeight - rightHeight; + if (delta == -2) { + Node<K, V> rightLeft = right.left; + Node<K, V> rightRight = right.right; + int rightRightHeight = rightRight != null ? rightRight.height : 0; + int rightLeftHeight = rightLeft != null ? rightLeft.height : 0; + + int rightDelta = rightLeftHeight - rightRightHeight; + if (rightDelta == -1 || (rightDelta == 0 && !insert)) { + rotateLeft(node); // AVL right right + } else { + assert (rightDelta == 1); + rotateRight(right); // AVL right left + rotateLeft(node); + } + if (insert) { + break; // no further rotations will be necessary + } + + } else if (delta == 2) { + Node<K, V> leftLeft = left.left; + Node<K, V> leftRight = left.right; + int leftRightHeight = leftRight != null ? leftRight.height : 0; + int leftLeftHeight = leftLeft != null ? leftLeft.height : 0; + + int leftDelta = leftLeftHeight - leftRightHeight; + if (leftDelta == 1 || (leftDelta == 0 && !insert)) { + rotateRight(node); // AVL left left + } else { + assert (leftDelta == -1); + rotateLeft(left); // AVL left right + rotateRight(node); + } + if (insert) { + break; // no further rotations will be necessary + } + + } else if (delta == 0) { + node.height = leftHeight + 1; // leftHeight == rightHeight + if (insert) { + break; // the insert caused balance, so rebalancing is done! + } + + } else { + assert (delta == -1 || delta == 1); + node.height = Math.max(leftHeight, rightHeight) + 1; + if (!insert) { + break; // the height hasn't changed, so rebalancing is done! + } + } + } + } + + /** + * Rotates the subtree so that its root's right child is the new root. + */ + private void rotateLeft(Node<K, V> root) { + Node<K, V> left = root.left; + Node<K, V> pivot = root.right; + Node<K, V> pivotLeft = pivot.left; + Node<K, V> pivotRight = pivot.right; + + // move the pivot's left child to the root's right + root.right = pivotLeft; + if (pivotLeft != null) { + pivotLeft.parent = root; + } + + replaceInParent(root, pivot); + + // move the root to the pivot's left + pivot.left = root; + root.parent = pivot; + + // fix heights + root.height = Math.max(left != null ? left.height : 0, + pivotLeft != null ? pivotLeft.height : 0) + 1; + pivot.height = Math.max(root.height, + pivotRight != null ? pivotRight.height : 0) + 1; + } + + /** + * Rotates the subtree so that its root's left child is the new root. + */ + private void rotateRight(Node<K, V> root) { + Node<K, V> pivot = root.left; + Node<K, V> right = root.right; + Node<K, V> pivotLeft = pivot.left; + Node<K, V> pivotRight = pivot.right; + + // move the pivot's right child to the root's left + root.left = pivotRight; + if (pivotRight != null) { + pivotRight.parent = root; + } + + replaceInParent(root, pivot); + + // move the root to the pivot's right + pivot.right = root; + root.parent = pivot; + + // fixup heights + root.height = Math.max(right != null ? right.height : 0, + pivotRight != null ? pivotRight.height : 0) + 1; + pivot.height = Math.max(root.height, + pivotLeft != null ? pivotLeft.height : 0) + 1; + } + + private EntrySet entrySet; + private KeySet keySet; + + @Override public Set<Entry<K, V>> entrySet() { + EntrySet result = entrySet; + return result != null ? result : (entrySet = new EntrySet()); + } + + @Override public Set<K> keySet() { + KeySet result = keySet; + return result != null ? result : (keySet = new KeySet()); + } + + static final class Node<K, V> implements Entry<K, V> { + Node<K, V> parent; + Node<K, V> left; + Node<K, V> right; + Node<K, V> next; + Node<K, V> prev; + final K key; + V value; + int height; + + /** Create the header entry */ + Node() { + key = null; + next = prev = this; + } + + /** Create a regular entry */ + Node(Node<K, V> parent, K key, Node<K, V> next, Node<K, V> prev) { + this.parent = parent; + this.key = key; + this.height = 1; + this.next = next; + this.prev = prev; + prev.next = this; + next.prev = this; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V oldValue = this.value; + this.value = value; + return oldValue; + } + + @SuppressWarnings("rawtypes") + @Override public boolean equals(Object o) { + if (o instanceof Entry) { + Entry other = (Entry) o; + return (key == null ? other.getKey() == null : key.equals(other.getKey())) + && (value == null ? other.getValue() == null : value.equals(other.getValue())); + } + return false; + } + + @Override public int hashCode() { + return (key == null ? 0 : key.hashCode()) + ^ (value == null ? 0 : value.hashCode()); + } + + @Override public String toString() { + return key + "=" + value; + } + + /** + * Returns the first node in this subtree. + */ + public Node<K, V> first() { + Node<K, V> node = this; + Node<K, V> child = node.left; + while (child != null) { + node = child; + child = node.left; + } + return node; + } + + /** + * Returns the last node in this subtree. + */ + public Node<K, V> last() { + Node<K, V> node = this; + Node<K, V> child = node.right; + while (child != null) { + node = child; + child = node.right; + } + return node; + } + } + + private abstract class LinkedTreeMapIterator<T> implements Iterator<T> { + Node<K, V> next = header.next; + Node<K, V> lastReturned = null; + int expectedModCount = modCount; + + public final boolean hasNext() { + return next != header; + } + + final Node<K, V> nextNode() { + Node<K, V> e = next; + if (e == header) { + throw new NoSuchElementException(); + } + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + next = e.next; + return lastReturned = e; + } + + public final void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + removeInternal(lastReturned, true); + lastReturned = null; + expectedModCount = modCount; + } + } + + class EntrySet extends AbstractSet<Entry<K, V>> { + @Override public int size() { + return size; + } + + @Override public Iterator<Entry<K, V>> iterator() { + return new LinkedTreeMapIterator<Entry<K, V>>() { + public Entry<K, V> next() { + return nextNode(); + } + }; + } + + @Override public boolean contains(Object o) { + return o instanceof Entry && findByEntry((Entry<?, ?>) o) != null; + } + + @Override public boolean remove(Object o) { + if (!(o instanceof Entry)) { + return false; + } + + Node<K, V> node = findByEntry((Entry<?, ?>) o); + if (node == null) { + return false; + } + removeInternal(node, true); + return true; + } + + @Override public void clear() { + LinkedTreeMap.this.clear(); + } + } + + final class KeySet extends AbstractSet<K> { + @Override public int size() { + return size; + } + + @Override public Iterator<K> iterator() { + return new LinkedTreeMapIterator<K>() { + public K next() { + return nextNode().key; + } + }; + } + + @Override public boolean contains(Object o) { + return containsKey(o); + } + + @Override public boolean remove(Object key) { + return removeInternalByKey(key) != null; + } + + @Override public void clear() { + LinkedTreeMap.this.clear(); + } + } + + /** + * If somebody is unlucky enough to have to serialize one of these, serialize + * it as a LinkedHashMap so that they won't need Gson on the other side to + * deserialize it. Using serialization defeats our DoS defence, so most apps + * shouldn't use it. + */ + private Object writeReplace() throws ObjectStreamException { + return new LinkedHashMap<K, V>(this); + } +}
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java b/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java new file mode 100644 index 00000000..6ef20607 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 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 com.google.gson.internal; + +/** + * Defines a generic object construction factory. The purpose of this class + * is to construct a default instance of a class that can be used for object + * navigation while deserialization from its JSON representation. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public interface ObjectConstructor<T> { + + /** + * Returns a new instance. + */ + public T construct(); +}
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/internal/Primitives.java b/gson/src/main/java/com/google/gson/internal/Primitives.java new file mode 100644 index 00000000..a98f6242 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/Primitives.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2008 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 com.google.gson.internal; + + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Contains static utility methods pertaining to primitive types and their + * corresponding wrapper types. + * + * @author Kevin Bourrillion + */ +public final class Primitives { + private Primitives() { + throw new UnsupportedOperationException(); + } + + /** A map from primitive types to their corresponding wrapper types. */ + private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE; + + /** A map from wrapper types to their corresponding primitive types. */ + private static final Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE_TYPE; + + // Sad that we can't use a BiMap. :( + + static { + Map<Class<?>, Class<?>> primToWrap = new HashMap<Class<?>, Class<?>>(16); + Map<Class<?>, Class<?>> wrapToPrim = new HashMap<Class<?>, Class<?>>(16); + + add(primToWrap, wrapToPrim, boolean.class, Boolean.class); + add(primToWrap, wrapToPrim, byte.class, Byte.class); + add(primToWrap, wrapToPrim, char.class, Character.class); + add(primToWrap, wrapToPrim, double.class, Double.class); + add(primToWrap, wrapToPrim, float.class, Float.class); + add(primToWrap, wrapToPrim, int.class, Integer.class); + add(primToWrap, wrapToPrim, long.class, Long.class); + add(primToWrap, wrapToPrim, short.class, Short.class); + add(primToWrap, wrapToPrim, void.class, Void.class); + + PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap); + WRAPPER_TO_PRIMITIVE_TYPE = Collections.unmodifiableMap(wrapToPrim); + } + + private static void add(Map<Class<?>, Class<?>> forward, + Map<Class<?>, Class<?>> backward, Class<?> key, Class<?> value) { + forward.put(key, value); + backward.put(value, key); + } + + /** + * Returns true if this type is a primitive. + */ + public static boolean isPrimitive(Type type) { + return PRIMITIVE_TO_WRAPPER_TYPE.containsKey(type); + } + + /** + * Returns {@code true} if {@code type} is one of the nine + * primitive-wrapper types, such as {@link Integer}. + * + * @see Class#isPrimitive + */ + public static boolean isWrapperType(Type type) { + return WRAPPER_TO_PRIMITIVE_TYPE.containsKey( + $Gson$Preconditions.checkNotNull(type)); + } + + /** + * Returns the corresponding wrapper type of {@code type} if it is a primitive + * type; otherwise returns {@code type} itself. Idempotent. + * <pre> + * wrap(int.class) == Integer.class + * wrap(Integer.class) == Integer.class + * wrap(String.class) == String.class + * </pre> + */ + public static <T> Class<T> wrap(Class<T> type) { + // cast is safe: long.class and Long.class are both of type Class<Long> + @SuppressWarnings("unchecked") + Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE.get( + $Gson$Preconditions.checkNotNull(type)); + return (wrapped == null) ? type : wrapped; + } + + /** + * Returns the corresponding primitive type of {@code type} if it is a + * wrapper type; otherwise returns {@code type} itself. Idempotent. + * <pre> + * unwrap(Integer.class) == int.class + * unwrap(int.class) == int.class + * unwrap(String.class) == String.class + * </pre> + */ + public static <T> Class<T> unwrap(Class<T> type) { + // cast is safe: long.class and Long.class are both of type Class<Long> + @SuppressWarnings("unchecked") + Class<T> unwrapped = (Class<T>) WRAPPER_TO_PRIMITIVE_TYPE.get( + $Gson$Preconditions.checkNotNull(type)); + return (unwrapped == null) ? type : unwrapped; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/src/main/java/com/google/gson/internal/Streams.java new file mode 100644 index 00000000..7f00f11d --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/Streams.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 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 com.google.gson.internal; + +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.internal.bind.TypeAdapters; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.MalformedJsonException; +import java.io.EOFException; +import java.io.IOException; +import java.io.Writer; + +/** + * Reads and writes GSON parse trees over streams. + */ +public final class Streams { + private Streams() { + throw new UnsupportedOperationException(); + } + + /** + * Takes a reader in any state and returns the next value as a JsonElement. + */ + public static JsonElement parse(JsonReader reader) throws JsonParseException { + boolean isEmpty = true; + try { + reader.peek(); + isEmpty = false; + return TypeAdapters.JSON_ELEMENT.read(reader); + } catch (EOFException e) { + /* + * For compatibility with JSON 1.5 and earlier, we return a JsonNull for + * empty documents instead of throwing. + */ + if (isEmpty) { + return JsonNull.INSTANCE; + } + // The stream ended prematurely so it is likely a syntax error. + throw new JsonSyntaxException(e); + } catch (MalformedJsonException e) { + throw new JsonSyntaxException(e); + } catch (IOException e) { + throw new JsonIOException(e); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + /** + * Writes the JSON element to the writer, recursively. + */ + public static void write(JsonElement element, JsonWriter writer) throws IOException { + TypeAdapters.JSON_ELEMENT.write(writer, element); + } + + @SuppressWarnings("resource") + public static Writer writerForAppendable(Appendable appendable) { + return appendable instanceof Writer ? (Writer) appendable : new AppendableWriter(appendable); + } + + /** + * Adapts an {@link Appendable} so it can be passed anywhere a {@link Writer} + * is used. + */ + private static final class AppendableWriter extends Writer { + private final Appendable appendable; + private final CurrentWrite currentWrite = new CurrentWrite(); + + private AppendableWriter(Appendable appendable) { + this.appendable = appendable; + } + + @Override public void write(char[] chars, int offset, int length) throws IOException { + currentWrite.chars = chars; + appendable.append(currentWrite, offset, offset + length); + } + + @Override public void write(int i) throws IOException { + appendable.append((char) i); + } + + @Override public void flush() {} + @Override public void close() {} + + /** + * A mutable char sequence pointing at a single char[]. + */ + static class CurrentWrite implements CharSequence { + char[] chars; + public int length() { + return chars.length; + } + public char charAt(int i) { + return chars[i]; + } + public CharSequence subSequence(int start, int end) { + return new String(chars, start, end - start); + } + } + } + +} diff --git a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java new file mode 100644 index 00000000..fce0be37 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal; + +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Do sneaky things to allocate objects without invoking their constructors. + * + * @author Joel Leitch + * @author Jesse Wilson + */ +public abstract class UnsafeAllocator { + public abstract <T> T newInstance(Class<T> c) throws Exception; + + public static UnsafeAllocator create() { + // try JVM + // public class Unsafe { + // public Object allocateInstance(Class<?> type); + // } + try { + Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); + Field f = unsafeClass.getDeclaredField("theUnsafe"); + f.setAccessible(true); + final Object unsafe = f.get(null); + final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); + return new UnsafeAllocator() { + @Override + @SuppressWarnings("unchecked") + public <T> T newInstance(Class<T> c) throws Exception { + return (T) allocateInstance.invoke(unsafe, c); + } + }; + } catch (Exception ignored) { + } + + // try dalvikvm, post-gingerbread + // public class ObjectStreamClass { + // private static native int getConstructorId(Class<?> c); + // private static native Object newInstance(Class<?> instantiationClass, int methodId); + // } + try { + Method getConstructorId = ObjectStreamClass.class + .getDeclaredMethod("getConstructorId", Class.class); + getConstructorId.setAccessible(true); + final int constructorId = (Integer) getConstructorId.invoke(null, Object.class); + final Method newInstance = ObjectStreamClass.class + .getDeclaredMethod("newInstance", Class.class, int.class); + newInstance.setAccessible(true); + return new UnsafeAllocator() { + @Override + @SuppressWarnings("unchecked") + public <T> T newInstance(Class<T> c) throws Exception { + return (T) newInstance.invoke(null, c, constructorId); + } + }; + } catch (Exception ignored) { + } + + // try dalvikvm, pre-gingerbread + // public class ObjectInputStream { + // private static native Object newInstance( + // Class<?> instantiationClass, Class<?> constructorClass); + // } + try { + final Method newInstance = ObjectInputStream.class + .getDeclaredMethod("newInstance", Class.class, Class.class); + newInstance.setAccessible(true); + return new UnsafeAllocator() { + @Override + @SuppressWarnings("unchecked") + public <T> T newInstance(Class<T> c) throws Exception { + return (T) newInstance.invoke(null, c, Object.class); + } + }; + } catch (Exception ignored) { + } + + // give up + return new UnsafeAllocator() { + @Override + public <T> T newInstance(Class<T> c) { + throw new UnsupportedOperationException("Cannot allocate " + c); + } + }; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java new file mode 100644 index 00000000..55d7e309 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Adapt an array of objects. + */ +public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> { + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings({"unchecked", "rawtypes"}) + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Type type = typeToken.getType(); + if (!(type instanceof GenericArrayType || type instanceof Class && ((Class<?>) type).isArray())) { + return null; + } + + Type componentType = $Gson$Types.getArrayComponentType(type); + TypeAdapter<?> componentTypeAdapter = gson.getAdapter(TypeToken.get(componentType)); + return new ArrayTypeAdapter( + gson, componentTypeAdapter, $Gson$Types.getRawType(componentType)); + } + }; + + private final Class<E> componentType; + private final TypeAdapter<E> componentTypeAdapter; + + public ArrayTypeAdapter(Gson context, TypeAdapter<E> componentTypeAdapter, Class<E> componentType) { + this.componentTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<E>(context, componentTypeAdapter, componentType); + this.componentType = componentType; + } + + public Object read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + List<E> list = new ArrayList<E>(); + in.beginArray(); + while (in.hasNext()) { + E instance = componentTypeAdapter.read(in); + list.add(instance); + } + in.endArray(); + Object array = Array.newInstance(componentType, list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(array, i, list.get(i)); + } + return array; + } + + @SuppressWarnings("unchecked") + @Override public void write(JsonWriter out, Object array) throws IOException { + if (array == null) { + out.nullValue(); + return; + } + + out.beginArray(); + for (int i = 0, length = Array.getLength(array); i < length; i++) { + E value = (E) Array.get(array, i); + componentTypeAdapter.write(out, value); + } + out.endArray(); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java new file mode 100644 index 00000000..0b95445a --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collection; + +/** + * Adapt a homogeneous collection of objects. + */ +public final class CollectionTypeAdapterFactory implements TypeAdapterFactory { + private final ConstructorConstructor constructorConstructor; + + public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) { + this.constructorConstructor = constructorConstructor; + } + + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Type type = typeToken.getType(); + + Class<? super T> rawType = typeToken.getRawType(); + if (!Collection.class.isAssignableFrom(rawType)) { + return null; + } + + Type elementType = $Gson$Types.getCollectionElementType(type, rawType); + TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)); + ObjectConstructor<T> constructor = constructorConstructor.get(typeToken); + + @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter + TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter, constructor); + return result; + } + + private static final class Adapter<E> extends TypeAdapter<Collection<E>> { + private final TypeAdapter<E> elementTypeAdapter; + private final ObjectConstructor<? extends Collection<E>> constructor; + + public Adapter(Gson context, Type elementType, + TypeAdapter<E> elementTypeAdapter, + ObjectConstructor<? extends Collection<E>> constructor) { + this.elementTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType); + this.constructor = constructor; + } + + public Collection<E> read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + Collection<E> collection = constructor.construct(); + in.beginArray(); + while (in.hasNext()) { + E instance = elementTypeAdapter.read(in); + collection.add(instance); + } + in.endArray(); + return collection; + } + + public void write(JsonWriter out, Collection<E> collection) throws IOException { + if (collection == null) { + out.nullValue(); + return; + } + + out.beginArray(); + for (E element : collection) { + elementTypeAdapter.write(out, element); + } + out.endArray(); + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java new file mode 100644 index 00000000..f2571724 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Adapter for Date. Although this class appears stateless, it is not. + * DateFormat captures its time zone and locale when it is created, which gives + * this class state. DateFormat isn't thread safe either, so this class has + * to synchronize its read and write methods. + */ +public final class DateTypeAdapter extends TypeAdapter<Date> { + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return typeToken.getRawType() == Date.class ? (TypeAdapter<T>) new DateTypeAdapter() : null; + } + }; + + private final DateFormat enUsFormat + = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US); + private final DateFormat localFormat + = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT); + private final DateFormat iso8601Format = buildIso8601Format(); + + private static DateFormat buildIso8601Format() { + DateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC")); + return iso8601Format; + } + + @Override public Date read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return deserializeToDate(in.nextString()); + } + + private synchronized Date deserializeToDate(String json) { + try { + return localFormat.parse(json); + } catch (ParseException ignored) { + } + try { + return enUsFormat.parse(json); + } catch (ParseException ignored) { + } + try { + return iso8601Format.parse(json); + } catch (ParseException e) { + throw new JsonSyntaxException(json, e); + } + } + + @Override public synchronized void write(JsonWriter out, Date value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + String dateFormatAsString = enUsFormat.format(value); + out.value(dateFormatAsString); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java new file mode 100644 index 00000000..7e0be28f --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.reflect.TypeToken; + +/** + * Given a type T, looks for the annotation {@link JsonAdapter} and uses an instance of the + * specified class as the default type adapter. + * + * @since 2.3 + */ +public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory { + + private final ConstructorConstructor constructorConstructor; + + public JsonAdapterAnnotationTypeAdapterFactory(ConstructorConstructor constructorConstructor) { + this.constructorConstructor = constructorConstructor; + } + + @SuppressWarnings("unchecked") + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) { + JsonAdapter annotation = targetType.getRawType().getAnnotation(JsonAdapter.class); + if (annotation == null) { + return null; + } + return (TypeAdapter<T>) getTypeAdapter(constructorConstructor, gson, targetType, annotation); + } + + @SuppressWarnings("unchecked") // Casts guarded by conditionals. + static TypeAdapter<?> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson, + TypeToken<?> fieldType, JsonAdapter annotation) { + Class<?> value = annotation.value(); + if (TypeAdapter.class.isAssignableFrom(value)) { + Class<TypeAdapter<?>> typeAdapter = (Class<TypeAdapter<?>>) value; + return constructorConstructor.get(TypeToken.get(typeAdapter)).construct(); + } + if (TypeAdapterFactory.class.isAssignableFrom(value)) { + Class<TypeAdapterFactory> typeAdapterFactory = (Class<TypeAdapterFactory>) value; + return constructorConstructor.get(TypeToken.get(typeAdapterFactory)) + .construct() + .create(gson, fieldType); + } + + throw new IllegalArgumentException( + "@JsonAdapter value must be TypeAdapter or TypeAdapterFactory reference."); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java new file mode 100644 index 00000000..6a836280 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This reader walks the elements of a JsonElement as if it was coming from a + * character stream. + * + * @author Jesse Wilson + */ +public final class JsonTreeReader extends JsonReader { + private static final Reader UNREADABLE_READER = new Reader() { + @Override public int read(char[] buffer, int offset, int count) throws IOException { + throw new AssertionError(); + } + @Override public void close() throws IOException { + throw new AssertionError(); + } + }; + private static final Object SENTINEL_CLOSED = new Object(); + + private final List<Object> stack = new ArrayList<Object>(); + + public JsonTreeReader(JsonElement element) { + super(UNREADABLE_READER); + stack.add(element); + } + + @Override public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + JsonArray array = (JsonArray) peekStack(); + stack.add(array.iterator()); + } + + @Override public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + popStack(); // empty iterator + popStack(); // array + } + + @Override public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + JsonObject object = (JsonObject) peekStack(); + stack.add(object.entrySet().iterator()); + } + + @Override public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + popStack(); // empty iterator + popStack(); // object + } + + @Override public boolean hasNext() throws IOException { + JsonToken token = peek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } + + @Override public JsonToken peek() throws IOException { + if (stack.isEmpty()) { + return JsonToken.END_DOCUMENT; + } + + Object o = peekStack(); + if (o instanceof Iterator) { + boolean isObject = stack.get(stack.size() - 2) instanceof JsonObject; + Iterator<?> iterator = (Iterator<?>) o; + if (iterator.hasNext()) { + if (isObject) { + return JsonToken.NAME; + } else { + stack.add(iterator.next()); + return peek(); + } + } else { + return isObject ? JsonToken.END_OBJECT : JsonToken.END_ARRAY; + } + } else if (o instanceof JsonObject) { + return JsonToken.BEGIN_OBJECT; + } else if (o instanceof JsonArray) { + return JsonToken.BEGIN_ARRAY; + } else if (o instanceof JsonPrimitive) { + JsonPrimitive primitive = (JsonPrimitive) o; + if (primitive.isString()) { + return JsonToken.STRING; + } else if (primitive.isBoolean()) { + return JsonToken.BOOLEAN; + } else if (primitive.isNumber()) { + return JsonToken.NUMBER; + } else { + throw new AssertionError(); + } + } else if (o instanceof JsonNull) { + return JsonToken.NULL; + } else if (o == SENTINEL_CLOSED) { + throw new IllegalStateException("JsonReader is closed"); + } else { + throw new AssertionError(); + } + } + + private Object peekStack() { + return stack.get(stack.size() - 1); + } + + private Object popStack() { + return stack.remove(stack.size() - 1); + } + + private void expect(JsonToken expected) throws IOException { + if (peek() != expected) { + throw new IllegalStateException("Expected " + expected + " but was " + peek()); + } + } + + @Override public String nextName() throws IOException { + expect(JsonToken.NAME); + Iterator<?> i = (Iterator<?>) peekStack(); + Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next(); + stack.add(entry.getValue()); + return (String) entry.getKey(); + } + + @Override public String nextString() throws IOException { + JsonToken token = peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected " + JsonToken.STRING + " but was " + token); + } + return ((JsonPrimitive) popStack()).getAsString(); + } + + @Override public boolean nextBoolean() throws IOException { + expect(JsonToken.BOOLEAN); + return ((JsonPrimitive) popStack()).getAsBoolean(); + } + + @Override public void nextNull() throws IOException { + expect(JsonToken.NULL); + popStack(); + } + + @Override public double nextDouble() throws IOException { + JsonToken token = peek(); + if (token != JsonToken.NUMBER && token != JsonToken.STRING) { + throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token); + } + double result = ((JsonPrimitive) peekStack()).getAsDouble(); + if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) { + throw new NumberFormatException("JSON forbids NaN and infinities: " + result); + } + popStack(); + return result; + } + + @Override public long nextLong() throws IOException { + JsonToken token = peek(); + if (token != JsonToken.NUMBER && token != JsonToken.STRING) { + throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token); + } + long result = ((JsonPrimitive) peekStack()).getAsLong(); + popStack(); + return result; + } + + @Override public int nextInt() throws IOException { + JsonToken token = peek(); + if (token != JsonToken.NUMBER && token != JsonToken.STRING) { + throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token); + } + int result = ((JsonPrimitive) peekStack()).getAsInt(); + popStack(); + return result; + } + + @Override public void close() throws IOException { + stack.clear(); + stack.add(SENTINEL_CLOSED); + } + + @Override public void skipValue() throws IOException { + if (peek() == JsonToken.NAME) { + nextName(); + } else { + popStack(); + } + } + + @Override public String toString() { + return getClass().getSimpleName(); + } + + public void promoteNameToValue() throws IOException { + expect(JsonToken.NAME); + Iterator<?> i = (Iterator<?>) peekStack(); + Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next(); + stack.add(entry.getValue()); + stack.add(new JsonPrimitive((String)entry.getKey())); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java new file mode 100644 index 00000000..5f9f0395 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +/** + * This writer creates a JsonElement. + */ +public final class JsonTreeWriter extends JsonWriter { + private static final Writer UNWRITABLE_WRITER = new Writer() { + @Override public void write(char[] buffer, int offset, int counter) { + throw new AssertionError(); + } + @Override public void flush() throws IOException { + throw new AssertionError(); + } + @Override public void close() throws IOException { + throw new AssertionError(); + } + }; + /** Added to the top of the stack when this writer is closed to cause following ops to fail. */ + private static final JsonPrimitive SENTINEL_CLOSED = new JsonPrimitive("closed"); + + /** The JsonElements and JsonArrays under modification, outermost to innermost. */ + private final List<JsonElement> stack = new ArrayList<JsonElement>(); + + /** The name for the next JSON object value. If non-null, the top of the stack is a JsonObject. */ + private String pendingName; + + /** the JSON element constructed by this writer. */ + private JsonElement product = JsonNull.INSTANCE; // TODO: is this really what we want?; + + public JsonTreeWriter() { + super(UNWRITABLE_WRITER); + } + + /** + * Returns the top level object produced by this writer. + */ + public JsonElement get() { + if (!stack.isEmpty()) { + throw new IllegalStateException("Expected one JSON element but was " + stack); + } + return product; + } + + private JsonElement peek() { + return stack.get(stack.size() - 1); + } + + private void put(JsonElement value) { + if (pendingName != null) { + if (!value.isJsonNull() || getSerializeNulls()) { + JsonObject object = (JsonObject) peek(); + object.add(pendingName, value); + } + pendingName = null; + } else if (stack.isEmpty()) { + product = value; + } else { + JsonElement element = peek(); + if (element instanceof JsonArray) { + ((JsonArray) element).add(value); + } else { + throw new IllegalStateException(); + } + } + } + + @Override public JsonWriter beginArray() throws IOException { + JsonArray array = new JsonArray(); + put(array); + stack.add(array); + return this; + } + + @Override public JsonWriter endArray() throws IOException { + if (stack.isEmpty() || pendingName != null) { + throw new IllegalStateException(); + } + JsonElement element = peek(); + if (element instanceof JsonArray) { + stack.remove(stack.size() - 1); + return this; + } + throw new IllegalStateException(); + } + + @Override public JsonWriter beginObject() throws IOException { + JsonObject object = new JsonObject(); + put(object); + stack.add(object); + return this; + } + + @Override public JsonWriter endObject() throws IOException { + if (stack.isEmpty() || pendingName != null) { + throw new IllegalStateException(); + } + JsonElement element = peek(); + if (element instanceof JsonObject) { + stack.remove(stack.size() - 1); + return this; + } + throw new IllegalStateException(); + } + + @Override public JsonWriter name(String name) throws IOException { + if (stack.isEmpty() || pendingName != null) { + throw new IllegalStateException(); + } + JsonElement element = peek(); + if (element instanceof JsonObject) { + pendingName = name; + return this; + } + throw new IllegalStateException(); + } + + @Override public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + put(new JsonPrimitive(value)); + return this; + } + + @Override public JsonWriter nullValue() throws IOException { + put(JsonNull.INSTANCE); + return this; + } + + @Override public JsonWriter value(boolean value) throws IOException { + put(new JsonPrimitive(value)); + return this; + } + + @Override public JsonWriter value(double value) throws IOException { + if (!isLenient() && (Double.isNaN(value) || Double.isInfinite(value))) { + throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value); + } + put(new JsonPrimitive(value)); + return this; + } + + @Override public JsonWriter value(long value) throws IOException { + put(new JsonPrimitive(value)); + return this; + } + + @Override public JsonWriter value(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + if (!isLenient()) { + double d = value.doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value); + } + } + + put(new JsonPrimitive(value)); + return this; + } + + @Override public void flush() throws IOException { + } + + @Override public void close() throws IOException { + if (!stack.isEmpty()) { + throw new IOException("Incomplete document"); + } + stack.add(SENTINEL_CLOSED); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java new file mode 100644 index 00000000..c3c616c4 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.JsonReaderInternalAccess; +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Adapts maps to either JSON objects or JSON arrays. + * + * <h3>Maps as JSON objects</h3> + * For primitive keys or when complex map key serialization is not enabled, this + * converts Java {@link Map Maps} to JSON Objects. This requires that map keys + * can be serialized as strings; this is insufficient for some key types. For + * example, consider a map whose keys are points on a grid. The default JSON + * form encodes reasonably: <pre> {@code + * Map<Point, String> original = new LinkedHashMap<Point, String>(); + * original.put(new Point(5, 6), "a"); + * original.put(new Point(8, 8), "b"); + * System.out.println(gson.toJson(original, type)); + * }</pre> + * The above code prints this JSON object:<pre> {@code + * { + * "(5,6)": "a", + * "(8,8)": "b" + * } + * }</pre> + * But GSON is unable to deserialize this value because the JSON string name is + * just the {@link Object#toString() toString()} of the map key. Attempting to + * convert the above JSON to an object fails with a parse exception: + * <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)" + * at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler + * at com.google.gson.ObjectNavigator.navigateClassFields + * ...</pre> + * + * <h3>Maps as JSON arrays</h3> + * An alternative approach taken by this type adapter when it is required and + * complex map key serialization is enabled is to encode maps as arrays of map + * entries. Each map entry is a two element array containing a key and a value. + * This approach is more flexible because any type can be used as the map's key; + * not just strings. But it's also less portable because the receiver of such + * JSON must be aware of the map entry convention. + * + * <p>Register this adapter when you are creating your GSON instance. + * <pre> {@code + * Gson gson = new GsonBuilder() + * .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter()) + * .create(); + * }</pre> + * This will change the structure of the JSON emitted by the code above. Now we + * get an array. In this case the arrays elements are map entries: + * <pre> {@code + * [ + * [ + * { + * "x": 5, + * "y": 6 + * }, + * "a", + * ], + * [ + * { + * "x": 8, + * "y": 8 + * }, + * "b" + * ] + * ] + * }</pre> + * This format will serialize and deserialize just fine as long as this adapter + * is registered. + */ +public final class MapTypeAdapterFactory implements TypeAdapterFactory { + private final ConstructorConstructor constructorConstructor; + private final boolean complexMapKeySerialization; + + public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, + boolean complexMapKeySerialization) { + this.constructorConstructor = constructorConstructor; + this.complexMapKeySerialization = complexMapKeySerialization; + } + + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Type type = typeToken.getType(); + + Class<? super T> rawType = typeToken.getRawType(); + if (!Map.class.isAssignableFrom(rawType)) { + return null; + } + + Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type); + Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); + TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]); + TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1])); + ObjectConstructor<T> constructor = constructorConstructor.get(typeToken); + + @SuppressWarnings({"unchecked", "rawtypes"}) + // we don't define a type parameter for the key or value types + TypeAdapter<T> result = new Adapter(gson, keyAndValueTypes[0], keyAdapter, + keyAndValueTypes[1], valueAdapter, constructor); + return result; + } + + /** + * Returns a type adapter that writes the value as a string. + */ + private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) { + return (keyType == boolean.class || keyType == Boolean.class) + ? TypeAdapters.BOOLEAN_AS_STRING + : context.getAdapter(TypeToken.get(keyType)); + } + + private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> { + private final TypeAdapter<K> keyTypeAdapter; + private final TypeAdapter<V> valueTypeAdapter; + private final ObjectConstructor<? extends Map<K, V>> constructor; + + public Adapter(Gson context, Type keyType, TypeAdapter<K> keyTypeAdapter, + Type valueType, TypeAdapter<V> valueTypeAdapter, + ObjectConstructor<? extends Map<K, V>> constructor) { + this.keyTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<K>(context, keyTypeAdapter, keyType); + this.valueTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<V>(context, valueTypeAdapter, valueType); + this.constructor = constructor; + } + + public Map<K, V> read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } + + Map<K, V> map = constructor.construct(); + + if (peek == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + while (in.hasNext()) { + in.beginArray(); // entry array + K key = keyTypeAdapter.read(in); + V value = valueTypeAdapter.read(in); + V replaced = map.put(key, value); + if (replaced != null) { + throw new JsonSyntaxException("duplicate key: " + key); + } + in.endArray(); + } + in.endArray(); + } else { + in.beginObject(); + while (in.hasNext()) { + JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in); + K key = keyTypeAdapter.read(in); + V value = valueTypeAdapter.read(in); + V replaced = map.put(key, value); + if (replaced != null) { + throw new JsonSyntaxException("duplicate key: " + key); + } + } + in.endObject(); + } + return map; + } + + public void write(JsonWriter out, Map<K, V> map) throws IOException { + if (map == null) { + out.nullValue(); + return; + } + + if (!complexMapKeySerialization) { + out.beginObject(); + for (Map.Entry<K, V> entry : map.entrySet()) { + out.name(String.valueOf(entry.getKey())); + valueTypeAdapter.write(out, entry.getValue()); + } + out.endObject(); + return; + } + + boolean hasComplexKeys = false; + List<JsonElement> keys = new ArrayList<JsonElement>(map.size()); + + List<V> values = new ArrayList<V>(map.size()); + for (Map.Entry<K, V> entry : map.entrySet()) { + JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey()); + keys.add(keyElement); + values.add(entry.getValue()); + hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); + } + + if (hasComplexKeys) { + out.beginArray(); + for (int i = 0; i < keys.size(); i++) { + out.beginArray(); // entry array + Streams.write(keys.get(i), out); + valueTypeAdapter.write(out, values.get(i)); + out.endArray(); + } + out.endArray(); + } else { + out.beginObject(); + for (int i = 0; i < keys.size(); i++) { + JsonElement keyElement = keys.get(i); + out.name(keyToString(keyElement)); + valueTypeAdapter.write(out, values.get(i)); + } + out.endObject(); + } + } + + private String keyToString(JsonElement keyElement) { + if (keyElement.isJsonPrimitive()) { + JsonPrimitive primitive = keyElement.getAsJsonPrimitive(); + if (primitive.isNumber()) { + return String.valueOf(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + return Boolean.toString(primitive.getAsBoolean()); + } else if (primitive.isString()) { + return primitive.getAsString(); + } else { + throw new AssertionError(); + } + } else if (keyElement.isJsonNull()) { + return "null"; + } else { + throw new AssertionError(); + } + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java new file mode 100644 index 00000000..235d2b38 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.LinkedTreeMap; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Adapts types whose static type is only 'Object'. Uses getClass() on + * serialization and a primitive/Map/List on deserialization. + */ +public final class ObjectTypeAdapter extends TypeAdapter<Object> { + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + if (type.getRawType() == Object.class) { + return (TypeAdapter<T>) new ObjectTypeAdapter(gson); + } + return null; + } + }; + + private final Gson gson; + + private ObjectTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override public Object read(JsonReader in) throws IOException { + JsonToken token = in.peek(); + switch (token) { + case BEGIN_ARRAY: + List<Object> list = new ArrayList<Object>(); + in.beginArray(); + while (in.hasNext()) { + list.add(read(in)); + } + in.endArray(); + return list; + + case BEGIN_OBJECT: + Map<String, Object> map = new LinkedTreeMap<String, Object>(); + in.beginObject(); + while (in.hasNext()) { + map.put(in.nextName(), read(in)); + } + in.endObject(); + return map; + + case STRING: + return in.nextString(); + + case NUMBER: + return in.nextDouble(); + + case BOOLEAN: + return in.nextBoolean(); + + case NULL: + in.nextNull(); + return null; + + default: + throw new IllegalStateException(); + } + } + + @SuppressWarnings("unchecked") + @Override public void write(JsonWriter out, Object value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass()); + if (typeAdapter instanceof ObjectTypeAdapter) { + out.beginObject(); + out.endObject(); + return; + } + + typeAdapter.write(out, value); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java new file mode 100644 index 00000000..3957c369 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import static com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory.getTypeAdapter; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.gson.FieldNamingStrategy; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.Excluder; +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.Primitives; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Type adapter that reflects over the fields and methods of a class. + */ +public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { + private final ConstructorConstructor constructorConstructor; + private final FieldNamingStrategy fieldNamingPolicy; + private final Excluder excluder; + + public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor, + FieldNamingStrategy fieldNamingPolicy, Excluder excluder) { + this.constructorConstructor = constructorConstructor; + this.fieldNamingPolicy = fieldNamingPolicy; + this.excluder = excluder; + } + + public boolean excludeField(Field f, boolean serialize) { + return excludeField(f, serialize, excluder); + } + + static boolean excludeField(Field f, boolean serialize, Excluder excluder) { + return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize); + } + + /** first element holds the default name */ + private List<String> getFieldNames(Field f) { + return getFieldName(fieldNamingPolicy, f); + } + + /** first element holds the default name */ + static List<String> getFieldName(FieldNamingStrategy fieldNamingPolicy, Field f) { + SerializedName serializedName = f.getAnnotation(SerializedName.class); + List<String> fieldNames = new LinkedList<String>(); + if (serializedName == null) { + fieldNames.add(fieldNamingPolicy.translateName(f)); + } else { + fieldNames.add(serializedName.value()); + for (String alternate : serializedName.alternate()) { + fieldNames.add(alternate); + } + } + return fieldNames; + } + + public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { + Class<? super T> raw = type.getRawType(); + + if (!Object.class.isAssignableFrom(raw)) { + return null; // it's a primitive! + } + + ObjectConstructor<T> constructor = constructorConstructor.get(type); + return new Adapter<T>(constructor, getBoundFields(gson, type, raw)); + } + + private ReflectiveTypeAdapterFactory.BoundField createBoundField( + final Gson context, final Field field, final String name, + final TypeToken<?> fieldType, boolean serialize, boolean deserialize) { + final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); + // special casing primitives here saves ~5% on Android... + return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) { + final TypeAdapter<?> typeAdapter = getFieldAdapter(context, field, fieldType); + @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree + @Override void write(JsonWriter writer, Object value) + throws IOException, IllegalAccessException { + Object fieldValue = field.get(value); + TypeAdapter t = + new TypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType()); + t.write(writer, fieldValue); + } + @Override void read(JsonReader reader, Object value) + throws IOException, IllegalAccessException { + Object fieldValue = typeAdapter.read(reader); + if (fieldValue != null || !isPrimitive) { + field.set(value, fieldValue); + } + } + public boolean writeField(Object value) throws IOException, IllegalAccessException { + if (!serialized) return false; + Object fieldValue = field.get(value); + return fieldValue != value; // avoid recursion for example for Throwable.cause + } + }; + } + + private TypeAdapter<?> getFieldAdapter(Gson gson, Field field, TypeToken<?> fieldType) { + JsonAdapter annotation = field.getAnnotation(JsonAdapter.class); + if (annotation != null) { + TypeAdapter<?> adapter = getTypeAdapter(constructorConstructor, gson, fieldType, annotation); + if (adapter != null) return adapter; + } + return gson.getAdapter(fieldType); + } + + private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) { + Map<String, BoundField> result = new LinkedHashMap<String, BoundField>(); + if (raw.isInterface()) { + return result; + } + + Type declaredType = type.getType(); + while (raw != Object.class) { + Field[] fields = raw.getDeclaredFields(); + for (Field field : fields) { + boolean serialize = excludeField(field, true); + boolean deserialize = excludeField(field, false); + if (!serialize && !deserialize) { + continue; + } + field.setAccessible(true); + Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); + List<String> fieldNames = getFieldNames(field); + BoundField previous = null; + for (int i = 0; i < fieldNames.size(); ++i) { + String name = fieldNames.get(i); + if (i != 0) serialize = false; // only serialize the default name + BoundField boundField = createBoundField(context, field, name, + TypeToken.get(fieldType), serialize, deserialize); + BoundField replaced = result.put(name, boundField); + if (previous == null) previous = replaced; + } + if (previous != null) { + throw new IllegalArgumentException(declaredType + + " declares multiple JSON fields named " + previous.name); + } + } + type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass())); + raw = type.getRawType(); + } + return result; + } + + static abstract class BoundField { + final String name; + final boolean serialized; + final boolean deserialized; + + protected BoundField(String name, boolean serialized, boolean deserialized) { + this.name = name; + this.serialized = serialized; + this.deserialized = deserialized; + } + abstract boolean writeField(Object value) throws IOException, IllegalAccessException; + abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; + abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; + } + + public static final class Adapter<T> extends TypeAdapter<T> { + private final ObjectConstructor<T> constructor; + private final Map<String, BoundField> boundFields; + + private Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) { + this.constructor = constructor; + this.boundFields = boundFields; + } + + @Override public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + T instance = constructor.construct(); + + try { + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + BoundField field = boundFields.get(name); + if (field == null || !field.deserialized) { + in.skipValue(); + } else { + field.read(in, instance); + } + } + } catch (IllegalStateException e) { + throw new JsonSyntaxException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + in.endObject(); + return instance; + } + + @Override public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + out.beginObject(); + try { + for (BoundField boundField : boundFields.values()) { + if (boundField.writeField(value)) { + out.name(boundField.name); + boundField.write(out, value); + } + } + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + out.endObject(); + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/SqlDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/SqlDateTypeAdapter.java new file mode 100644 index 00000000..5ecc2e96 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/SqlDateTypeAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +/** + * Adapter for java.sql.Date. Although this class appears stateless, it is not. + * DateFormat captures its time zone and locale when it is created, which gives + * this class state. DateFormat isn't thread safe either, so this class has + * to synchronize its read and write methods. + */ +public final class SqlDateTypeAdapter extends TypeAdapter<java.sql.Date> { + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return typeToken.getRawType() == java.sql.Date.class + ? (TypeAdapter<T>) new SqlDateTypeAdapter() : null; + } + }; + + private final DateFormat format = new SimpleDateFormat("MMM d, yyyy"); + + @Override + public synchronized java.sql.Date read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + final long utilDate = format.parse(in.nextString()).getTime(); + return new java.sql.Date(utilDate); + } catch (ParseException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public synchronized void write(JsonWriter out, java.sql.Date value) throws IOException { + out.value(value == null ? null : format.format(value)); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/TimeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/TimeTypeAdapter.java new file mode 100644 index 00000000..bbbb4d97 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/TimeTypeAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.sql.Time; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Adapter for Time. Although this class appears stateless, it is not. + * DateFormat captures its time zone and locale when it is created, which gives + * this class state. DateFormat isn't thread safe either, so this class has + * to synchronize its read and write methods. + */ +public final class TimeTypeAdapter extends TypeAdapter<Time> { + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return typeToken.getRawType() == Time.class ? (TypeAdapter<T>) new TimeTypeAdapter() : null; + } + }; + + private final DateFormat format = new SimpleDateFormat("hh:mm:ss a"); + + @Override public synchronized Time read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + Date date = format.parse(in.nextString()); + return new Time(date.getTime()); + } catch (ParseException e) { + throw new JsonSyntaxException(e); + } + } + + @Override public synchronized void write(JsonWriter out, Time value) throws IOException { + out.value(value == null ? null : format.format(value)); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java new file mode 100644 index 00000000..7e52c27d --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -0,0 +1,81 @@ +/*
+ * Copyright (C) 2011 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 com.google.gson.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
+ private final Gson context;
+ private final TypeAdapter<T> delegate;
+ private final Type type;
+
+ TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
+ this.context = context;
+ this.delegate = delegate;
+ this.type = type;
+ }
+
+ @Override
+ public T read(JsonReader in) throws IOException {
+ return delegate.read(in);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public void write(JsonWriter out, T value) throws IOException {
+ // Order of preference for choosing type adapters
+ // First preference: a type adapter registered for the runtime type
+ // Second preference: a type adapter registered for the declared type
+ // Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type)
+ // Fourth preference: reflective type adapter for the declared type
+
+ TypeAdapter chosen = delegate;
+ Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
+ if (runtimeType != type) {
+ TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
+ if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
+ // The user registered a type adapter for the runtime type, so we will use that
+ chosen = runtimeTypeAdapter;
+ } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
+ // The user registered a type adapter for Base class, so we prefer it over the
+ // reflective type adapter for the runtime type
+ chosen = delegate;
+ } else {
+ // Use the type adapter for runtime type
+ chosen = runtimeTypeAdapter;
+ }
+ }
+ chosen.write(out, value);
+ }
+
+ /**
+ * Finds a compatible runtime type if it is more specific
+ */
+ private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
+ if (value != null
+ && (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
+ type = value.getClass();
+ }
+ return type;
+ }
+}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java new file mode 100644 index 00000000..ec7ceb4f --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2011 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 com.google.gson.internal.bind; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.sql.Timestamp; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.UUID; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Type adapters for basic types. + */ +public final class TypeAdapters { + private TypeAdapters() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("rawtypes") + public static final TypeAdapter<Class> CLASS = new TypeAdapter<Class>() { + @Override + public void write(JsonWriter out, Class value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: " + + value.getName() + ". Forgot to register a type adapter?"); + } + } + @Override + public Class read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + throw new UnsupportedOperationException( + "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"); + } + } + }; + public static final TypeAdapterFactory CLASS_FACTORY = newFactory(Class.class, CLASS); + + public static final TypeAdapter<BitSet> BIT_SET = new TypeAdapter<BitSet>() { + public BitSet read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + BitSet bitset = new BitSet(); + in.beginArray(); + int i = 0; + JsonToken tokenType = in.peek(); + while (tokenType != JsonToken.END_ARRAY) { + boolean set; + switch (tokenType) { + case NUMBER: + set = in.nextInt() != 0; + break; + case BOOLEAN: + set = in.nextBoolean(); + break; + case STRING: + String stringValue = in.nextString(); + try { + set = Integer.parseInt(stringValue) != 0; + } catch (NumberFormatException e) { + throw new JsonSyntaxException( + "Error: Expecting: bitset number value (1, 0), Found: " + stringValue); + } + break; + default: + throw new JsonSyntaxException("Invalid bitset value type: " + tokenType); + } + if (set) { + bitset.set(i); + } + ++i; + tokenType = in.peek(); + } + in.endArray(); + return bitset; + } + + public void write(JsonWriter out, BitSet src) throws IOException { + if (src == null) { + out.nullValue(); + return; + } + + out.beginArray(); + for (int i = 0; i < src.length(); i++) { + int value = (src.get(i)) ? 1 : 0; + out.value(value); + } + out.endArray(); + } + }; + + public static final TypeAdapterFactory BIT_SET_FACTORY = newFactory(BitSet.class, BIT_SET); + + public static final TypeAdapter<Boolean> BOOLEAN = new TypeAdapter<Boolean>() { + @Override + public Boolean read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (in.peek() == JsonToken.STRING) { + // support strings for compatibility with GSON 1.7 + return Boolean.parseBoolean(in.nextString()); + } + return in.nextBoolean(); + } + @Override + public void write(JsonWriter out, Boolean value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.value(value); + } + }; + + /** + * Writes a boolean as a string. Useful for map keys, where booleans aren't + * otherwise permitted. + */ + public static final TypeAdapter<Boolean> BOOLEAN_AS_STRING = new TypeAdapter<Boolean>() { + @Override public Boolean read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return Boolean.valueOf(in.nextString()); + } + + @Override public void write(JsonWriter out, Boolean value) throws IOException { + out.value(value == null ? "null" : value.toString()); + } + }; + + public static final TypeAdapterFactory BOOLEAN_FACTORY + = newFactory(boolean.class, Boolean.class, BOOLEAN); + + public static final TypeAdapter<Number> BYTE = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + int intValue = in.nextInt(); + return (byte) intValue; + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory BYTE_FACTORY + = newFactory(byte.class, Byte.class, BYTE); + + public static final TypeAdapter<Number> SHORT = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return (short) in.nextInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory SHORT_FACTORY + = newFactory(short.class, Short.class, SHORT); + + public static final TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return in.nextInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory INTEGER_FACTORY + = newFactory(int.class, Integer.class, INTEGER); + + public static final TypeAdapter<Number> LONG = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return in.nextLong(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter<Number> FLOAT = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return (float) in.nextDouble(); + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter<Number> DOUBLE = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return in.nextDouble(); + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter<Number> NUMBER = new TypeAdapter<Number>() { + @Override + public Number read(JsonReader in) throws IOException { + JsonToken jsonToken = in.peek(); + switch (jsonToken) { + case NULL: + in.nextNull(); + return null; + case NUMBER: + return new LazilyParsedNumber(in.nextString()); + default: + throw new JsonSyntaxException("Expecting number, got: " + jsonToken); + } + } + @Override + public void write(JsonWriter out, Number value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory NUMBER_FACTORY = newFactory(Number.class, NUMBER); + + public static final TypeAdapter<Character> CHARACTER = new TypeAdapter<Character>() { + @Override + public Character read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String str = in.nextString(); + if (str.length() != 1) { + throw new JsonSyntaxException("Expecting character, got: " + str); + } + return str.charAt(0); + } + @Override + public void write(JsonWriter out, Character value) throws IOException { + out.value(value == null ? null : String.valueOf(value)); + } + }; + + public static final TypeAdapterFactory CHARACTER_FACTORY + = newFactory(char.class, Character.class, CHARACTER); + + public static final TypeAdapter<String> STRING = new TypeAdapter<String>() { + @Override + public String read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } + /* coerce booleans to strings for backwards compatibility */ + if (peek == JsonToken.BOOLEAN) { + return Boolean.toString(in.nextBoolean()); + } + return in.nextString(); + } + @Override + public void write(JsonWriter out, String value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter<BigDecimal> BIG_DECIMAL = new TypeAdapter<BigDecimal>() { + @Override public BigDecimal read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return new BigDecimal(in.nextString()); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override public void write(JsonWriter out, BigDecimal value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapter<BigInteger> BIG_INTEGER = new TypeAdapter<BigInteger>() { + @Override public BigInteger read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + return new BigInteger(in.nextString()); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override public void write(JsonWriter out, BigInteger value) throws IOException { + out.value(value); + } + }; + + public static final TypeAdapterFactory STRING_FACTORY = newFactory(String.class, STRING); + + public static final TypeAdapter<StringBuilder> STRING_BUILDER = new TypeAdapter<StringBuilder>() { + @Override + public StringBuilder read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return new StringBuilder(in.nextString()); + } + @Override + public void write(JsonWriter out, StringBuilder value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory STRING_BUILDER_FACTORY = + newFactory(StringBuilder.class, STRING_BUILDER); + + public static final TypeAdapter<StringBuffer> STRING_BUFFER = new TypeAdapter<StringBuffer>() { + @Override + public StringBuffer read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return new StringBuffer(in.nextString()); + } + @Override + public void write(JsonWriter out, StringBuffer value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory STRING_BUFFER_FACTORY = + newFactory(StringBuffer.class, STRING_BUFFER); + + public static final TypeAdapter<URL> URL = new TypeAdapter<URL>() { + @Override + public URL read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String nextString = in.nextString(); + return "null".equals(nextString) ? null : new URL(nextString); + } + @Override + public void write(JsonWriter out, URL value) throws IOException { + out.value(value == null ? null : value.toExternalForm()); + } + }; + + public static final TypeAdapterFactory URL_FACTORY = newFactory(URL.class, URL); + + public static final TypeAdapter<URI> URI = new TypeAdapter<URI>() { + @Override + public URI read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + try { + String nextString = in.nextString(); + return "null".equals(nextString) ? null : new URI(nextString); + } catch (URISyntaxException e) { + throw new JsonIOException(e); + } + } + @Override + public void write(JsonWriter out, URI value) throws IOException { + out.value(value == null ? null : value.toASCIIString()); + } + }; + + public static final TypeAdapterFactory URI_FACTORY = newFactory(URI.class, URI); + + public static final TypeAdapter<InetAddress> INET_ADDRESS = new TypeAdapter<InetAddress>() { + @Override + public InetAddress read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + // regrettably, this should have included both the host name and the host address + return InetAddress.getByName(in.nextString()); + } + @Override + public void write(JsonWriter out, InetAddress value) throws IOException { + out.value(value == null ? null : value.getHostAddress()); + } + }; + + public static final TypeAdapterFactory INET_ADDRESS_FACTORY = + newTypeHierarchyFactory(InetAddress.class, INET_ADDRESS); + + public static final TypeAdapter<UUID> UUID = new TypeAdapter<UUID>() { + @Override + public UUID read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return java.util.UUID.fromString(in.nextString()); + } + @Override + public void write(JsonWriter out, UUID value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory UUID_FACTORY = newFactory(UUID.class, UUID); + + public static final TypeAdapterFactory TIMESTAMP_FACTORY = new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + if (typeToken.getRawType() != Timestamp.class) { + return null; + } + + final TypeAdapter<Date> dateTypeAdapter = gson.getAdapter(Date.class); + return (TypeAdapter<T>) new TypeAdapter<Timestamp>() { + @Override public Timestamp read(JsonReader in) throws IOException { + Date date = dateTypeAdapter.read(in); + return date != null ? new Timestamp(date.getTime()) : null; + } + + @Override public void write(JsonWriter out, Timestamp value) throws IOException { + dateTypeAdapter.write(out, value); + } + }; + } + }; + + public static final TypeAdapter<Calendar> CALENDAR = new TypeAdapter<Calendar>() { + private static final String YEAR = "year"; + private static final String MONTH = "month"; + private static final String DAY_OF_MONTH = "dayOfMonth"; + private static final String HOUR_OF_DAY = "hourOfDay"; + private static final String MINUTE = "minute"; + private static final String SECOND = "second"; + + @Override + public Calendar read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + in.beginObject(); + int year = 0; + int month = 0; + int dayOfMonth = 0; + int hourOfDay = 0; + int minute = 0; + int second = 0; + while (in.peek() != JsonToken.END_OBJECT) { + String name = in.nextName(); + int value = in.nextInt(); + if (YEAR.equals(name)) { + year = value; + } else if (MONTH.equals(name)) { + month = value; + } else if (DAY_OF_MONTH.equals(name)) { + dayOfMonth = value; + } else if (HOUR_OF_DAY.equals(name)) { + hourOfDay = value; + } else if (MINUTE.equals(name)) { + minute = value; + } else if (SECOND.equals(name)) { + second = value; + } + } + in.endObject(); + return new GregorianCalendar(year, month, dayOfMonth, hourOfDay, minute, second); + } + + @Override + public void write(JsonWriter out, Calendar value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name(YEAR); + out.value(value.get(Calendar.YEAR)); + out.name(MONTH); + out.value(value.get(Calendar.MONTH)); + out.name(DAY_OF_MONTH); + out.value(value.get(Calendar.DAY_OF_MONTH)); + out.name(HOUR_OF_DAY); + out.value(value.get(Calendar.HOUR_OF_DAY)); + out.name(MINUTE); + out.value(value.get(Calendar.MINUTE)); + out.name(SECOND); + out.value(value.get(Calendar.SECOND)); + out.endObject(); + } + }; + + public static final TypeAdapterFactory CALENDAR_FACTORY = + newFactoryForMultipleTypes(Calendar.class, GregorianCalendar.class, CALENDAR); + + public static final TypeAdapter<Locale> LOCALE = new TypeAdapter<Locale>() { + @Override + public Locale read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String locale = in.nextString(); + StringTokenizer tokenizer = new StringTokenizer(locale, "_"); + String language = null; + String country = null; + String variant = null; + if (tokenizer.hasMoreElements()) { + language = tokenizer.nextToken(); + } + if (tokenizer.hasMoreElements()) { + country = tokenizer.nextToken(); + } + if (tokenizer.hasMoreElements()) { + variant = tokenizer.nextToken(); + } + if (country == null && variant == null) { + return new Locale(language); + } else if (variant == null) { + return new Locale(language, country); + } else { + return new Locale(language, country, variant); + } + } + @Override + public void write(JsonWriter out, Locale value) throws IOException { + out.value(value == null ? null : value.toString()); + } + }; + + public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE); + + public static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() { + @Override public JsonElement read(JsonReader in) throws IOException { + switch (in.peek()) { + case STRING: + return new JsonPrimitive(in.nextString()); + case NUMBER: + String number = in.nextString(); + return new JsonPrimitive(new LazilyParsedNumber(number)); + case BOOLEAN: + return new JsonPrimitive(in.nextBoolean()); + case NULL: + in.nextNull(); + return JsonNull.INSTANCE; + case BEGIN_ARRAY: + JsonArray array = new JsonArray(); + in.beginArray(); + while (in.hasNext()) { + array.add(read(in)); + } + in.endArray(); + return array; + case BEGIN_OBJECT: + JsonObject object = new JsonObject(); + in.beginObject(); + while (in.hasNext()) { + object.add(in.nextName(), read(in)); + } + in.endObject(); + return object; + case END_DOCUMENT: + case NAME: + case END_OBJECT: + case END_ARRAY: + default: + throw new IllegalArgumentException(); + } + } + + @Override public void write(JsonWriter out, JsonElement value) throws IOException { + if (value == null || value.isJsonNull()) { + out.nullValue(); + } else if (value.isJsonPrimitive()) { + JsonPrimitive primitive = value.getAsJsonPrimitive(); + if (primitive.isNumber()) { + out.value(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + out.value(primitive.getAsBoolean()); + } else { + out.value(primitive.getAsString()); + } + + } else if (value.isJsonArray()) { + out.beginArray(); + for (JsonElement e : value.getAsJsonArray()) { + write(out, e); + } + out.endArray(); + + } else if (value.isJsonObject()) { + out.beginObject(); + for (Map.Entry<String, JsonElement> e : value.getAsJsonObject().entrySet()) { + out.name(e.getKey()); + write(out, e.getValue()); + } + out.endObject(); + + } else { + throw new IllegalArgumentException("Couldn't write " + value.getClass()); + } + } + }; + + public static final TypeAdapterFactory JSON_ELEMENT_FACTORY + = newTypeHierarchyFactory(JsonElement.class, JSON_ELEMENT); + + private static final class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> { + private final Map<String, T> nameToConstant = new HashMap<String, T>(); + private final Map<T, String> constantToName = new HashMap<T, String>(); + + public EnumTypeAdapter(Class<T> classOfT) { + try { + for (T constant : classOfT.getEnumConstants()) { + String name = constant.name(); + SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class); + if (annotation != null) { + name = annotation.value(); + for (String alternate : annotation.alternate()) { + nameToConstant.put(alternate, constant); + } + } + nameToConstant.put(name, constant); + constantToName.put(constant, name); + } + } catch (NoSuchFieldException e) { + throw new AssertionError(); + } + } + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return nameToConstant.get(in.nextString()); + } + + public void write(JsonWriter out, T value) throws IOException { + out.value(value == null ? null : constantToName.get(value)); + } + } + + public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() { + @SuppressWarnings({"rawtypes", "unchecked"}) + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Class<? super T> rawType = typeToken.getRawType(); + if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) { + return null; + } + if (!rawType.isEnum()) { + rawType = rawType.getSuperclass(); // handle anonymous subclasses + } + return (TypeAdapter<T>) new EnumTypeAdapter(rawType); + } + }; + + public static <TT> TypeAdapterFactory newFactory( + final TypeToken<TT> type, final TypeAdapter<TT> typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return typeToken.equals(type) ? (TypeAdapter<T>) typeAdapter : null; + } + }; + } + + public static <TT> TypeAdapterFactory newFactory( + final Class<TT> type, final TypeAdapter<TT> typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return typeToken.getRawType() == type ? (TypeAdapter<T>) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + type.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + public static <TT> TypeAdapterFactory newFactory( + final Class<TT> unboxed, final Class<TT> boxed, final TypeAdapter<? super TT> typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Class<? super T> rawType = typeToken.getRawType(); + return (rawType == unboxed || rawType == boxed) ? (TypeAdapter<T>) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + boxed.getName() + + "+" + unboxed.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + public static <TT> TypeAdapterFactory newFactoryForMultipleTypes(final Class<TT> base, + final Class<? extends TT> sub, final TypeAdapter<? super TT> typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Class<? super T> rawType = typeToken.getRawType(); + return (rawType == base || rawType == sub) ? (TypeAdapter<T>) typeAdapter : null; + } + @Override public String toString() { + return "Factory[type=" + base.getName() + + "+" + sub.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } + + public static <TT> TypeAdapterFactory newTypeHierarchyFactory( + final Class<TT> clazz, final TypeAdapter<TT> typeAdapter) { + return new TypeAdapterFactory() { + @SuppressWarnings("unchecked") + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + return clazz.isAssignableFrom(typeToken.getRawType()) ? (TypeAdapter<T>) typeAdapter : null; + } + @Override public String toString() { + return "Factory[typeHierarchy=" + clazz.getName() + ",adapter=" + typeAdapter + "]"; + } + }; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/package-info.java b/gson/src/main/java/com/google/gson/internal/package-info.java new file mode 100644 index 00000000..b5139b6d --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/package-info.java @@ -0,0 +1,7 @@ +/** + * Do NOT use any class in this package as they are meant for internal use in Gson. + * These classes will very likely change incompatibly in future versions. You have been warned. + * + * @author Inderjeet Singh, Joel Leitch, Jesse Wilson + */ +package com.google.gson.internal;
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/package-info.java b/gson/src/main/java/com/google/gson/package-info.java new file mode 100644 index 00000000..428e280c --- /dev/null +++ b/gson/src/main/java/com/google/gson/package-info.java @@ -0,0 +1,11 @@ +/** + * This package provides the {@link com.google.gson.Gson} class to convert Json to Java and + * vice-versa. + * + * <p>The primary class to use is {@link com.google.gson.Gson} which can be constructed with + * {@code new Gson()} (using default settings) or by using {@link com.google.gson.GsonBuilder} + * (to configure various options such as using versioning and so on).</p> + * + * @author Inderjeet Singh, Joel Leitch + */ +package com.google.gson;
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java new file mode 100644 index 00000000..e16e8e6d --- /dev/null +++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2008 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 com.google.gson.reflect; + +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.$Gson$Preconditions; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a generic type {@code T}. Java doesn't yet provide a way to + * represent generic types, so this class does. Forces clients to create a + * subclass of this class which enables retrieval the type information even at + * runtime. + * + * <p>For example, to create a type literal for {@code List<String>}, you can + * create an empty anonymous inner class: + * + * <p> + * {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};} + * + * <p>This syntax cannot be used to create type literals that have wildcard + * parameters, such as {@code Class<?>} or {@code List<? extends CharSequence>}. + * + * @author Bob Lee + * @author Sven Mawson + * @author Jesse Wilson + */ +public class TypeToken<T> { + final Class<? super T> rawType; + final Type type; + final int hashCode; + + /** + * Constructs a new type literal. Derives represented class from type + * parameter. + * + * <p>Clients create an empty anonymous subclass. Doing so embeds the type + * parameter in the anonymous class's type hierarchy so we can reconstitute it + * at runtime despite erasure. + */ + @SuppressWarnings("unchecked") + protected TypeToken() { + this.type = getSuperclassTypeParameter(getClass()); + this.rawType = (Class<? super T>) $Gson$Types.getRawType(type); + this.hashCode = type.hashCode(); + } + + /** + * Unsafe. Constructs a type literal manually. + */ + @SuppressWarnings("unchecked") + TypeToken(Type type) { + this.type = $Gson$Types.canonicalize($Gson$Preconditions.checkNotNull(type)); + this.rawType = (Class<? super T>) $Gson$Types.getRawType(this.type); + this.hashCode = this.type.hashCode(); + } + + /** + * Returns the type from super class's type parameter in {@link $Gson$Types#canonicalize + * canonical form}. + */ + static Type getSuperclassTypeParameter(Class<?> subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter."); + } + ParameterizedType parameterized = (ParameterizedType) superclass; + return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); + } + + /** + * Returns the raw (non-generic) type for this type. + */ + public final Class<? super T> getRawType() { + return rawType; + } + + /** + * Gets underlying {@code Type} instance. + */ + public final Type getType() { + return type; + } + + /** + * Check if this type is assignable from the given class object. + * + * @deprecated this implementation may be inconsistent with javac for types + * with wildcards. + */ + @Deprecated + public boolean isAssignableFrom(Class<?> cls) { + return isAssignableFrom((Type) cls); + } + + /** + * Check if this type is assignable from the given Type. + * + * @deprecated this implementation may be inconsistent with javac for types + * with wildcards. + */ + @Deprecated + public boolean isAssignableFrom(Type from) { + if (from == null) { + return false; + } + + if (type.equals(from)) { + return true; + } + + if (type instanceof Class<?>) { + return rawType.isAssignableFrom($Gson$Types.getRawType(from)); + } else if (type instanceof ParameterizedType) { + return isAssignableFrom(from, (ParameterizedType) type, + new HashMap<String, Type>()); + } else if (type instanceof GenericArrayType) { + return rawType.isAssignableFrom($Gson$Types.getRawType(from)) + && isAssignableFrom(from, (GenericArrayType) type); + } else { + throw buildUnexpectedTypeError( + type, Class.class, ParameterizedType.class, GenericArrayType.class); + } + } + + /** + * Check if this type is assignable from the given type token. + * + * @deprecated this implementation may be inconsistent with javac for types + * with wildcards. + */ + @Deprecated + public boolean isAssignableFrom(TypeToken<?> token) { + return isAssignableFrom(token.getType()); + } + + /** + * Private helper function that performs some assignability checks for + * the provided GenericArrayType. + */ + private static boolean isAssignableFrom(Type from, GenericArrayType to) { + Type toGenericComponentType = to.getGenericComponentType(); + if (toGenericComponentType instanceof ParameterizedType) { + Type t = from; + if (from instanceof GenericArrayType) { + t = ((GenericArrayType) from).getGenericComponentType(); + } else if (from instanceof Class<?>) { + Class<?> classType = (Class<?>) from; + while (classType.isArray()) { + classType = classType.getComponentType(); + } + t = classType; + } + return isAssignableFrom(t, (ParameterizedType) toGenericComponentType, + new HashMap<String, Type>()); + } + // No generic defined on "to"; therefore, return true and let other + // checks determine assignability + return true; + } + + /** + * Private recursive helper function to actually do the type-safe checking + * of assignability. + */ + private static boolean isAssignableFrom(Type from, ParameterizedType to, + Map<String, Type> typeVarMap) { + + if (from == null) { + return false; + } + + if (to.equals(from)) { + return true; + } + + // First figure out the class and any type information. + Class<?> clazz = $Gson$Types.getRawType(from); + ParameterizedType ptype = null; + if (from instanceof ParameterizedType) { + ptype = (ParameterizedType) from; + } + + // Load up parameterized variable info if it was parameterized. + if (ptype != null) { + Type[] tArgs = ptype.getActualTypeArguments(); + TypeVariable<?>[] tParams = clazz.getTypeParameters(); + for (int i = 0; i < tArgs.length; i++) { + Type arg = tArgs[i]; + TypeVariable<?> var = tParams[i]; + while (arg instanceof TypeVariable<?>) { + TypeVariable<?> v = (TypeVariable<?>) arg; + arg = typeVarMap.get(v.getName()); + } + typeVarMap.put(var.getName(), arg); + } + + // check if they are equivalent under our current mapping. + if (typeEquals(ptype, to, typeVarMap)) { + return true; + } + } + + for (Type itype : clazz.getGenericInterfaces()) { + if (isAssignableFrom(itype, to, new HashMap<String, Type>(typeVarMap))) { + return true; + } + } + + // Interfaces didn't work, try the superclass. + Type sType = clazz.getGenericSuperclass(); + return isAssignableFrom(sType, to, new HashMap<String, Type>(typeVarMap)); + } + + /** + * Checks if two parameterized types are exactly equal, under the variable + * replacement described in the typeVarMap. + */ + private static boolean typeEquals(ParameterizedType from, + ParameterizedType to, Map<String, Type> typeVarMap) { + if (from.getRawType().equals(to.getRawType())) { + Type[] fromArgs = from.getActualTypeArguments(); + Type[] toArgs = to.getActualTypeArguments(); + for (int i = 0; i < fromArgs.length; i++) { + if (!matches(fromArgs[i], toArgs[i], typeVarMap)) { + return false; + } + } + return true; + } + return false; + } + + private static AssertionError buildUnexpectedTypeError( + Type token, Class<?>... expected) { + + // Build exception message + StringBuilder exceptionMessage = + new StringBuilder("Unexpected type. Expected one of: "); + for (Class<?> clazz : expected) { + exceptionMessage.append(clazz.getName()).append(", "); + } + exceptionMessage.append("but got: ").append(token.getClass().getName()) + .append(", for type token: ").append(token.toString()).append('.'); + + return new AssertionError(exceptionMessage.toString()); + } + + /** + * Checks if two types are the same or are equivalent under a variable mapping + * given in the type map that was provided. + */ + private static boolean matches(Type from, Type to, Map<String, Type> typeMap) { + return to.equals(from) + || (from instanceof TypeVariable + && to.equals(typeMap.get(((TypeVariable<?>) from).getName()))); + + } + + @Override public final int hashCode() { + return this.hashCode; + } + + @Override public final boolean equals(Object o) { + return o instanceof TypeToken<?> + && $Gson$Types.equals(type, ((TypeToken<?>) o).type); + } + + @Override public final String toString() { + return $Gson$Types.typeToString(type); + } + + /** + * Gets type literal for the given {@code Type} instance. + */ + public static TypeToken<?> get(Type type) { + return new TypeToken<Object>(type); + } + + /** + * Gets type literal for the given {@code Class} instance. + */ + public static <T> TypeToken<T> get(Class<T> type) { + return new TypeToken<T>(type); + } +} diff --git a/gson/src/main/java/com/google/gson/reflect/package-info.java b/gson/src/main/java/com/google/gson/reflect/package-info.java new file mode 100644 index 00000000..e666c431 --- /dev/null +++ b/gson/src/main/java/com/google/gson/reflect/package-info.java @@ -0,0 +1,6 @@ +/** + * This package provides utility classes for finding type information for generic types. + * + * @author Inderjeet Singh, Joel Leitch + */ +package com.google.gson.reflect;
\ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java new file mode 100644 index 00000000..388f30b0 --- /dev/null +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -0,0 +1,1624 @@ +/* + * Copyright (C) 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 com.google.gson.stream; + +import com.google.gson.internal.JsonReaderInternalAccess; +import com.google.gson.internal.bind.JsonTreeReader; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; + +/** + * 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 for 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 a JSON stream in Java?", + * "geo": null, + * "user": { + * "name": "json_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@json_newb just use 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")); + * try { + * return readMessagesArray(reader); + * } finally { + * reader.close(); + * } + * } + * + * 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. + * + * <a name="nonexecuteprefix"/><h3>Non-Execute Prefix</h3> + * Web servers that serve private data using JSON may be vulnerable to <a + * href="http://en.wikipedia.org/wiki/JSON#Cross-site_request_forgery">Cross-site + * request forgery</a> attacks. In such an attack, a malicious site gains access + * to a private JSON file by executing it with an HTML {@code <script>} tag. + * + * <p>Prefixing JSON files with <code>")]}'\n"</code> makes them non-executable + * by {@code <script>} tags, disarming the attack. Since the prefix is malformed + * JSON, strict parsing fails when it is encountered. This class permits the + * non-execute prefix when {@link #setLenient(boolean) lenient parsing} is + * enabled. + * + * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + * + * @author Jesse Wilson + * @since 1.6 + */ +public class JsonReader implements Closeable { + /** The only non-execute prefix this parser permits */ + private static final char[] NON_EXECUTE_PREFIX = ")]}'\n".toCharArray(); + private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10; + + private static final int PEEKED_NONE = 0; + private static final int PEEKED_BEGIN_OBJECT = 1; + private static final int PEEKED_END_OBJECT = 2; + private static final int PEEKED_BEGIN_ARRAY = 3; + private static final int PEEKED_END_ARRAY = 4; + private static final int PEEKED_TRUE = 5; + private static final int PEEKED_FALSE = 6; + private static final int PEEKED_NULL = 7; + private static final int PEEKED_SINGLE_QUOTED = 8; + private static final int PEEKED_DOUBLE_QUOTED = 9; + private static final int PEEKED_UNQUOTED = 10; + /** When this is returned, the string value is stored in peekedString. */ + private static final int PEEKED_BUFFERED = 11; + private static final int PEEKED_SINGLE_QUOTED_NAME = 12; + private static final int PEEKED_DOUBLE_QUOTED_NAME = 13; + private static final int PEEKED_UNQUOTED_NAME = 14; + /** When this is returned, the integer value is stored in peekedLong. */ + private static final int PEEKED_LONG = 15; + private static final int PEEKED_NUMBER = 16; + private static final int PEEKED_EOF = 17; + + /* State machine when parsing numbers */ + private static final int NUMBER_CHAR_NONE = 0; + private static final int NUMBER_CHAR_SIGN = 1; + private static final int NUMBER_CHAR_DIGIT = 2; + private static final int NUMBER_CHAR_DECIMAL = 3; + private static final int NUMBER_CHAR_FRACTION_DIGIT = 4; + private static final int NUMBER_CHAR_EXP_E = 5; + private static final int NUMBER_CHAR_EXP_SIGN = 6; + private static final int NUMBER_CHAR_EXP_DIGIT = 7; + + /** 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. + * We decode literals directly out of this buffer, so it must be at least as + * long as the longest token that can be reported as a number. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + private int lineNumber = 0; + private int lineStart = 0; + + private int peeked = PEEKED_NONE; + + /** + * A peeked value that was composed entirely of digits with an optional + * leading dash. Positive values may not have a leading 0. + */ + private long peekedLong; + + /** + * The number of characters in a peeked number literal. Increment 'pos' by + * this after reading a number. + */ + private int peekedNumberLength; + + /** + * A peeked string that should be parsed on the next double, long or string. + * This is populated before a numeric value is parsed and used if that parsing + * fails. + */ + private String peekedString; + + /* + * The nesting stack. Using a manual array rather than an ArrayList saves 20%. + */ + private int[] stack = new int[32]; + private int stackSize = 0; + { + stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; + } + + /* + * The path members. It corresponds directly to stack: At indices where the + * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT), + * pathNames contains the name at this scope. Where it contains an array + * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in + * that array. Otherwise the value is undefined, and we take advantage of that + * by incrementing pathIndices when doing so isn't useful. + */ + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + /** + * 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>Streams that start with the <a href="#nonexecuteprefix">non-execute + * prefix</a>, <code>")]}'\n"</code>. + * <li>Streams that include multiple top-level values. With strict parsing, + * each stream must contain exactly one top-level value. + * <li>Top-level values of any type. With strict parsing, the top-level + * value must be an object or an array. + * <li>Numbers may be {@link Double#isNaN() NaNs} or {@link + * Double#isInfinite() infinities}. + * <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 final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this parser is liberal in what it accepts. + */ + public final boolean isLenient() { + return 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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_BEGIN_ARRAY) { + push(JsonScope.EMPTY_ARRAY); + pathIndices[stackSize - 1] = 0; + peeked = PEEKED_NONE; + } else { + throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_END_ARRAY) { + stackSize--; + pathIndices[stackSize - 1]++; + peeked = PEEKED_NONE; + } else { + throw new IllegalStateException("Expected END_ARRAY but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_BEGIN_OBJECT) { + push(JsonScope.EMPTY_OBJECT); + peeked = PEEKED_NONE; + } else { + throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current object. + */ + public void endObject() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_END_OBJECT) { + stackSize--; + pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! + pathIndices[stackSize - 1]++; + peeked = PEEKED_NONE; + } else { + throw new IllegalStateException("Expected END_OBJECT but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + switch (p) { + case PEEKED_BEGIN_OBJECT: + return JsonToken.BEGIN_OBJECT; + case PEEKED_END_OBJECT: + return JsonToken.END_OBJECT; + case PEEKED_BEGIN_ARRAY: + return JsonToken.BEGIN_ARRAY; + case PEEKED_END_ARRAY: + return JsonToken.END_ARRAY; + case PEEKED_SINGLE_QUOTED_NAME: + case PEEKED_DOUBLE_QUOTED_NAME: + case PEEKED_UNQUOTED_NAME: + return JsonToken.NAME; + case PEEKED_TRUE: + case PEEKED_FALSE: + return JsonToken.BOOLEAN; + case PEEKED_NULL: + return JsonToken.NULL; + case PEEKED_SINGLE_QUOTED: + case PEEKED_DOUBLE_QUOTED: + case PEEKED_UNQUOTED: + case PEEKED_BUFFERED: + return JsonToken.STRING; + case PEEKED_LONG: + case PEEKED_NUMBER: + return JsonToken.NUMBER; + case PEEKED_EOF: + return JsonToken.END_DOCUMENT; + default: + throw new AssertionError(); + } + } + + private int doPeek() throws IOException { + int peekStack = stack[stackSize - 1]; + if (peekStack == JsonScope.EMPTY_ARRAY) { + stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; + } else if (peekStack == JsonScope.NONEMPTY_ARRAY) { + // Look for a comma before the next element. + int c = nextNonWhitespace(true); + switch (c) { + case ']': + return peeked = PEEKED_END_ARRAY; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated array"); + } + } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { + stack[stackSize - 1] = JsonScope.DANGLING_NAME; + // Look for a comma before the next element. + if (peekStack == JsonScope.NONEMPTY_OBJECT) { + int c = nextNonWhitespace(true); + switch (c) { + case '}': + return peeked = PEEKED_END_OBJECT; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + int c = nextNonWhitespace(true); + switch (c) { + case '"': + return peeked = PEEKED_DOUBLE_QUOTED_NAME; + case '\'': + checkLenient(); + return peeked = PEEKED_SINGLE_QUOTED_NAME; + case '}': + if (peekStack != JsonScope.NONEMPTY_OBJECT) { + return peeked = PEEKED_END_OBJECT; + } else { + throw syntaxError("Expected name"); + } + default: + checkLenient(); + pos--; // Don't consume the first character in an unquoted string. + if (isLiteral((char) c)) { + return peeked = PEEKED_UNQUOTED_NAME; + } else { + throw syntaxError("Expected name"); + } + } + } else if (peekStack == JsonScope.DANGLING_NAME) { + stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; + // Look for a colon before the value. + int c = nextNonWhitespace(true); + switch (c) { + case ':': + break; + case '=': + checkLenient(); + if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + break; + default: + throw syntaxError("Expected ':'"); + } + } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { + if (lenient) { + consumeNonExecutePrefix(); + } + stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; + } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) { + int c = nextNonWhitespace(false); + if (c == -1) { + return peeked = PEEKED_EOF; + } else { + checkLenient(); + pos--; + } + } else if (peekStack == JsonScope.CLOSED) { + throw new IllegalStateException("JsonReader is closed"); + } + + int c = nextNonWhitespace(true); + switch (c) { + case ']': + if (peekStack == JsonScope.EMPTY_ARRAY) { + return peeked = PEEKED_END_ARRAY; + } + // fall-through to handle ",]" + case ';': + case ',': + // In lenient mode, a 0-length literal in an array means 'null'. + if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) { + checkLenient(); + pos--; + return peeked = PEEKED_NULL; + } else { + throw syntaxError("Unexpected value"); + } + case '\'': + checkLenient(); + return peeked = PEEKED_SINGLE_QUOTED; + case '"': + if (stackSize == 1) { + checkLenient(); + } + return peeked = PEEKED_DOUBLE_QUOTED; + case '[': + return peeked = PEEKED_BEGIN_ARRAY; + case '{': + return peeked = PEEKED_BEGIN_OBJECT; + default: + pos--; // Don't consume the first character in a literal value. + } + + if (stackSize == 1) { + checkLenient(); // Top-level value isn't an array or an object. + } + + int result = peekKeyword(); + if (result != PEEKED_NONE) { + return result; + } + + result = peekNumber(); + if (result != PEEKED_NONE) { + return result; + } + + if (!isLiteral(buffer[pos])) { + throw syntaxError("Expected value"); + } + + checkLenient(); + return peeked = PEEKED_UNQUOTED; + } + + private int peekKeyword() throws IOException { + // Figure out which keyword we're matching against by its first character. + char c = buffer[pos]; + String keyword; + String keywordUpper; + int peeking; + if (c == 't' || c == 'T') { + keyword = "true"; + keywordUpper = "TRUE"; + peeking = PEEKED_TRUE; + } else if (c == 'f' || c == 'F') { + keyword = "false"; + keywordUpper = "FALSE"; + peeking = PEEKED_FALSE; + } else if (c == 'n' || c == 'N') { + keyword = "null"; + keywordUpper = "NULL"; + peeking = PEEKED_NULL; + } else { + return PEEKED_NONE; + } + + // Confirm that chars [1..length) match the keyword. + int length = keyword.length(); + for (int i = 1; i < length; i++) { + if (pos + i >= limit && !fillBuffer(i + 1)) { + return PEEKED_NONE; + } + c = buffer[pos + i]; + if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { + return PEEKED_NONE; + } + } + + if ((pos + length < limit || fillBuffer(length + 1)) + && isLiteral(buffer[pos + length])) { + return PEEKED_NONE; // Don't match trues, falsey or nullsoft! + } + + // We've found the keyword followed either by EOF or by a non-literal character. + pos += length; + return peeked = peeking; + } + + private int peekNumber() throws IOException { + // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. + char[] buffer = this.buffer; + int p = pos; + int l = limit; + + long value = 0; // Negative to accommodate Long.MIN_VALUE more easily. + boolean negative = false; + boolean fitsInLong = true; + int last = NUMBER_CHAR_NONE; + + int i = 0; + + charactersOfNumber: + for (; true; i++) { + if (p + i == l) { + if (i == buffer.length) { + // Though this looks like a well-formed number, it's too long to continue reading. Give up + // and let the application handle this as an unquoted literal. + return PEEKED_NONE; + } + if (!fillBuffer(i + 1)) { + break; + } + p = pos; + l = limit; + } + + char c = buffer[p + i]; + switch (c) { + case '-': + if (last == NUMBER_CHAR_NONE) { + negative = true; + last = NUMBER_CHAR_SIGN; + continue; + } else if (last == NUMBER_CHAR_EXP_E) { + last = NUMBER_CHAR_EXP_SIGN; + continue; + } + return PEEKED_NONE; + + case '+': + if (last == NUMBER_CHAR_EXP_E) { + last = NUMBER_CHAR_EXP_SIGN; + continue; + } + return PEEKED_NONE; + + case 'e': + case 'E': + if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) { + last = NUMBER_CHAR_EXP_E; + continue; + } + return PEEKED_NONE; + + case '.': + if (last == NUMBER_CHAR_DIGIT) { + last = NUMBER_CHAR_DECIMAL; + continue; + } + return PEEKED_NONE; + + default: + if (c < '0' || c > '9') { + if (!isLiteral(c)) { + break charactersOfNumber; + } + return PEEKED_NONE; + } + if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) { + value = -(c - '0'); + last = NUMBER_CHAR_DIGIT; + } else if (last == NUMBER_CHAR_DIGIT) { + if (value == 0) { + return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal). + } + long newValue = value * 10 - (c - '0'); + fitsInLong &= value > MIN_INCOMPLETE_INTEGER + || (value == MIN_INCOMPLETE_INTEGER && newValue < value); + value = newValue; + } else if (last == NUMBER_CHAR_DECIMAL) { + last = NUMBER_CHAR_FRACTION_DIGIT; + } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) { + last = NUMBER_CHAR_EXP_DIGIT; + } + } + } + + // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER. + if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) { + peekedLong = negative ? value : -value; + pos += i; + return peeked = PEEKED_LONG; + } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT + || last == NUMBER_CHAR_EXP_DIGIT) { + peekedNumberLength = i; + return peeked = PEEKED_NUMBER; + } else { + return PEEKED_NONE; + } + } + + private boolean isLiteral(char c) throws IOException { + 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': + return false; + default: + return true; + } + } + + /** + * Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and + * consumes it. + * + * @throws java.io.IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + String result; + if (p == PEEKED_UNQUOTED_NAME) { + result = nextUnquotedValue(); + } else if (p == PEEKED_SINGLE_QUOTED_NAME) { + result = nextQuotedValue('\''); + } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { + result = nextQuotedValue('"'); + } else { + throw new IllegalStateException("Expected a name but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peeked = PEEKED_NONE; + pathNames[stackSize - 1] = result; + return result; + } + + /** + * Returns the {@link com.google.gson.stream.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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + String result; + if (p == PEEKED_UNQUOTED) { + result = nextUnquotedValue(); + } else if (p == PEEKED_SINGLE_QUOTED) { + result = nextQuotedValue('\''); + } else if (p == PEEKED_DOUBLE_QUOTED) { + result = nextQuotedValue('"'); + } else if (p == PEEKED_BUFFERED) { + result = peekedString; + peekedString = null; + } else if (p == PEEKED_LONG) { + result = Long.toString(peekedLong); + } else if (p == PEEKED_NUMBER) { + result = new String(buffer, pos, peekedNumberLength); + pos += peekedNumberLength; + } else { + throw new IllegalStateException("Expected a string but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + /** + * Returns the {@link com.google.gson.stream.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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_TRUE) { + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return true; + } else if (p == PEEKED_FALSE) { + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return false; + } + throw new IllegalStateException("Expected a boolean but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + + /** + * 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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + if (p == PEEKED_NULL) { + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + } else { + throw new IllegalStateException("Expected null but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + } + + /** + * Returns the {@link com.google.gson.stream.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 using {@link Double#parseDouble(String)}. + * + * @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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + if (p == PEEKED_LONG) { + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return (double) peekedLong; + } + + if (p == PEEKED_NUMBER) { + peekedString = new String(buffer, pos, peekedNumberLength); + pos += peekedNumberLength; + } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { + peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + } else if (p == PEEKED_UNQUOTED) { + peekedString = nextUnquotedValue(); + } else if (p != PEEKED_BUFFERED) { + throw new IllegalStateException("Expected a double but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + + peeked = PEEKED_BUFFERED; + double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { + throw new MalformedJsonException("JSON forbids NaN and infinities: " + result + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peekedString = null; + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + /** + * Returns the {@link com.google.gson.stream.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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + if (p == PEEKED_LONG) { + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return peekedLong; + } + + if (p == PEEKED_NUMBER) { + peekedString = new String(buffer, pos, peekedNumberLength); + pos += peekedNumberLength; + } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { + peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + try { + long result = Long.parseLong(peekedString); + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } catch (NumberFormatException ignored) { + // Fall back to parse as a double below. + } + } else { + throw new IllegalStateException("Expected a long but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + + peeked = PEEKED_BUFFERED; + double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + long result = (long) asDouble; + if (result != asDouble) { // Make sure no precision was lost casting to 'long'. + throw new NumberFormatException("Expected a long but was " + peekedString + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peekedString = null; + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + /** + * 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 nextQuotedValue(char quote) throws IOException { + // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. + char[] buffer = this.buffer; + StringBuilder builder = new StringBuilder(); + while (true) { + int p = pos; + int l = limit; + /* the index of the first character not yet appended to the builder. */ + int start = p; + while (p < l) { + int c = buffer[p++]; + + if (c == quote) { + pos = p; + builder.append(buffer, start, p - start - 1); + return builder.toString(); + } else if (c == '\\') { + pos = p; + builder.append(buffer, start, p - start - 1); + builder.append(readEscapeCharacter()); + p = pos; + l = limit; + start = p; + } else if (c == '\n') { + lineNumber++; + lineStart = p; + } + } + + builder.append(buffer, start, p - start); + pos = p; + if (!fillBuffer(1)) { + throw syntaxError("Unterminated string"); + } + } + } + + /** + * Returns an unquoted value as a string. + */ + @SuppressWarnings("fallthrough") + private String nextUnquotedValue() throws IOException { + StringBuilder builder = null; + int i = 0; + + findNonLiteralCharacter: + while (true) { + for (; pos + i < limit; i++) { + switch (buffer[pos + i]) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + break findNonLiteralCharacter; + } + } + + // Attempt to load the entire literal into the buffer at once. + if (i < buffer.length) { + if (fillBuffer(i + 1)) { + continue; + } else { + break; + } + } + + // use a StringBuilder when the value is too long. This is too long to be a number! + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, pos, i); + pos += i; + i = 0; + if (!fillBuffer(1)) { + break; + } + } + + String result; + if (builder == null) { + result = new String(buffer, pos, i); + } else { + builder.append(buffer, pos, i); + result = builder.toString(); + } + pos += i; + return result; + } + + private void skipQuotedValue(char quote) throws IOException { + // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. + char[] buffer = this.buffer; + do { + int p = pos; + int l = limit; + /* the index of the first character not yet appended to the builder. */ + while (p < l) { + int c = buffer[p++]; + if (c == quote) { + pos = p; + return; + } else if (c == '\\') { + pos = p; + readEscapeCharacter(); + p = pos; + l = limit; + } else if (c == '\n') { + lineNumber++; + lineStart = p; + } + } + pos = p; + } while (fillBuffer(1)); + throw syntaxError("Unterminated string"); + } + + private void skipUnquotedValue() throws IOException { + do { + int i = 0; + for (; pos + i < limit; i++) { + switch (buffer[pos + i]) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + pos += i; + return; + } + } + pos += i; + } while (fillBuffer(1)); + } + + /** + * Returns the {@link com.google.gson.stream.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 { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + int result; + if (p == PEEKED_LONG) { + result = (int) peekedLong; + if (peekedLong != result) { // Make sure no precision was lost casting to 'int'. + throw new NumberFormatException("Expected an int but was " + peekedLong + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + if (p == PEEKED_NUMBER) { + peekedString = new String(buffer, pos, peekedNumberLength); + pos += peekedNumberLength; + } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { + peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + try { + result = Integer.parseInt(peekedString); + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } catch (NumberFormatException ignored) { + // Fall back to parse as a double below. + } + } else { + throw new IllegalStateException("Expected an int but was " + peek() + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + + peeked = PEEKED_BUFFERED; + double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. + result = (int) asDouble; + if (result != asDouble) { // Make sure no precision was lost casting to 'int'. + throw new NumberFormatException("Expected an int but was " + peekedString + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + peekedString = null; + peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; + return result; + } + + /** + * Closes this JSON reader and the underlying {@link java.io.Reader}. + */ + public void close() throws IOException { + peeked = PEEKED_NONE; + stack[0] = JsonScope.CLOSED; + stackSize = 1; + 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 { + int count = 0; + do { + int p = peeked; + if (p == PEEKED_NONE) { + p = doPeek(); + } + + if (p == PEEKED_BEGIN_ARRAY) { + push(JsonScope.EMPTY_ARRAY); + count++; + } else if (p == PEEKED_BEGIN_OBJECT) { + push(JsonScope.EMPTY_OBJECT); + count++; + } else if (p == PEEKED_END_ARRAY) { + stackSize--; + count--; + } else if (p == PEEKED_END_OBJECT) { + stackSize--; + count--; + } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { + skipUnquotedValue(); + } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { + skipQuotedValue('\''); + } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { + skipQuotedValue('"'); + } else if (p == PEEKED_NUMBER) { + pos += peekedNumberLength; + } + peeked = PEEKED_NONE; + } while (count != 0); + + pathIndices[stackSize - 1]++; + pathNames[stackSize - 1] = "null"; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + int[] newStack = new int[stackSize * 2]; + int[] newPathIndices = new int[stackSize * 2]; + String[] newPathNames = new String[stackSize * 2]; + System.arraycopy(stack, 0, newStack, 0, stackSize); + System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize); + System.arraycopy(pathNames, 0, newPathNames, 0, stackSize); + stack = newStack; + pathIndices = newPathIndices; + pathNames = newPathNames; + } + stack[stackSize++] = newTop; + } + + /** + * 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 { + char[] buffer = this.buffer; + lineStart -= pos; + 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 this is the first read, consume an optional byte order mark (BOM) if it exists + if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') { + pos++; + lineStart++; + minimum++; + } + + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int getLineNumber() { + return lineNumber + 1; + } + + private int getColumnNumber() { + return pos - lineStart + 1; + } + + /** + * Returns the next character in the stream that is neither whitespace nor a + * part of a comment. When this returns, the returned character is always at + * {@code buffer[pos-1]}; this means the caller can always push back the + * returned character by decrementing {@code pos}. + */ + private int nextNonWhitespace(boolean throwOnEof) throws IOException { + /* + * This code uses ugly local variables 'p' and 'l' representing the 'pos' + * and 'limit' fields respectively. Using locals rather than fields saves + * a few field reads for each whitespace character in a pretty-printed + * document, resulting in a 5% speedup. We need to flush 'p' to its field + * before any (potentially indirect) call to fillBuffer() and reread both + * 'p' and 'l' after any (potentially indirect) call to the same method. + */ + char[] buffer = this.buffer; + int p = pos; + int l = limit; + while (true) { + if (p == l) { + pos = p; + if (!fillBuffer(1)) { + break; + } + p = pos; + l = limit; + } + + int c = buffer[p++]; + if (c == '\n') { + lineNumber++; + lineStart = p; + continue; + } else if (c == ' ' || c == '\r' || c == '\t') { + continue; + } + + if (c == '/') { + pos = p; + if (p == l) { + pos--; // push back '/' so it's still in the buffer when this method returns + boolean charsLoaded = fillBuffer(2); + pos++; // consume the '/' again + if (!charsLoaded) { + return c; + } + } + + checkLenient(); + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + p = pos + 2; + l = limit; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + p = pos; + l = limit; + continue; + + default: + return c; + } + } else if (c == '#') { + pos = p; + /* + * 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(); + p = pos; + l = limit; + } else { + pos = p; + return c; + } + } + if (throwOnEof) { + throw new EOFException("End of input" + + " at line " + getLineNumber() + " column " + getColumnNumber()); + } else { + return -1; + } + } + + 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 == '\n') { + lineNumber++; + lineStart = pos; + break; + } else if (c == '\r') { + break; + } + } + } + + /** + * @param toFind a string to search for. Must not contain a newline. + */ + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) { + if (buffer[pos] == '\n') { + lineNumber++; + lineStart = pos + 1; + continue; + } + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + @Override public String toString() { + return getClass().getSimpleName() + + " at line " + getLineNumber() + " column " + getColumnNumber(); + } + + /** + * Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to + * the current location in the JSON value. + */ + public String getPath() { + StringBuilder result = new StringBuilder().append('$'); + for (int i = 0, size = stackSize; i < size; i++) { + switch (stack[i]) { + case JsonScope.EMPTY_ARRAY: + case JsonScope.NONEMPTY_ARRAY: + result.append('[').append(pathIndices[i]).append(']'); + break; + + case JsonScope.EMPTY_OBJECT: + case JsonScope.DANGLING_NAME: + case JsonScope.NONEMPTY_OBJECT: + result.append('.'); + if (pathNames[i] != null) { + result.append(pathNames[i]); + } + break; + + case JsonScope.NONEMPTY_DOCUMENT: + case JsonScope.EMPTY_DOCUMENT: + case JsonScope.CLOSED: + break; + } + } + return result.toString(); + } + + /** + * 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"); + } + // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16); + char result = 0; + for (int i = pos, end = i + 4; i < end; i++) { + char c = buffer[i]; + result <<= 4; + if (c >= '0' && c <= '9') { + result += (c - '0'); + } else if (c >= 'a' && c <= 'f') { + result += (c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + result += (c - 'A' + 10); + } else { + throw new NumberFormatException("\\u" + new String(buffer, pos, 4)); + } + } + pos += 4; + return result; + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\n': + lineNumber++; + lineStart = pos; + // fall-through + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + private IOException syntaxError(String message) throws IOException { + throw new MalformedJsonException(message + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); + } + + /** + * Consumes the non-execute prefix if it exists. + */ + private void consumeNonExecutePrefix() throws IOException { + // fast forward through the leading whitespace + nextNonWhitespace(true); + pos--; + + if (pos + NON_EXECUTE_PREFIX.length > limit && !fillBuffer(NON_EXECUTE_PREFIX.length)) { + return; + } + + for (int i = 0; i < NON_EXECUTE_PREFIX.length; i++) { + if (buffer[pos + i] != NON_EXECUTE_PREFIX[i]) { + return; // not a security token! + } + } + + // we consumed a security token! + pos += NON_EXECUTE_PREFIX.length; + } + + static { + JsonReaderInternalAccess.INSTANCE = new JsonReaderInternalAccess() { + @Override public void promoteNameToValue(JsonReader reader) throws IOException { + if (reader instanceof JsonTreeReader) { + ((JsonTreeReader)reader).promoteNameToValue(); + return; + } + int p = reader.peeked; + if (p == PEEKED_NONE) { + p = reader.doPeek(); + } + if (p == PEEKED_DOUBLE_QUOTED_NAME) { + reader.peeked = PEEKED_DOUBLE_QUOTED; + } else if (p == PEEKED_SINGLE_QUOTED_NAME) { + reader.peeked = PEEKED_SINGLE_QUOTED; + } else if (p == PEEKED_UNQUOTED_NAME) { + reader.peeked = PEEKED_UNQUOTED; + } else { + throw new IllegalStateException("Expected a name but was " + reader.peek() + " " + + " at line " + reader.getLineNumber() + " column " + reader.getColumnNumber() + + " path " + reader.getPath()); + } + } + }; + } +} diff --git a/gson/src/main/java/com/google/gson/stream/JsonScope.java b/gson/src/main/java/com/google/gson/stream/JsonScope.java new file mode 100644 index 00000000..da691372 --- /dev/null +++ b/gson/src/main/java/com/google/gson/stream/JsonScope.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 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 com.google.gson.stream; + +/** + * Lexical scoping elements within a JSON reader or writer. + * + * @author Jesse Wilson + * @since 1.6 + */ +final class JsonScope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + static final int EMPTY_ARRAY = 1; + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + static final int NONEMPTY_ARRAY = 2; + + /** + * An object with no name/value pairs requires no separators or newlines + * before it is closed. + */ + static final int EMPTY_OBJECT = 3; + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + static final int DANGLING_NAME = 4; + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + static final int NONEMPTY_OBJECT = 5; + + /** + * No object or array has been started. + */ + static final int EMPTY_DOCUMENT = 6; + + /** + * A document with at an array or object. + */ + static final int NONEMPTY_DOCUMENT = 7; + + /** + * A document that's been closed and cannot be accessed. + */ + static final int CLOSED = 8; +} diff --git a/gson/src/main/java/com/google/gson/stream/JsonToken.java b/gson/src/main/java/com/google/gson/stream/JsonToken.java new file mode 100644 index 00000000..f1025b3f --- /dev/null +++ b/gson/src/main/java/com/google/gson/stream/JsonToken.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 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 com.google.gson.stream; + +/** + * A structure, name or value type in a JSON-encoded string. + * + * @author Jesse Wilson + * @since 1.6 + */ +public enum JsonToken { + + /** + * The opening of a JSON array. Written using {@link JsonWriter#beginArray} + * and read using {@link JsonReader#beginArray}. + */ + 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/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java new file mode 100644 index 00000000..8d3bdb34 --- /dev/null +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -0,0 +1,651 @@ +/* + * Copyright (C) 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 com.google.gson.stream; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.Writer; + +import static com.google.gson.stream.JsonScope.DANGLING_NAME; +import static com.google.gson.stream.JsonScope.EMPTY_ARRAY; +import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT; +import static com.google.gson.stream.JsonScope.EMPTY_OBJECT; +import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY; +import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT; +import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; + +/** + * 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 stream JSON in Java?", + * "geo": null, + * "user": { + * "name": "json_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@json_newb just use 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.setIndent(" "); + * 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}. + * + * @author Jesse Wilson + * @since 1.6 + */ +public class JsonWriter implements Closeable, Flushable { + + /* + * 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)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets as + * newline characters. This prevents eval() from failing with a syntax + * error. http://code.google.com/p/google-gson/issues/detail?id=341 + */ + private static final String[] REPLACEMENT_CHARS; + private static final String[] HTML_SAFE_REPLACEMENT_CHARS; + static { + REPLACEMENT_CHARS = new String[128]; + for (int i = 0; i <= 0x1f; i++) { + REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); + } + REPLACEMENT_CHARS['"'] = "\\\""; + REPLACEMENT_CHARS['\\'] = "\\\\"; + REPLACEMENT_CHARS['\t'] = "\\t"; + REPLACEMENT_CHARS['\b'] = "\\b"; + REPLACEMENT_CHARS['\n'] = "\\n"; + REPLACEMENT_CHARS['\r'] = "\\r"; + REPLACEMENT_CHARS['\f'] = "\\f"; + HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); + HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; + HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; + HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; + HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; + HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027"; + } + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private int[] stack = new int[32]; + private int stackSize = 0; + { + push(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 = ":"; + + private boolean lenient; + + private boolean htmlSafe; + + private String deferredName; + + private boolean serializeNulls = true; + + /** + * 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 final void setIndent(String indent) { + if (indent.length() == 0) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Configure this writer to relax its syntax rules. By default, this writer + * only emits well-formed JSON as specified by <a + * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the writer + * to lenient permits the following: + * <ul> + * <li>Top-level values of any type. With strict writing, the top-level + * value must be an object or an array. + * <li>Numbers may be {@link Double#isNaN() NaNs} or {@link + * Double#isInfinite() infinities}. + * </ul> + */ + public final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this writer has relaxed syntax rules. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Configure this writer to emit JSON that's safe for direct inclusion in HTML + * and XML documents. This escapes the HTML characters {@code <}, {@code >}, + * {@code &} and {@code =} before writing them to the stream. Without this + * setting, your XML/HTML encoder should replace these characters with the + * corresponding escape sequences. + */ + public final void setHtmlSafe(boolean htmlSafe) { + this.htmlSafe = htmlSafe; + } + + /** + * Returns true if this writer writes JSON that's safe for inclusion in HTML + * and XML documents. + */ + public final boolean isHtmlSafe() { + return htmlSafe; + } + + /** + * Sets whether object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + /** + * Returns true if object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final boolean getSerializeNulls() { + return serializeNulls; + } + + /** + * 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 { + writeDeferredName(); + return open(EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(EMPTY_ARRAY, 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 { + writeDeferredName(); + return open(EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(int empty, String openBracket) throws IOException { + beforeValue(true); + push(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(int empty, int nonempty, String closeBracket) + throws IOException { + int context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } + + stackSize--; + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + private void push(int newTop) { + if (stackSize == stack.length) { + int[] newStack = new int[stackSize * 2]; + System.arraycopy(stack, 0, newStack, 0, stackSize); + stack = newStack; + } + stack[stackSize++] = newTop; + } + + /** + * Returns the value on the top of the stack. + */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(int topOfStack) { + stack[stackSize - 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"); + } + if (deferredName != null) { + throw new IllegalStateException(); + } + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + deferredName = name; + return this; + } + + private void writeDeferredName() throws IOException { + if (deferredName != null) { + beforeName(); + string(deferredName); + deferredName = null; + } + } + + /** + * 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(); + } + writeDeferredName(); + beforeValue(false); + string(value); + return this; + } + + /** + * Writes {@code value} directly to the writer without quoting or + * escaping. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter jsonValue(String value) throws IOException { + if (value == null) { + return nullValue(); + } + writeDeferredName(); + beforeValue(false); + out.append(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + if (deferredName != null) { + if (serializeNulls) { + writeDeferredName(); + } else { + deferredName = null; + return this; // skip the name and the value + } + } + beforeValue(false); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + writeDeferredName(); + 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); + } + writeDeferredName(); + beforeValue(false); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + writeDeferredName(); + beforeValue(false); + out.write(Long.toString(value)); + 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(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + writeDeferredName(); + String string = value.toString(); + if (!lenient + && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(false); + out.append(string); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + public void flush() throws IOException { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + 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(); + + int size = stackSize; + if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + stackSize = 0; + } + + private void string(String value) throws IOException { + String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS; + out.write("\""); + int last = 0; + int length = value.length(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + String replacement; + if (c < 128) { + replacement = replacements[c]; + if (replacement == null) { + continue; + } + } else if (c == '\u2028') { + replacement = "\\u2028"; + } else if (c == '\u2029') { + replacement = "\\u2029"; + } else { + continue; + } + if (last < i) { + out.write(value, last, i - last); + } + out.write(replacement); + last = i + 1; + } + if (last < length) { + out.write(value, last, length - last); + } + out.write("\""); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write("\n"); + for (int i = 1, size = stackSize; i < 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 { + int context = peek(); + if (context == NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem."); + } + newline(); + replaceTop(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. + */ + @SuppressWarnings("fallthrough") + private void beforeValue(boolean root) throws IOException { + switch (peek()) { + case NONEMPTY_DOCUMENT: + if (!lenient) { + throw new IllegalStateException( + "JSON must have only one top-level value."); + } + // fall-through + case EMPTY_DOCUMENT: // first in document + if (!lenient && !root) { + throw new IllegalStateException( + "JSON must start with an array or an object."); + } + replaceTop(NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case DANGLING_NAME: // value for name + out.append(separator); + replaceTop(NONEMPTY_OBJECT); + break; + + default: + throw new IllegalStateException("Nesting problem."); + } + } +} diff --git a/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java b/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java new file mode 100644 index 00000000..9da70ebc --- /dev/null +++ b/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 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 com.google.gson.stream; + +import java.io.IOException; + +/** + * Thrown when a reader encounters malformed JSON. Some syntax errors can be + * ignored by calling {@link JsonReader#setLenient(boolean)}. + */ +public final class MalformedJsonException extends IOException { + private static final long serialVersionUID = 1L; + + public MalformedJsonException(String msg) { + super(msg); + } + + public MalformedJsonException(String msg, Throwable throwable) { + super(msg); + // Using initCause() instead of calling super() because Java 1.5 didn't retrofit IOException + // with a constructor with Throwable. This was done in Java 1.6 + initCause(throwable); + } + + public MalformedJsonException(Throwable throwable) { + // Using initCause() instead of calling super() because Java 1.5 didn't retrofit IOException + // with a constructor with Throwable. This was done in Java 1.6 + initCause(throwable); + } +} |