diff options
Diffstat (limited to 'gson/src')
154 files changed, 30719 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); + } +} diff --git a/gson/src/test/java/com/google/gson/CommentsTest.java b/gson/src/test/java/com/google/gson/CommentsTest.java new file mode 100644 index 00000000..306e5aff --- /dev/null +++ b/gson/src/test/java/com/google/gson/CommentsTest.java @@ -0,0 +1,45 @@ +/* + * 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; + +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; + +/** + * @author Jesse Wilson + */ +public final class CommentsTest extends TestCase { + + /** + * Test for issue 212. + */ + public void testParseComments() { + String json = "[\n" + + " // this is a comment\n" + + " \"a\",\n" + + " /* this is another comment */\n" + + " \"b\",\n" + + " # this is yet another comment\n" + + " \"c\"\n" + + "]"; + + List<String> abc = new Gson().fromJson(json, new TypeToken<List<String>>() {}.getType()); + assertEquals(Arrays.asList("a", "b", "c"), abc); + } +} diff --git a/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java new file mode 100644 index 00000000..966a8c18 --- /dev/null +++ b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java @@ -0,0 +1,163 @@ +/* + * 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.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import junit.framework.TestCase; + +/** + * A simple unit test for the {@link DefaultDateTypeAdapter} class. + * + * @author Joel Leitch + */ +public class DefaultDateTypeAdapterTest extends TestCase { + + public void testFormattingInEnUs() { + assertFormattingAlwaysEmitsUsLocale(Locale.US); + } + + public void testFormattingInFr() { + assertFormattingAlwaysEmitsUsLocale(Locale.FRANCE); + } + + private void assertFormattingAlwaysEmitsUsLocale(Locale locale) { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(locale); + try { + assertFormatted("Jan 1, 1970 12:00:00 AM", new DefaultDateTypeAdapter()); + assertFormatted("1/1/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertFormatted("Jan 1, 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertFormatted("January 1, 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertFormatted("1/1/70 12:00 AM", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertFormatted("Jan 1, 1970 12:00:00 AM", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertFormatted("January 1, 1970 12:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertFormatted("Thursday, January 1, 1970 12:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testParsingDatesFormattedWithSystemLocale() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.FRANCE); + try { + assertParsed("1 janv. 1970 00:00:00", new DefaultDateTypeAdapter()); + assertParsed("01/01/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertParsed("1 janv. 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertParsed("1 janvier 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertParsed("01/01/70 00:00", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertParsed("1 janv. 1970 00:00:00", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertParsed("1 janvier 1970 00:00:00 UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertParsed("jeudi 1 janvier 1970 00 h 00 UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testParsingDatesFormattedWithUsLocale() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + assertParsed("Jan 1, 1970 0:00:00 AM", new DefaultDateTypeAdapter()); + assertParsed("1/1/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertParsed("Jan 1, 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertParsed("January 1, 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertParsed("1/1/70 0:00 AM", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertParsed("Jan 1, 1970 0:00:00 AM", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertParsed("January 1, 1970 0:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertParsed("Thursday, January 1, 1970 0:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testFormatUsesDefaultTimezone() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + assertFormatted("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter()); + assertParsed("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter()); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testDateSerialization() throws Exception { + int dateStyle = DateFormat.LONG; + DefaultDateTypeAdapter dateTypeAdapter = new DefaultDateTypeAdapter(dateStyle); + DateFormat formatter = DateFormat.getDateInstance(dateStyle, Locale.US); + Date currentDate = new Date(); + + String dateString = dateTypeAdapter.serialize(currentDate, Date.class, null).getAsString(); + assertEquals(formatter.format(currentDate), dateString); + } + + public void testDatePattern() throws Exception { + String pattern = "yyyy-MM-dd"; + DefaultDateTypeAdapter dateTypeAdapter = new DefaultDateTypeAdapter(pattern); + DateFormat formatter = new SimpleDateFormat(pattern); + Date currentDate = new Date(); + + String dateString = dateTypeAdapter.serialize(currentDate, Date.class, null).getAsString(); + assertEquals(formatter.format(currentDate), dateString); + } + + public void testInvalidDatePattern() throws Exception { + try { + new DefaultDateTypeAdapter("I am a bad Date pattern...."); + fail("Invalid date pattern should fail."); + } catch (IllegalArgumentException expected) { } + } + + private void assertFormatted(String formatted, DefaultDateTypeAdapter adapter) { + assertEquals(formatted, adapter.serialize(new Date(0), Date.class, null).getAsString()); + } + + private void assertParsed(String date, DefaultDateTypeAdapter adapter) { + assertEquals(date, new Date(0), adapter.deserialize(new JsonPrimitive(date), Date.class, null)); + assertEquals("ISO 8601", new Date(0), adapter.deserialize( + new JsonPrimitive("1970-01-01T00:00:00Z"), Date.class, null)); + } +} diff --git a/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java b/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java new file mode 100644 index 00000000..6b853f5d --- /dev/null +++ b/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java @@ -0,0 +1,45 @@ +/* + * 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 java.net.InetAddress; + +import junit.framework.TestCase; + +/** + * Unit tests for the default serializer/deserializer for the {@code InetAddress} type. + * + * @author Joel Leitch + */ +public class DefaultInetAddressTypeAdapterTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testInetAddressSerializationAndDeserialization() throws Exception { + InetAddress address = InetAddress.getByName("8.8.8.8"); + String jsonAddress = gson.toJson(address); + assertEquals("\"8.8.8.8\"", jsonAddress); + + InetAddress value = gson.fromJson(jsonAddress, InetAddress.class); + assertEquals(value, address); + } +} diff --git a/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.java b/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.java new file mode 100644 index 00000000..5c061953 --- /dev/null +++ b/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.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; + +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import junit.framework.TestCase; + +/** + * Unit test for the default JSON map serialization object located in the + * {@link DefaultTypeAdapters} class. + * + * @author Joel Leitch + */ +public class DefaultMapJsonSerializerTest extends TestCase { + private Gson gson = new Gson(); + + public void testEmptyMapNoTypeSerialization() { + Map<String, String> emptyMap = new HashMap<String, String>(); + JsonElement element = gson.toJsonTree(emptyMap, emptyMap.getClass()); + assertTrue(element instanceof JsonObject); + JsonObject emptyMapJsonObject = (JsonObject) element; + assertTrue(emptyMapJsonObject.entrySet().isEmpty()); + } + + public void testEmptyMapSerialization() { + Type mapType = new TypeToken<Map<String, String>>() { }.getType(); + Map<String, String> emptyMap = new HashMap<String, String>(); + JsonElement element = gson.toJsonTree(emptyMap, mapType); + + assertTrue(element instanceof JsonObject); + JsonObject emptyMapJsonObject = (JsonObject) element; + assertTrue(emptyMapJsonObject.entrySet().isEmpty()); + } + + public void testNonEmptyMapSerialization() { + Type mapType = new TypeToken<Map<String, String>>() { }.getType(); + Map<String, String> myMap = new HashMap<String, String>(); + String key = "key1"; + myMap.put(key, "value1"); + Gson gson = new Gson(); + JsonElement element = gson.toJsonTree(myMap, mapType); + + assertTrue(element.isJsonObject()); + JsonObject mapJsonObject = element.getAsJsonObject(); + assertTrue(mapJsonObject.has(key)); + } +} diff --git a/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java new file mode 100644 index 00000000..dd8a7a92 --- /dev/null +++ b/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java @@ -0,0 +1,89 @@ +/* + * 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.annotations.Expose; + +import com.google.gson.internal.Excluder; +import junit.framework.TestCase; + +import java.lang.reflect.Field; + +/** + * Unit tests for GsonBuilder.REQUIRE_EXPOSE_DESERIALIZE. + * + * @author Joel Leitch + */ +public class ExposeAnnotationExclusionStrategyTest extends TestCase { + private Excluder excluder = Excluder.DEFAULT.excludeFieldsWithoutExposeAnnotation(); + + public void testNeverSkipClasses() throws Exception { + assertFalse(excluder.excludeClass(MockObject.class, true)); + assertFalse(excluder.excludeClass(MockObject.class, false)); + } + + public void testSkipNonAnnotatedFields() throws Exception { + Field f = createFieldAttributes("hiddenField"); + assertTrue(excluder.excludeField(f, true)); + assertTrue(excluder.excludeField(f, false)); + } + + public void testSkipExplicitlySkippedFields() throws Exception { + Field f = createFieldAttributes("explicitlyHiddenField"); + assertTrue(excluder.excludeField(f, true)); + assertTrue(excluder.excludeField(f, false)); + } + + public void testNeverSkipExposedAnnotatedFields() throws Exception { + Field f = createFieldAttributes("exposedField"); + assertFalse(excluder.excludeField(f, true)); + assertFalse(excluder.excludeField(f, false)); + } + + public void testNeverSkipExplicitlyExposedAnnotatedFields() throws Exception { + Field f = createFieldAttributes("explicitlyExposedField"); + assertFalse(excluder.excludeField(f, true)); + assertFalse(excluder.excludeField(f, false)); + } + + public void testDifferentSerializeAndDeserializeField() throws Exception { + Field f = createFieldAttributes("explicitlyDifferentModeField"); + assertFalse(excluder.excludeField(f, true)); + assertTrue(excluder.excludeField(f, false)); + } + + private static Field createFieldAttributes(String fieldName) throws Exception { + return MockObject.class.getField(fieldName); + } + + @SuppressWarnings("unused") + private static class MockObject { + @Expose + public final int exposedField = 0; + + @Expose(serialize=true, deserialize=true) + public final int explicitlyExposedField = 0; + + @Expose(serialize=false, deserialize=false) + public final int explicitlyHiddenField = 0; + + @Expose(serialize=true, deserialize=false) + public final int explicitlyDifferentModeField = 0; + + public final int hiddenField = 0; + } +} diff --git a/gson/src/test/java/com/google/gson/FieldAttributesTest.java b/gson/src/test/java/com/google/gson/FieldAttributesTest.java new file mode 100644 index 00000000..8a9d9533 --- /dev/null +++ b/gson/src/test/java/com/google/gson/FieldAttributesTest.java @@ -0,0 +1,82 @@ +/* + * 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.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Unit tests for the {@link FieldAttributes} class. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class FieldAttributesTest extends TestCase { + private FieldAttributes fieldAttributes; + + @Override + protected void setUp() throws Exception { + super.setUp(); + fieldAttributes = new FieldAttributes(Foo.class.getField("bar")); + } + + public void testNullField() throws Exception { + try { + new FieldAttributes(null); + fail("Field parameter can not be null"); + } catch (NullPointerException expected) { } + } + + public void testDeclaringClass() throws Exception { + assertEquals(Foo.class, fieldAttributes.getDeclaringClass()); + } + + public void testModifiers() throws Exception { + assertFalse(fieldAttributes.hasModifier(Modifier.STATIC)); + assertFalse(fieldAttributes.hasModifier(Modifier.FINAL)); + assertFalse(fieldAttributes.hasModifier(Modifier.ABSTRACT)); + assertFalse(fieldAttributes.hasModifier(Modifier.VOLATILE)); + assertFalse(fieldAttributes.hasModifier(Modifier.PROTECTED)); + + assertTrue(fieldAttributes.hasModifier(Modifier.PUBLIC)); + assertTrue(fieldAttributes.hasModifier(Modifier.TRANSIENT)); + } + + public void testIsSynthetic() throws Exception { + assertFalse(fieldAttributes.isSynthetic()); + } + + public void testName() throws Exception { + assertEquals("bar", fieldAttributes.getName()); + } + + public void testDeclaredTypeAndClass() throws Exception { + Type expectedType = new TypeToken<List<String>>() {}.getType(); + assertEquals(expectedType, fieldAttributes.getDeclaredType()); + assertEquals(List.class, fieldAttributes.getDeclaredClass()); + } + + private static class Foo { + @SuppressWarnings("unused") + public transient List<String> bar; + } +} diff --git a/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java b/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java new file mode 100644 index 00000000..42acb8a2 --- /dev/null +++ b/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java @@ -0,0 +1,57 @@ +/* + * 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.$Gson$Types; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Unit tests for the {@code GenericArrayType}s created by the {@link $Gson$Types} class. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class GenericArrayTypeTest extends TestCase { + private GenericArrayType ourType; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ourType = $Gson$Types.arrayOf($Gson$Types.newParameterizedTypeWithOwner(null, List.class, String.class)); + } + + public void testOurTypeFunctionality() throws Exception { + Type parameterizedType = new TypeToken<List<String>>() {}.getType(); + Type genericArrayType = new TypeToken<List<String>[]>() {}.getType(); + + assertEquals(parameterizedType, ourType.getGenericComponentType()); + assertEquals(genericArrayType, ourType); + assertEquals(genericArrayType.hashCode(), ourType.hashCode()); + } + + public void testNotEquals() throws Exception { + Type differentGenericArrayType = new TypeToken<List<String>[][]>() {}.getType(); + assertFalse(differentGenericArrayType.equals(ourType)); + assertFalse(ourType.equals(differentGenericArrayType)); + } +} diff --git a/gson/src/test/java/com/google/gson/GsonBuilderTest.java b/gson/src/test/java/com/google/gson/GsonBuilderTest.java new file mode 100755 index 00000000..73601c0e --- /dev/null +++ b/gson/src/test/java/com/google/gson/GsonBuilderTest.java @@ -0,0 +1,87 @@ +/*
+ * 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.Modifier;
+import java.lang.reflect.Type;
+
+import junit.framework.TestCase;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * Unit tests for {@link GsonBuilder}.
+ *
+ * @author Inderjeet Singh
+ */
+public class GsonBuilderTest extends TestCase {
+ private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() {
+ @Override public void write(JsonWriter out, Object value) {
+ throw new AssertionError();
+ }
+ @Override public Object read(JsonReader in) {
+ throw new AssertionError();
+ }
+ };
+
+ public void testCreatingMoreThanOnce() {
+ GsonBuilder builder = new GsonBuilder();
+ builder.create();
+ builder.create();
+ }
+
+ public void testExcludeFieldsWithModifiers() {
+ Gson gson = new GsonBuilder()
+ .excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
+ .create();
+ assertEquals("{\"d\":\"d\"}", gson.toJson(new HasModifiers()));
+ }
+
+ public void testRegisterTypeAdapterForCoreType() {
+ Type[] types = {
+ byte.class,
+ int.class,
+ double.class,
+ Short.class,
+ Long.class,
+ String.class,
+ };
+ for (Type type : types) {
+ new GsonBuilder().registerTypeAdapter(type, NULL_TYPE_ADAPTER);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ static class HasModifiers {
+ private String a = "a";
+ volatile String b = "b";
+ private volatile String c = "c";
+ String d = "d";
+ }
+
+ public void testTransientFieldExclusion() {
+ Gson gson = new GsonBuilder()
+ .excludeFieldsWithModifiers()
+ .create();
+ assertEquals("{\"a\":\"a\"}", gson.toJson(new HasTransients()));
+ }
+
+ static class HasTransients {
+ transient String a = "a";
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java b/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java new file mode 100644 index 00000000..922cecc4 --- /dev/null +++ b/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java @@ -0,0 +1,149 @@ +/* + * 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.math.BigInteger; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import junit.framework.TestCase; + +/** + * Contains numerous tests involving registered type converters with a Gson instance. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class GsonTypeAdapterTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new GsonBuilder() + .registerTypeAdapter(AtomicLong.class, new ExceptionTypeAdapter()) + .registerTypeAdapter(AtomicInteger.class, new AtomicIntegerTypeAdapter()) + .create(); + } + + public void testDefaultTypeAdapterThrowsParseException() throws Exception { + try { + gson.fromJson("{\"abc\":123}", BigInteger.class); + fail("Should have thrown a JsonParseException"); + } catch (JsonParseException expected) { } + } + + public void testTypeAdapterThrowsException() throws Exception { + try { + gson.toJson(new AtomicLong(0)); + fail("Type Adapter should have thrown an exception"); + } catch (IllegalStateException expected) { } + + try { + gson.fromJson("123", AtomicLong.class); + fail("Type Adapter should have thrown an exception"); + } catch (JsonParseException expected) { } + } + + public void testTypeAdapterProperlyConvertsTypes() throws Exception { + int intialValue = 1; + AtomicInteger atomicInt = new AtomicInteger(intialValue); + String json = gson.toJson(atomicInt); + assertEquals(intialValue + 1, Integer.parseInt(json)); + + atomicInt = gson.fromJson(json, AtomicInteger.class); + assertEquals(intialValue, atomicInt.get()); + } + + public void testTypeAdapterDoesNotAffectNonAdaptedTypes() throws Exception { + String expected = "blah"; + String actual = gson.toJson(expected); + assertEquals("\"" + expected + "\"", actual); + + actual = gson.fromJson(actual, String.class); + assertEquals(expected, actual); + } + + private static class ExceptionTypeAdapter + implements JsonSerializer<AtomicLong>, JsonDeserializer<AtomicLong> { + public JsonElement serialize( + AtomicLong src, Type typeOfSrc, JsonSerializationContext context) { + throw new IllegalStateException(); + } + + public AtomicLong deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + throw new IllegalStateException(); + } + } + + private static class AtomicIntegerTypeAdapter + implements JsonSerializer<AtomicInteger>, JsonDeserializer<AtomicInteger> { + public JsonElement serialize(AtomicInteger src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.incrementAndGet()); + } + + public AtomicInteger deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + int intValue = json.getAsInt(); + return new AtomicInteger(--intValue); + } + } + + static abstract class Abstract { + String a; + } + + static class Concrete extends Abstract { + String b; + } + + // https://groups.google.com/d/topic/google-gson/EBmOCa8kJPE/discussion + public void testDeserializerForAbstractClass() { + Concrete instance = new Concrete(); + instance.a = "android"; + instance.b = "beep"; + assertSerialized("{\"a\":\"android\"}", Abstract.class, true, true, instance); + assertSerialized("{\"a\":\"android\"}", Abstract.class, true, false, instance); + assertSerialized("{\"a\":\"android\"}", Abstract.class, false, true, instance); + assertSerialized("{\"a\":\"android\"}", Abstract.class, false, false, instance); + assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, true, true, instance); + assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, true, false, instance); + assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, false, true, instance); + assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, false, false, instance); + } + + private void assertSerialized(String expected, Class<?> instanceType, boolean registerAbstractDeserializer, + boolean registerAbstractHierarchyDeserializer, Object instance) { + JsonDeserializer<Abstract> deserializer = new JsonDeserializer<Abstract>() { + public Abstract deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + throw new AssertionError(); + } + }; + GsonBuilder builder = new GsonBuilder(); + if (registerAbstractDeserializer) { + builder.registerTypeAdapter(Abstract.class, deserializer); + } + if (registerAbstractHierarchyDeserializer) { + builder.registerTypeHierarchyAdapter(Abstract.class, deserializer); + } + Gson gson = builder.create(); + assertEquals(expected, gson.toJson(instance, instanceType)); + } +} diff --git a/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java new file mode 100644 index 00000000..86f7a622 --- /dev/null +++ b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java @@ -0,0 +1,58 @@ +/* + * 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.Excluder; +import java.lang.reflect.Field; +import junit.framework.TestCase; + +/** + * Unit test for GsonBuilder.EXCLUDE_INNER_CLASSES. + * + * @author Joel Leitch + */ +public class InnerClassExclusionStrategyTest extends TestCase { + public InnerClass innerClass = new InnerClass(); + public StaticNestedClass staticNestedClass = new StaticNestedClass(); + private Excluder excluder = Excluder.DEFAULT.disableInnerClassSerialization(); + + public void testExcludeInnerClassObject() throws Exception { + Class<?> clazz = innerClass.getClass(); + assertTrue(excluder.excludeClass(clazz, true)); + } + + public void testExcludeInnerClassField() throws Exception { + Field f = getClass().getField("innerClass"); + assertTrue(excluder.excludeField(f, true)); + } + + public void testIncludeStaticNestedClassObject() throws Exception { + Class<?> clazz = staticNestedClass.getClass(); + assertFalse(excluder.excludeClass(clazz, true)); + } + + public void testIncludeStaticNestedClassField() throws Exception { + Field f = getClass().getField("staticNestedClass"); + assertFalse(excluder.excludeField(f, true)); + } + + class InnerClass { + } + + static class StaticNestedClass { + } +} diff --git a/gson/src/test/java/com/google/gson/JavaSerializationTest.java b/gson/src/test/java/com/google/gson/JavaSerializationTest.java new file mode 100644 index 00000000..fbaea19d --- /dev/null +++ b/gson/src/test/java/com/google/gson/JavaSerializationTest.java @@ -0,0 +1,75 @@ +/* + * 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; + +import com.google.gson.reflect.TypeToken; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import junit.framework.TestCase; + +/** + * Check that Gson doesn't return non-serializable data types. + * + * @author Jesse Wilson + */ +public final class JavaSerializationTest extends TestCase { + private final Gson gson = new Gson(); + + public void testMapIsSerializable() throws Exception { + Type type = new TypeToken<Map<String, Integer>>() {}.getType(); + Map<String, Integer> map = gson.fromJson("{\"b\":1,\"c\":2,\"a\":3}", type); + Map<String, Integer> serialized = serializedCopy(map); + assertEquals(map, serialized); + // Also check that the iteration order is retained. + assertEquals(Arrays.asList("b", "c", "a"), new ArrayList<String>(serialized.keySet())); + } + + public void testListIsSerializable() throws Exception { + Type type = new TypeToken<List<String>>() {}.getType(); + List<String> list = gson.fromJson("[\"a\",\"b\",\"c\"]", type); + List<String> serialized = serializedCopy(list); + assertEquals(list, serialized); + } + + public void testNumberIsSerializable() throws Exception { + Type type = new TypeToken<List<Number>>() {}.getType(); + List<Number> list = gson.fromJson("[1,3.14,6.673e-11]", type); + List<Number> serialized = serializedCopy(list); + assertEquals(1.0, serialized.get(0).doubleValue()); + assertEquals(3.14, serialized.get(1).doubleValue()); + assertEquals(6.673e-11, serialized.get(2).doubleValue()); + } + + @SuppressWarnings("unchecked") // Serialization promises to return the same type. + private <T> T serializedCopy(T object) throws IOException, ClassNotFoundException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bytesOut); + out.writeObject(object); + out.close(); + ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray()); + ObjectInputStream in = new ObjectInputStream(bytesIn); + return (T) in.readObject(); + } +} diff --git a/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/src/test/java/com/google/gson/JsonArrayTest.java new file mode 100644 index 00000000..b77d6f1b --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonArrayTest.java @@ -0,0 +1,102 @@ +/* + * 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 junit.framework.TestCase; + +import com.google.gson.common.MoreAsserts; + +/** + * @author Jesse Wilson + */ +public final class JsonArrayTest extends TestCase { + + public void testEqualsOnEmptyArray() { + MoreAsserts.assertEqualsAndHashCode(new JsonArray(), new JsonArray()); + } + + public void testEqualsNonEmptyArray() { + JsonArray a = new JsonArray(); + JsonArray b = new JsonArray(); + + assertEquals(a, a); + + a.add(new JsonObject()); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + + b.add(new JsonObject()); + MoreAsserts.assertEqualsAndHashCode(a, b); + + a.add(new JsonObject()); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + + b.add(JsonNull.INSTANCE); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + } + + public void testRemove() { + JsonArray array = new JsonArray(); + try { + array.remove(0); + fail(); + } catch (IndexOutOfBoundsException expected) {} + JsonPrimitive a = new JsonPrimitive("a"); + array.add(a); + assertTrue(array.remove(a)); + assertFalse(array.contains(a)); + array.add(a); + array.add(new JsonPrimitive("b")); + assertEquals("b", array.remove(1).getAsString()); + assertEquals(1, array.size()); + assertTrue(array.contains(a)); + } + + public void testSet() { + JsonArray array = new JsonArray(); + try { + array.set(0, new JsonPrimitive(1)); + fail(); + } catch (IndexOutOfBoundsException expected) {} + JsonPrimitive a = new JsonPrimitive("a"); + array.add(a); + array.set(0, new JsonPrimitive("b")); + assertEquals("b", array.get(0).getAsString()); + array.set(0, null); + assertNull(array.get(0)); + array.set(0, new JsonPrimitive("c")); + assertEquals("c", array.get(0).getAsString()); + assertEquals(1, array.size()); + } + + public void testDeepCopy() { + JsonArray original = new JsonArray(); + JsonArray firstEntry = new JsonArray(); + original.add(firstEntry); + + JsonArray copy = original.deepCopy(); + original.add(new JsonPrimitive("y")); + + assertEquals(1, copy.size()); + firstEntry.add(new JsonPrimitive("z")); + + assertEquals(1, original.get(0).getAsJsonArray().size()); + assertEquals(0, copy.get(0).getAsJsonArray().size()); + } +} diff --git a/gson/src/test/java/com/google/gson/JsonNullTest.java b/gson/src/test/java/com/google/gson/JsonNullTest.java new file mode 100644 index 00000000..6157e387 --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonNullTest.java @@ -0,0 +1,40 @@ +/* + * 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.common.MoreAsserts; +import junit.framework.TestCase; + +/** + * @author Jesse Wilson + */ +public final class JsonNullTest extends TestCase { + + @SuppressWarnings("deprecation") + public void testEqualsAndHashcode() { + MoreAsserts.assertEqualsAndHashCode(new JsonNull(), new JsonNull()); + MoreAsserts.assertEqualsAndHashCode(new JsonNull(), JsonNull.INSTANCE); + MoreAsserts.assertEqualsAndHashCode(JsonNull.INSTANCE, JsonNull.INSTANCE); + } + + public void testDeepCopy() { + @SuppressWarnings("deprecation") + JsonNull a = new JsonNull(); + assertSame(JsonNull.INSTANCE, a.deepCopy()); + assertSame(JsonNull.INSTANCE, JsonNull.INSTANCE.deepCopy()); + } +} diff --git a/gson/src/test/java/com/google/gson/JsonObjectTest.java b/gson/src/test/java/com/google/gson/JsonObjectTest.java new file mode 100644 index 00000000..9423a24d --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonObjectTest.java @@ -0,0 +1,172 @@ +/* + * 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.common.MoreAsserts; + +import junit.framework.TestCase; + +/** + * Unit test for the {@link JsonObject} class. + * + * @author Joel Leitch + */ +public class JsonObjectTest extends TestCase { + + public void testAddingAndRemovingObjectProperties() throws Exception { + JsonObject jsonObj = new JsonObject(); + String propertyName = "property"; + assertFalse(jsonObj.has(propertyName)); + assertNull(jsonObj.get(propertyName)); + + JsonPrimitive value = new JsonPrimitive("blah"); + jsonObj.add(propertyName, value); + assertEquals(value, jsonObj.get(propertyName)); + + JsonElement removedElement = jsonObj.remove(propertyName); + assertEquals(value, removedElement); + assertFalse(jsonObj.has(propertyName)); + assertNull(jsonObj.get(propertyName)); + } + + public void testAddingNullPropertyValue() throws Exception { + String propertyName = "property"; + JsonObject jsonObj = new JsonObject(); + jsonObj.add(propertyName, null); + + assertTrue(jsonObj.has(propertyName)); + + JsonElement jsonElement = jsonObj.get(propertyName); + assertNotNull(jsonElement); + assertTrue(jsonElement.isJsonNull()); + } + + public void testAddingNullOrEmptyPropertyName() throws Exception { + JsonObject jsonObj = new JsonObject(); + try { + jsonObj.add(null, JsonNull.INSTANCE); + fail("Should not allow null property names."); + } catch (NullPointerException expected) { } + + jsonObj.add("", JsonNull.INSTANCE); + jsonObj.add(" \t", JsonNull.INSTANCE); + } + + public void testAddingBooleanProperties() throws Exception { + String propertyName = "property"; + JsonObject jsonObj = new JsonObject(); + jsonObj.addProperty(propertyName, true); + + assertTrue(jsonObj.has(propertyName)); + + JsonElement jsonElement = jsonObj.get(propertyName); + assertNotNull(jsonElement); + assertTrue(jsonElement.getAsBoolean()); + } + + public void testAddingStringProperties() throws Exception { + String propertyName = "property"; + String value = "blah"; + + JsonObject jsonObj = new JsonObject(); + jsonObj.addProperty(propertyName, value); + + assertTrue(jsonObj.has(propertyName)); + + JsonElement jsonElement = jsonObj.get(propertyName); + assertNotNull(jsonElement); + assertEquals(value, jsonElement.getAsString()); + } + + public void testAddingCharacterProperties() throws Exception { + String propertyName = "property"; + char value = 'a'; + + JsonObject jsonObj = new JsonObject(); + jsonObj.addProperty(propertyName, value); + + assertTrue(jsonObj.has(propertyName)); + + JsonElement jsonElement = jsonObj.get(propertyName); + assertNotNull(jsonElement); + assertEquals(String.valueOf(value), jsonElement.getAsString()); + assertEquals(value, jsonElement.getAsCharacter()); + } + + /** + * From bug report http://code.google.com/p/google-gson/issues/detail?id=182 + */ + public void testPropertyWithQuotes() { + JsonObject jsonObj = new JsonObject(); + jsonObj.add("a\"b", new JsonPrimitive("c\"d")); + String json = new Gson().toJson(jsonObj); + assertEquals("{\"a\\\"b\":\"c\\\"d\"}", json); + } + + /** + * From issue 227. + */ + public void testWritePropertyWithEmptyStringName() { + JsonObject jsonObj = new JsonObject(); + jsonObj.add("", new JsonPrimitive(true)); + assertEquals("{\"\":true}", new Gson().toJson(jsonObj)); + + } + + public void testReadPropertyWithEmptyStringName() { + JsonObject jsonObj = new JsonParser().parse("{\"\":true}").getAsJsonObject(); + assertEquals(true, jsonObj.get("").getAsBoolean()); + } + + public void testEqualsOnEmptyObject() { + MoreAsserts.assertEqualsAndHashCode(new JsonObject(), new JsonObject()); + } + + public void testEqualsNonEmptyObject() { + JsonObject a = new JsonObject(); + JsonObject b = new JsonObject(); + + assertEquals(a, a); + + a.add("foo", new JsonObject()); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + + b.add("foo", new JsonObject()); + MoreAsserts.assertEqualsAndHashCode(a, b); + + a.add("bar", new JsonObject()); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + + b.add("bar", JsonNull.INSTANCE); + assertFalse(a.equals(b)); + assertFalse(b.equals(a)); + } + + public void testDeepCopy() { + JsonObject original = new JsonObject(); + JsonArray firstEntry = new JsonArray(); + original.add("key", firstEntry); + + JsonObject copy = original.deepCopy(); + firstEntry.add(new JsonPrimitive("z")); + + assertEquals(1, original.get("key").getAsJsonArray().size()); + assertEquals(0, copy.get("key").getAsJsonArray().size()); + } +} diff --git a/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/src/test/java/com/google/gson/JsonParserTest.java new file mode 100644 index 00000000..7efa7fd2 --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonParserTest.java @@ -0,0 +1,126 @@ +/* + * 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.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.StringReader; + +import junit.framework.TestCase; + +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; + +/** + * Unit test for {@link JsonParser} + * + * @author Inderjeet Singh + */ +public class JsonParserTest extends TestCase { + private JsonParser parser; + + @Override + protected void setUp() throws Exception { + super.setUp(); + parser = new JsonParser(); + } + + public void testParseInvalidJson() { + try { + parser.parse("[[]"); + fail(); + } catch (JsonSyntaxException expected) { } + } + + public void testParseUnquotedStringArrayFails() { + JsonElement element = parser.parse("[a,b,c]"); + assertEquals("a", element.getAsJsonArray().get(0).getAsString()); + assertEquals("b", element.getAsJsonArray().get(1).getAsString()); + assertEquals("c", element.getAsJsonArray().get(2).getAsString()); + assertEquals(3, element.getAsJsonArray().size()); + } + + public void testParseString() { + String json = "{a:10,b:'c'}"; + JsonElement e = parser.parse(json); + assertTrue(e.isJsonObject()); + assertEquals(10, e.getAsJsonObject().get("a").getAsInt()); + assertEquals("c", e.getAsJsonObject().get("b").getAsString()); + } + + public void testParseEmptyString() { + JsonElement e = parser.parse("\" \""); + assertTrue(e.isJsonPrimitive()); + assertEquals(" ", e.getAsString()); + } + + public void testParseEmptyWhitespaceInput() { + JsonElement e = parser.parse(" "); + assertTrue(e.isJsonNull()); + } + + public void testParseUnquotedSingleWordStringFails() { + assertEquals("Test", parser.parse("Test").getAsString()); + } + + public void testParseUnquotedMultiWordStringFails() { + String unquotedSentence = "Test is a test..blah blah"; + try { + parser.parse(unquotedSentence); + fail(); + } catch (JsonSyntaxException expected) { } + } + + public void testParseMixedArray() { + String json = "[{},13,\"stringValue\"]"; + JsonElement e = parser.parse(json); + assertTrue(e.isJsonArray()); + + JsonArray array = e.getAsJsonArray(); + assertEquals("{}", array.get(0).toString()); + assertEquals(13, array.get(1).getAsInt()); + assertEquals("stringValue", array.get(2).getAsString()); + } + + public void testParseReader() { + StringReader reader = new StringReader("{a:10,b:'c'}"); + JsonElement e = parser.parse(reader); + assertTrue(e.isJsonObject()); + assertEquals(10, e.getAsJsonObject().get("a").getAsInt()); + assertEquals("c", e.getAsJsonObject().get("b").getAsString()); + } + + public void testReadWriteTwoObjects() throws Exception { + Gson gson = new Gson(); + CharArrayWriter writer = new CharArrayWriter(); + BagOfPrimitives expectedOne = new BagOfPrimitives(1, 1, true, "one"); + writer.write(gson.toJson(expectedOne).toCharArray()); + BagOfPrimitives expectedTwo = new BagOfPrimitives(2, 2, false, "two"); + writer.write(gson.toJson(expectedTwo).toCharArray()); + CharArrayReader reader = new CharArrayReader(writer.toCharArray()); + + JsonReader parser = new JsonReader(reader); + parser.setLenient(true); + JsonElement element1 = Streams.parse(parser); + JsonElement element2 = Streams.parse(parser); + BagOfPrimitives actualOne = gson.fromJson(element1, BagOfPrimitives.class); + assertEquals("one", actualOne.stringValue); + BagOfPrimitives actualTwo = gson.fromJson(element2, BagOfPrimitives.class); + assertEquals("two", actualTwo.stringValue); + } +} diff --git a/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java b/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java new file mode 100644 index 00000000..fa3611c9 --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java @@ -0,0 +1,247 @@ +/* + * 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.common.MoreAsserts; + +import junit.framework.TestCase; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Unit test for the {@link JsonPrimitive} class. + * + * @author Joel Leitch + */ +public class JsonPrimitiveTest extends TestCase { + + public void testBoolean() throws Exception { + JsonPrimitive json = new JsonPrimitive(Boolean.TRUE); + + assertTrue(json.isBoolean()); + assertTrue(json.getAsBoolean()); + + // Extra support for booleans + json = new JsonPrimitive(1); + assertFalse(json.getAsBoolean()); + + json = new JsonPrimitive("1"); + assertFalse(json.getAsBoolean()); + + json = new JsonPrimitive("true"); + assertTrue(json.getAsBoolean()); + + json = new JsonPrimitive("TrUe"); + assertTrue(json.getAsBoolean()); + + json = new JsonPrimitive("1.3"); + assertFalse(json.getAsBoolean()); + } + + public void testParsingStringAsBoolean() throws Exception { + JsonPrimitive json = new JsonPrimitive("true"); + + assertFalse(json.isBoolean()); + assertTrue(json.getAsBoolean()); + } + + public void testParsingStringAsNumber() throws Exception { + JsonPrimitive json = new JsonPrimitive("1"); + + assertFalse(json.isNumber()); + assertEquals(1D, json.getAsDouble(), 0.00001); + assertEquals(1F, json.getAsFloat(), 0.00001); + assertEquals(1, json.getAsInt()); + assertEquals(1L, json.getAsLong()); + assertEquals((short) 1, json.getAsShort()); + assertEquals((byte) 1, json.getAsByte()); + assertEquals(new BigInteger("1"), json.getAsBigInteger()); + assertEquals(new BigDecimal("1"), json.getAsBigDecimal()); + } + + public void testStringsAndChar() throws Exception { + JsonPrimitive json = new JsonPrimitive("abc"); + assertTrue(json.isString()); + assertEquals('a', json.getAsCharacter()); + assertEquals("abc", json.getAsString()); + + json = new JsonPrimitive('z'); + assertTrue(json.isString()); + assertEquals('z', json.getAsCharacter()); + assertEquals("z", json.getAsString()); + } + + public void testExponential() throws Exception { + JsonPrimitive json = new JsonPrimitive("1E+7"); + + assertEquals(new BigDecimal("1E+7"), json.getAsBigDecimal()); + assertEquals(new Double("1E+7"), json.getAsDouble(), 0.00001); + assertEquals(new Float("1E+7"), json.getAsDouble(), 0.00001); + + try { + json.getAsInt(); + fail("Integers can not handle exponents like this."); + } catch (NumberFormatException expected) { } + } + + public void testByteEqualsShort() { + JsonPrimitive p1 = new JsonPrimitive(new Byte((byte)10)); + JsonPrimitive p2 = new JsonPrimitive(new Short((short)10)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testByteEqualsInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Byte((byte)10)); + JsonPrimitive p2 = new JsonPrimitive(new Integer(10)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testByteEqualsLong() { + JsonPrimitive p1 = new JsonPrimitive(new Byte((byte)10)); + JsonPrimitive p2 = new JsonPrimitive(new Long(10L)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testByteEqualsBigInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Byte((byte)10)); + JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testShortEqualsInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Short((short)10)); + JsonPrimitive p2 = new JsonPrimitive(new Integer(10)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testShortEqualsLong() { + JsonPrimitive p1 = new JsonPrimitive(new Short((short)10)); + JsonPrimitive p2 = new JsonPrimitive(new Long(10)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testShortEqualsBigInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Short((short)10)); + JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testIntegerEqualsLong() { + JsonPrimitive p1 = new JsonPrimitive(new Integer(10)); + JsonPrimitive p2 = new JsonPrimitive(new Long(10L)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testIntegerEqualsBigInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Integer(10)); + JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testLongEqualsBigInteger() { + JsonPrimitive p1 = new JsonPrimitive(new Long(10L)); + JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testFloatEqualsDouble() { + JsonPrimitive p1 = new JsonPrimitive(new Float(10.25F)); + JsonPrimitive p2 = new JsonPrimitive(new Double(10.25D)); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testFloatEqualsBigDecimal() { + JsonPrimitive p1 = new JsonPrimitive(new Float(10.25F)); + JsonPrimitive p2 = new JsonPrimitive(new BigDecimal("10.25")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testDoubleEqualsBigDecimal() { + JsonPrimitive p1 = new JsonPrimitive(new Double(10.25D)); + JsonPrimitive p2 = new JsonPrimitive(new BigDecimal("10.25")); + assertEquals(p1, p2); + assertEquals(p1.hashCode(), p2.hashCode()); + } + + public void testValidJsonOnToString() throws Exception { + JsonPrimitive json = new JsonPrimitive("Some\nEscaped\nValue"); + assertEquals("\"Some\\nEscaped\\nValue\"", json.toString()); + + json = new JsonPrimitive(new BigDecimal("1.333")); + assertEquals("1.333", json.toString()); + } + + public void testEquals() { + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive("A"), new JsonPrimitive("A")); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(true), new JsonPrimitive(true)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(5L), new JsonPrimitive(5L)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive('a'), new JsonPrimitive('a')); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Float.NaN), new JsonPrimitive(Float.NaN)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Float.NEGATIVE_INFINITY), + new JsonPrimitive(Float.NEGATIVE_INFINITY)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Float.POSITIVE_INFINITY), + new JsonPrimitive(Float.POSITIVE_INFINITY)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Double.NaN), new JsonPrimitive(Double.NaN)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Double.NEGATIVE_INFINITY), + new JsonPrimitive(Double.NEGATIVE_INFINITY)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Double.POSITIVE_INFINITY), + new JsonPrimitive(Double.POSITIVE_INFINITY)); + assertFalse(new JsonPrimitive("a").equals(new JsonPrimitive("b"))); + assertFalse(new JsonPrimitive(true).equals(new JsonPrimitive(false))); + assertFalse(new JsonPrimitive(0).equals(new JsonPrimitive(1))); + } + + public void testEqualsAcrossTypes() { + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive("a"), new JsonPrimitive('a')); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(new BigInteger("0")), new JsonPrimitive(0)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(0), new JsonPrimitive(0L)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(new BigInteger("0")), new JsonPrimitive(0)); + MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Float.NaN), new JsonPrimitive(Double.NaN)); + } + + public void testEqualsIntegerAndBigInteger() { + JsonPrimitive a = new JsonPrimitive(5L); + JsonPrimitive b = new JsonPrimitive(new BigInteger("18446744073709551621")); // 2^64 + 5 + // Ideally, the following assertion should have failed but the price is too much to pay + // assertFalse(a + " equals " + b, a.equals(b)); + assertTrue(a + " equals " + b, a.equals(b)); + } + + public void testEqualsDoesNotEquateStringAndNonStringTypes() { + assertFalse(new JsonPrimitive("true").equals(new JsonPrimitive(true))); + assertFalse(new JsonPrimitive("0").equals(new JsonPrimitive(0))); + assertFalse(new JsonPrimitive("NaN").equals(new JsonPrimitive(Float.NaN))); + } + + public void testDeepCopy() { + JsonPrimitive a = new JsonPrimitive("a"); + assertSame(a, a.deepCopy()); // Primitives are immutable! + } +} diff --git a/gson/src/test/java/com/google/gson/JsonStreamParserTest.java b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java new file mode 100644 index 00000000..1b40b58b --- /dev/null +++ b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java @@ -0,0 +1,74 @@ +/* + * 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 junit.framework.TestCase; + +import java.util.NoSuchElementException; + +/** + * Unit tests for {@link JsonStreamParser} + * + * @author Inderjeet Singh + */ +public class JsonStreamParserTest extends TestCase { + private JsonStreamParser parser; + + @Override + protected void setUp() throws Exception { + super.setUp(); + parser = new JsonStreamParser("'one' 'two'"); + } + + public void testParseTwoStrings() { + String actualOne = parser.next().getAsString(); + assertEquals("one", actualOne); + String actualTwo = parser.next().getAsString(); + assertEquals("two", actualTwo); + } + + public void testIterator() { + assertTrue(parser.hasNext()); + assertEquals("one", parser.next().getAsString()); + assertTrue(parser.hasNext()); + assertEquals("two", parser.next().getAsString()); + assertFalse(parser.hasNext()); + } + + public void testNoSideEffectForHasNext() throws Exception { + assertTrue(parser.hasNext()); + assertTrue(parser.hasNext()); + assertTrue(parser.hasNext()); + assertEquals("one", parser.next().getAsString()); + + assertTrue(parser.hasNext()); + assertTrue(parser.hasNext()); + assertEquals("two", parser.next().getAsString()); + + assertFalse(parser.hasNext()); + assertFalse(parser.hasNext()); + } + + public void testCallingNextBeyondAvailableInput() { + parser.next(); + parser.next(); + try { + parser.next(); + fail("Parser should not go beyond available input"); + } catch (NoSuchElementException expected) { + } + } +} diff --git a/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java b/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java new file mode 100644 index 00000000..d0a06320 --- /dev/null +++ b/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java @@ -0,0 +1,64 @@ +/* + * 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 junit.framework.TestCase; + +/** + * Unit test for the {@link LongSerializationPolicy} class. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class LongSerializationPolicyTest extends TestCase { + + public void testDefaultLongSerialization() throws Exception { + JsonElement element = LongSerializationPolicy.DEFAULT.serialize(1556L); + assertTrue(element.isJsonPrimitive()); + + JsonPrimitive jsonPrimitive = element.getAsJsonPrimitive(); + assertFalse(jsonPrimitive.isString()); + assertTrue(jsonPrimitive.isNumber()); + assertEquals(1556L, element.getAsLong()); + } + + public void testDefaultLongSerializationIntegration() { + Gson gson = new GsonBuilder() + .setLongSerializationPolicy(LongSerializationPolicy.DEFAULT) + .create(); + assertEquals("[1]", gson.toJson(new long[] { 1L }, long[].class)); + assertEquals("[1]", gson.toJson(new Long[] { 1L }, Long[].class)); + } + + public void testStringLongSerialization() throws Exception { + JsonElement element = LongSerializationPolicy.STRING.serialize(1556L); + assertTrue(element.isJsonPrimitive()); + + JsonPrimitive jsonPrimitive = element.getAsJsonPrimitive(); + assertFalse(jsonPrimitive.isNumber()); + assertTrue(jsonPrimitive.isString()); + assertEquals("1556", element.getAsString()); + } + + public void testStringLongSerializationIntegration() { + Gson gson = new GsonBuilder() + .setLongSerializationPolicy(LongSerializationPolicy.STRING) + .create(); + assertEquals("[\"1\"]", gson.toJson(new long[] { 1L }, long[].class)); + assertEquals("[\"1\"]", gson.toJson(new Long[] { 1L }, Long[].class)); + } +} diff --git a/gson/src/test/java/com/google/gson/MixedStreamTest.java b/gson/src/test/java/com/google/gson/MixedStreamTest.java new file mode 100644 index 00000000..00eb4bc8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/MixedStreamTest.java @@ -0,0 +1,239 @@ +/* + * 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; + +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; + +public final class MixedStreamTest extends TestCase { + + private static final Car BLUE_MUSTANG = new Car("mustang", 0x0000FF); + private static final Car BLACK_BMW = new Car("bmw", 0x000000); + private static final Car RED_MIATA = new Car("miata", 0xFF0000); + private static final String CARS_JSON = "[\n" + + " {\n" + + " \"name\": \"mustang\",\n" + + " \"color\": 255\n" + + " },\n" + + " {\n" + + " \"name\": \"bmw\",\n" + + " \"color\": 0\n" + + " },\n" + + " {\n" + + " \"name\": \"miata\",\n" + + " \"color\": 16711680\n" + + " }\n" + + "]"; + + public void testWriteMixedStreamed() throws IOException { + Gson gson = new Gson(); + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + + jsonWriter.beginArray(); + jsonWriter.setIndent(" "); + gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter); + gson.toJson(BLACK_BMW, Car.class, jsonWriter); + gson.toJson(RED_MIATA, Car.class, jsonWriter); + jsonWriter.endArray(); + + assertEquals(CARS_JSON, stringWriter.toString()); + } + + public void testReadMixedStreamed() throws IOException { + Gson gson = new Gson(); + StringReader stringReader = new StringReader(CARS_JSON); + JsonReader jsonReader = new JsonReader(stringReader); + + jsonReader.beginArray(); + assertEquals(BLUE_MUSTANG, gson.fromJson(jsonReader, Car.class)); + assertEquals(BLACK_BMW, gson.fromJson(jsonReader, Car.class)); + assertEquals(RED_MIATA, gson.fromJson(jsonReader, Car.class)); + jsonReader.endArray(); + } + + public void testReaderDoesNotMutateState() throws IOException { + Gson gson = new Gson(); + JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON)); + jsonReader.beginArray(); + + jsonReader.setLenient(false); + gson.fromJson(jsonReader, Car.class); + assertFalse(jsonReader.isLenient()); + + jsonReader.setLenient(true); + gson.fromJson(jsonReader, Car.class); + assertTrue(jsonReader.isLenient()); + } + + public void testWriteDoesNotMutateState() throws IOException { + Gson gson = new Gson(); + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.beginArray(); + + jsonWriter.setHtmlSafe(true); + jsonWriter.setLenient(true); + gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter); + assertTrue(jsonWriter.isHtmlSafe()); + assertTrue(jsonWriter.isLenient()); + + jsonWriter.setHtmlSafe(false); + jsonWriter.setLenient(false); + gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter); + assertFalse(jsonWriter.isHtmlSafe()); + assertFalse(jsonWriter.isLenient()); + } + + public void testReadInvalidState() throws IOException { + Gson gson = new Gson(); + JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON)); + jsonReader.beginArray(); + jsonReader.beginObject(); + try { + gson.fromJson(jsonReader, String.class); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testReadClosed() throws IOException { + Gson gson = new Gson(); + JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON)); + jsonReader.close(); + try { + gson.fromJson(jsonReader, new TypeToken<List<Car>>() {}.getType()); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testWriteInvalidState() throws IOException { + Gson gson = new Gson(); + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.beginObject(); + try { + gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testWriteClosed() throws IOException { + Gson gson = new Gson(); + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.beginArray(); + jsonWriter.endArray(); + jsonWriter.close(); + try { + gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testWriteNulls() { + Gson gson = new Gson(); + try { + gson.toJson(new JsonPrimitive("hello"), (JsonWriter) null); + fail(); + } catch (NullPointerException expected) { + } + + StringWriter stringWriter = new StringWriter(); + gson.toJson(null, new JsonWriter(stringWriter)); + assertEquals("null", stringWriter.toString()); + } + + public void testReadNulls() { + Gson gson = new Gson(); + try { + gson.fromJson((JsonReader) null, Integer.class); + fail(); + } catch (NullPointerException expected) { + } + try { + gson.fromJson(new JsonReader(new StringReader("true")), null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testWriteHtmlSafe() { + List<String> contents = Arrays.asList("<", ">", "&", "=", "'"); + Type type = new TypeToken<List<String>>() {}.getType(); + + StringWriter writer = new StringWriter(); + new Gson().toJson(contents, type, new JsonWriter(writer)); + assertEquals("[\"\\u003c\",\"\\u003e\",\"\\u0026\",\"\\u003d\",\"\\u0027\"]", + writer.toString()); + + writer = new StringWriter(); + new GsonBuilder().disableHtmlEscaping().create() + .toJson(contents, type, new JsonWriter(writer)); + assertEquals("[\"<\",\">\",\"&\",\"=\",\"'\"]", + writer.toString()); + } + + public void testWriteLenient() { + List<Double> doubles = Arrays.asList(Double.NaN, Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY, -0.0d, 0.5d, 0.0d); + Type type = new TypeToken<List<Double>>() {}.getType(); + + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + new GsonBuilder().serializeSpecialFloatingPointValues().create() + .toJson(doubles, type, jsonWriter); + assertEquals("[NaN,-Infinity,Infinity,-0.0,0.5,0.0]", writer.toString()); + + try { + new Gson().toJson(doubles, type, new JsonWriter(new StringWriter())); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + static final class Car { + String name; + int color; + + Car(String name, int color) { + this.name = name; + this.color = color; + } + + // used by Gson + Car() {} + + @Override public int hashCode() { + return name.hashCode() ^ color; + } + + @Override public boolean equals(Object o) { + return o instanceof Car + && ((Car) o).name.equals(name) + && ((Car) o).color == color; + } + } +} diff --git a/gson/src/test/java/com/google/gson/MockExclusionStrategy.java b/gson/src/test/java/com/google/gson/MockExclusionStrategy.java new file mode 100644 index 00000000..2e5db94d --- /dev/null +++ b/gson/src/test/java/com/google/gson/MockExclusionStrategy.java @@ -0,0 +1,41 @@ +/* + * 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 is a configurable {@link ExclusionStrategy} that can be used for + * unit testing. + * + * @author Joel Leitch + */ +final class MockExclusionStrategy implements ExclusionStrategy { + private final boolean skipClass; + private final boolean skipField; + + public MockExclusionStrategy(boolean skipClass, boolean skipField) { + this.skipClass = skipClass; + this.skipField = skipField; + } + + public boolean shouldSkipField(FieldAttributes f) { + return skipField; + } + + public boolean shouldSkipClass(Class<?> clazz) { + return skipClass; + } +} diff --git a/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java b/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java new file mode 100644 index 00000000..2891bffc --- /dev/null +++ b/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java @@ -0,0 +1,63 @@ +/* + * 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 java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import junit.framework.TestCase; + +public final class ObjectTypeAdapterTest extends TestCase { + private final Gson gson = new GsonBuilder().create(); + private final TypeAdapter<Object> adapter = gson.getAdapter(Object.class); + + public void testDeserialize() throws Exception { + Map<?, ?> map = (Map<?, ?>) adapter.fromJson("{\"a\":5,\"b\":[1,2,null],\"c\":{\"x\":\"y\"}}"); + assertEquals(5.0, map.get("a")); + assertEquals(Arrays.asList(1.0, 2.0, null), map.get("b")); + assertEquals(Collections.singletonMap("x", "y"), map.get("c")); + assertEquals(3, map.size()); + } + + public void testSerialize() throws Exception { + Object object = new RuntimeType(); + assertEquals("{'a':5,'b':[1,2,null]}", adapter.toJson(object).replace("\"", "'")); + } + + public void testSerializeNullValue() throws Exception { + Map<String, Object> map = new LinkedHashMap<String, Object>(); + map.put("a", null); + assertEquals("{'a':null}", adapter.toJson(map).replace('"', '\'')); + } + + public void testDeserializeNullValue() throws Exception { + Map<String, Object> map = new LinkedHashMap<String, Object>(); + map.put("a", null); + assertEquals(map, adapter.fromJson("{\"a\":null}")); + } + + public void testSerializeObject() throws Exception { + assertEquals("{}", adapter.toJson(new Object())); + } + + @SuppressWarnings("unused") + private class RuntimeType { + Object a = 5; + Object b = Arrays.asList(1, 2, null); + } +} diff --git a/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java new file mode 100644 index 00000000..79ae1698 --- /dev/null +++ b/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java @@ -0,0 +1,77 @@ +/* + * 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; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Locale; +import junit.framework.TestCase; + +/** + * @author Jesse Wilson + */ +public class OverrideCoreTypeAdaptersTest extends TestCase { + private static final TypeAdapter<Boolean> booleanAsIntAdapter = new TypeAdapter<Boolean>() { + @Override public void write(JsonWriter out, Boolean value) throws IOException { + out.value(value ? 1 : 0); + } + @Override public Boolean read(JsonReader in) throws IOException { + int value = in.nextInt(); + return value != 0; + } + }; + + private static final TypeAdapter<String> swapCaseStringAdapter = new TypeAdapter<String>() { + @Override public void write(JsonWriter out, String value) throws IOException { + out.value(value.toUpperCase(Locale.US)); + } + @Override public String read(JsonReader in) throws IOException { + return in.nextString().toLowerCase(Locale.US); + } + }; + + public void testOverrideWrapperBooleanAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Boolean.class, booleanAsIntAdapter) + .create(); + assertEquals("true", gson.toJson(true, boolean.class)); + assertEquals("1", gson.toJson(true, Boolean.class)); + assertEquals(Boolean.TRUE, gson.fromJson("true", boolean.class)); + assertEquals(Boolean.TRUE, gson.fromJson("1", Boolean.class)); + assertEquals(Boolean.FALSE, gson.fromJson("0", Boolean.class)); + } + + public void testOverridePrimitiveBooleanAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(boolean.class, booleanAsIntAdapter) + .create(); + assertEquals("1", gson.toJson(true, boolean.class)); + assertEquals("true", gson.toJson(true, Boolean.class)); + assertEquals(Boolean.TRUE, gson.fromJson("1", boolean.class)); + assertEquals(Boolean.TRUE, gson.fromJson("true", Boolean.class)); + assertEquals("0", gson.toJson(false, boolean.class)); + } + + public void testOverrideStringAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(String.class, swapCaseStringAdapter) + .create(); + assertEquals("\"HELLO\"", gson.toJson("Hello", String.class)); + assertEquals("hello", gson.fromJson("\"Hello\"", String.class)); + } +} diff --git a/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java b/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java new file mode 100644 index 00000000..6bae432f --- /dev/null +++ b/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java @@ -0,0 +1,177 @@ +/* + * 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.$Gson$Types; + +import com.google.gson.internal.Primitives; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + + +/** + * This class contains some test fixtures for Parameterized types. These classes should ideally + * belong either in the common or functional package, but they are placed here because they need + * access to package protected elements of com.google.gson. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ParameterizedTypeFixtures { + + public static class MyParameterizedType<T> { + public final T value; + public MyParameterizedType(T value) { + this.value = value; + } + public T getValue() { + return value; + } + + public String getExpectedJson() { + String valueAsJson = getExpectedJson(value); + return String.format("{\"value\":%s}", valueAsJson); + } + + private String getExpectedJson(Object obj) { + Class<?> clazz = obj.getClass(); + if (Primitives.isWrapperType(Primitives.wrap(clazz))) { + return obj.toString(); + } else if (obj.getClass().equals(String.class)) { + return "\"" + obj.toString() + "\""; + } else { + // Try invoking a getExpectedJson() method if it exists + try { + Method method = clazz.getMethod("getExpectedJson"); + Object results = method.invoke(obj); + return (String) results; + } catch (SecurityException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MyParameterizedType<T> other = (MyParameterizedType<T>) obj; + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } + } + + public static class MyParameterizedTypeInstanceCreator<T> + implements InstanceCreator<MyParameterizedType<T>>{ + private final T instanceOfT; + /** + * Caution the specified instance is reused by the instance creator for each call. + * This means that the fields of the same objects will be overwritten by Gson. + * This is usually fine in tests since there we deserialize just once, but quite + * dangerous in practice. + * + * @param instanceOfT + */ + public MyParameterizedTypeInstanceCreator(T instanceOfT) { + this.instanceOfT = instanceOfT; + } + public MyParameterizedType<T> createInstance(Type type) { + return new MyParameterizedType<T>(instanceOfT); + } + } + + public static class MyParameterizedTypeAdapter<T> + implements JsonSerializer<MyParameterizedType<T>>, JsonDeserializer<MyParameterizedType<T>> { + @SuppressWarnings("unchecked") + public static<T> String getExpectedJson(MyParameterizedType<T> obj) { + Class<T> clazz = (Class<T>) obj.value.getClass(); + boolean addQuotes = !clazz.isArray() && !Primitives.unwrap(clazz).isPrimitive(); + StringBuilder sb = new StringBuilder("{\""); + sb.append(obj.value.getClass().getSimpleName()).append("\":"); + if (addQuotes) { + sb.append("\""); + } + sb.append(obj.value.toString()); + if (addQuotes) { + sb.append("\""); + } + sb.append("}"); + return sb.toString(); + } + + public JsonElement serialize(MyParameterizedType<T> src, Type classOfSrc, + JsonSerializationContext context) { + JsonObject json = new JsonObject(); + T value = src.getValue(); + json.add(value.getClass().getSimpleName(), context.serialize(value)); + return json; + } + + @SuppressWarnings("unchecked") + public MyParameterizedType<T> deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + Type genericClass = ((ParameterizedType) typeOfT).getActualTypeArguments()[0]; + Class<?> rawType = $Gson$Types.getRawType(genericClass); + String className = rawType.getSimpleName(); + JsonElement jsonElement = json.getAsJsonObject().get(className); + + T value; + if (genericClass == Integer.class) { + value = (T) Integer.valueOf(jsonElement.getAsInt()); + } else if (genericClass == String.class) { + value = (T) jsonElement.getAsString(); + } else { + value = (T) jsonElement; + } + + if (Primitives.isPrimitive(genericClass)) { + PrimitiveTypeAdapter typeAdapter = new PrimitiveTypeAdapter(); + value = (T) typeAdapter.adaptType(value, rawType); + } + return new MyParameterizedType<T>(value); + } + } +} diff --git a/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java b/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java new file mode 100644 index 00000000..8b56579e --- /dev/null +++ b/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java @@ -0,0 +1,57 @@ +/* + * 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.$Gson$Types; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Unit tests for {@code ParamterizedType}s created by the {@link $Gson$Types} class. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ParameterizedTypeTest extends TestCase { + private ParameterizedType ourType; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ourType = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, String.class); + } + + public void testOurTypeFunctionality() throws Exception { + Type parameterizedType = new TypeToken<List<String>>() {}.getType(); + assertNull(ourType.getOwnerType()); + assertEquals(String.class, ourType.getActualTypeArguments()[0]); + assertEquals(List.class, ourType.getRawType()); + assertEquals(parameterizedType, ourType); + assertEquals(parameterizedType.hashCode(), ourType.hashCode()); + } + + public void testNotEquals() throws Exception { + Type differentParameterizedType = new TypeToken<List<Integer>>() {}.getType(); + assertFalse(differentParameterizedType.equals(ourType)); + assertFalse(ourType.equals(differentParameterizedType)); + } +} diff --git a/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java b/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java new file mode 100644 index 00000000..fb38687b --- /dev/null +++ b/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java @@ -0,0 +1,73 @@ +/* + * 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.Primitives; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Handles type conversion from some object to some primitive (or primitive + * wrapper instance). + * + * @author Joel Leitch + */ +final class PrimitiveTypeAdapter { + + @SuppressWarnings("unchecked") + public <T> T adaptType(Object from, Class<T> to) { + Class<?> aClass = Primitives.wrap(to); + if (Primitives.isWrapperType(aClass)) { + if (aClass == Character.class) { + String value = from.toString(); + if (value.length() == 1) { + return (T) (Character) from.toString().charAt(0); + } + throw new JsonParseException("The value: " + value + " contains more than a character."); + } + + try { + Constructor<?> constructor = aClass.getConstructor(String.class); + return (T) constructor.newInstance(from.toString()); + } catch (NoSuchMethodException e) { + throw new JsonParseException(e); + } catch (IllegalAccessException e) { + throw new JsonParseException(e); + } catch (InvocationTargetException e) { + throw new JsonParseException(e); + } catch (InstantiationException e) { + throw new JsonParseException(e); + } + } else if (Enum.class.isAssignableFrom(to)) { + // Case where the type being adapted to is an Enum + // We will try to convert from.toString() to the enum + try { + Method valuesMethod = to.getMethod("valueOf", String.class); + return (T) valuesMethod.invoke(null, from.toString()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + throw new JsonParseException("Can not adapt type " + from.getClass() + " to " + to); + } + } +} diff --git a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java new file mode 100644 index 00000000..d878850e --- /dev/null +++ b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java @@ -0,0 +1,55 @@ +/* + * 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.annotations.Since; +import com.google.gson.internal.Excluder; +import junit.framework.TestCase; + +/** + * Unit tests for the {@link Excluder} class. + * + * @author Joel Leitch + */ +public class VersionExclusionStrategyTest extends TestCase { + private static final double VERSION = 5.0D; + + public void testClassAndFieldAreAtSameVersion() throws Exception { + Excluder excluder = Excluder.DEFAULT.withVersion(VERSION); + assertFalse(excluder.excludeClass(MockObject.class, true)); + assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true)); + } + + public void testClassAndFieldAreBehindInVersion() throws Exception { + Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1); + assertFalse(excluder.excludeClass(MockObject.class, true)); + assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true)); + } + + public void testClassAndFieldAreAheadInVersion() throws Exception { + Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1); + assertTrue(excluder.excludeClass(MockObject.class, true)); + assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true)); + } + + @Since(VERSION) + private static class MockObject { + + @Since(VERSION) + public final int someField = 0; + } +} diff --git a/gson/src/test/java/com/google/gson/common/MoreAsserts.java b/gson/src/test/java/com/google/gson/common/MoreAsserts.java new file mode 100644 index 00000000..5e05832a --- /dev/null +++ b/gson/src/test/java/com/google/gson/common/MoreAsserts.java @@ -0,0 +1,73 @@ +/* + * 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.common; + +import junit.framework.Assert; + +import java.util.Collection; + +/** + * Handy asserts that we wish were present in {@link Assert} + * so that we didn't have to write them. + * + * @author Inderjeet Singh + */ +public class MoreAsserts { + + public static void assertEquals(int[] expected, int[] target) { + if (expected == null) { + Assert.assertNull(target); + } + Assert.assertEquals(expected.length, target.length); + for (int i = 0; i < expected.length; ++i) { + Assert.assertEquals(expected[i], target[i]); + } + } + + public static void assertEquals(Integer[] expected, Integer[] target) { + if (expected == null) { + Assert.assertNull(target); + } + Assert.assertEquals(expected.length, target.length); + for (int i = 0; i < expected.length; ++i) { + Assert.assertEquals(expected[i], target[i]); + } + } + + /** + * Asserts that the specified {@code value} is not present in {@code collection} + * @param collection the collection to look into + * @param value the value that needs to be checked for presence + */ + public static <T> void assertContains(Collection<T> collection, T value) { + for (T entry : collection) { + if (entry.equals(value)) { + return; + } + } + Assert.fail(value + " not present in " + collection); + } + + public static void assertEqualsAndHashCode(Object a, Object b) { + Assert.assertTrue(a.equals(b)); + Assert.assertTrue(b.equals(a)); + Assert.assertEquals(a.hashCode(), b.hashCode()); + Assert.assertFalse(a.equals(null)); + Assert.assertFalse(a.equals(new Object())); + } + +} diff --git a/gson/src/test/java/com/google/gson/common/TestTypes.java b/gson/src/test/java/com/google/gson/common/TestTypes.java new file mode 100644 index 00000000..4754d97b --- /dev/null +++ b/gson/src/test/java/com/google/gson/common/TestTypes.java @@ -0,0 +1,418 @@ +/* + * 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.common; + +import java.lang.reflect.Type; +import java.util.Collection; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; + +/** + * Types used for testing JSON serialization and deserialization + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class TestTypes { + + public static class Base { + public static final String BASE_NAME = Base.class.getSimpleName(); + public static final String BASE_FIELD_KEY = "baseName"; + public static final String SERIALIZER_KEY = "serializerName"; + public String baseName = BASE_NAME; + public String serializerName; + } + + public static class Sub extends Base { + public static final String SUB_NAME = Sub.class.getSimpleName(); + public static final String SUB_FIELD_KEY = "subName"; + public final String subName = SUB_NAME; + } + + public static class ClassWithBaseField { + public static final String FIELD_KEY = "base"; + public final Base base; + public ClassWithBaseField(Base base) { + this.base = base; + } + } + + public static class ClassWithBaseArrayField { + public static final String FIELD_KEY = "base"; + public final Base[] base; + public ClassWithBaseArrayField(Base[] base) { + this.base = base; + } + } + + public static class ClassWithBaseCollectionField { + public static final String FIELD_KEY = "base"; + public final Collection<Base> base; + public ClassWithBaseCollectionField(Collection<Base> base) { + this.base = base; + } + } + + public static class BaseSerializer implements JsonSerializer<Base> { + public static final String NAME = BaseSerializer.class.getSimpleName(); + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty(Base.SERIALIZER_KEY, NAME); + return obj; + } + } + public static class SubSerializer implements JsonSerializer<Sub> { + public static final String NAME = SubSerializer.class.getSimpleName(); + public JsonElement serialize(Sub src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty(Base.SERIALIZER_KEY, NAME); + return obj; + } + } + + public static class StringWrapper { + public final String someConstantStringInstanceField; + + public StringWrapper(String value) { + someConstantStringInstanceField = value; + } + } + + public static class BagOfPrimitives { + public static final long DEFAULT_VALUE = 0; + public long longValue; + public int intValue; + public boolean booleanValue; + public String stringValue; + + public BagOfPrimitives() { + this(DEFAULT_VALUE, 0, false, ""); + } + + public BagOfPrimitives(long longValue, int intValue, boolean booleanValue, String stringValue) { + this.longValue = longValue; + this.intValue = intValue; + this.booleanValue = booleanValue; + this.stringValue = stringValue; + } + + public int getIntValue() { + return intValue; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"longValue\":").append(longValue).append(","); + sb.append("\"intValue\":").append(intValue).append(","); + sb.append("\"booleanValue\":").append(booleanValue).append(","); + sb.append("\"stringValue\":\"").append(stringValue).append("\""); + sb.append("}"); + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (booleanValue ? 1231 : 1237); + result = prime * result + intValue; + result = prime * result + (int) (longValue ^ (longValue >>> 32)); + result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BagOfPrimitives other = (BagOfPrimitives) obj; + if (booleanValue != other.booleanValue) + return false; + if (intValue != other.intValue) + return false; + if (longValue != other.longValue) + return false; + if (stringValue == null) { + if (other.stringValue != null) + return false; + } else if (!stringValue.equals(other.stringValue)) + return false; + return true; + } + + @Override + public String toString() { + return String.format("(longValue=%d,intValue=%d,booleanValue=%b,stringValue=%s)", + longValue, intValue, booleanValue, stringValue); + } + } + + public static class BagOfPrimitiveWrappers { + private final Long longValue; + private final Integer intValue; + private final Boolean booleanValue; + + public BagOfPrimitiveWrappers(Long longValue, Integer intValue, Boolean booleanValue) { + this.longValue = longValue; + this.intValue = intValue; + this.booleanValue = booleanValue; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"longValue\":").append(longValue).append(","); + sb.append("\"intValue\":").append(intValue).append(","); + sb.append("\"booleanValue\":").append(booleanValue); + sb.append("}"); + return sb.toString(); + } + } + + public static class PrimitiveArray { + private final long[] longArray; + + public PrimitiveArray() { + this(new long[0]); + } + + public PrimitiveArray(long[] longArray) { + this.longArray = longArray; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{\"longArray\":["); + + boolean first = true; + for (long l : longArray) { + if (!first) { + sb.append(","); + } else { + first = false; + } + sb.append(l); + } + + sb.append("]}"); + return sb.toString(); + } + } + + public static class ClassWithNoFields { + // Nothing here.. . + @Override + public boolean equals(Object other) { + return other.getClass() == ClassWithNoFields.class; + } + } + + public static class Nested { + private final BagOfPrimitives primitive1; + private final BagOfPrimitives primitive2; + + public Nested() { + this(null, null); + } + + public Nested(BagOfPrimitives primitive1, BagOfPrimitives primitive2) { + this.primitive1 = primitive1; + this.primitive2 = primitive2; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + appendFields(sb); + sb.append("}"); + return sb.toString(); + } + + public void appendFields(StringBuilder sb) { + if (primitive1 != null) { + sb.append("\"primitive1\":").append(primitive1.getExpectedJson()); + } + if (primitive1 != null && primitive2 != null) { + sb.append(","); + } + if (primitive2 != null) { + sb.append("\"primitive2\":").append(primitive2.getExpectedJson()); + } + } + } + + public static class ClassWithTransientFields<T> { + public transient T transientT; + public final transient long transientLongValue; + private final long[] longValue; + + public ClassWithTransientFields() { + this(0L); + } + + public ClassWithTransientFields(long value) { + longValue = new long[] { value }; + transientLongValue = value + 1; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"longValue\":[").append(longValue[0]).append("]"); + sb.append("}"); + return sb.toString(); + } + } + + public static class ClassWithCustomTypeConverter { + private final BagOfPrimitives bag; + private final int value; + + public ClassWithCustomTypeConverter() { + this(new BagOfPrimitives(), 10); + } + + public ClassWithCustomTypeConverter(int value) { + this(new BagOfPrimitives(value, value, false, ""), value); + } + + public ClassWithCustomTypeConverter(BagOfPrimitives bag, int value) { + this.bag = bag; + this.value = value; + } + + public BagOfPrimitives getBag() { + return bag; + } + + public String getExpectedJson() { + return "{\"url\":\"" + bag.getExpectedJson() + "\",\"value\":" + value + "}"; + } + + public int getValue() { + return value; + } + } + + public static class ArrayOfObjects { + private final BagOfPrimitives[] elements; + public ArrayOfObjects() { + elements = new BagOfPrimitives[3]; + for (int i = 0; i < elements.length; ++i) { + elements[i] = new BagOfPrimitives(i, i+2, false, "i"+i); + } + } + public String getExpectedJson() { + StringBuilder sb = new StringBuilder("{\"elements\":["); + boolean first = true; + for (BagOfPrimitives element : elements) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(element.getExpectedJson()); + } + sb.append("]}"); + return sb.toString(); + } + } + + public static class ClassOverridingEquals { + public ClassOverridingEquals ref; + + public String getExpectedJson() { + if (ref == null) { + return "{}"; + } + return "{\"ref\":" + ref.getExpectedJson() + "}"; + } + @Override + public boolean equals(Object obj) { + return true; + } + + @Override + public int hashCode() { + return 1; + } + } + + public static class ClassWithArray { + public final Object[] array; + public ClassWithArray() { + array = null; + } + + public ClassWithArray(Object[] array) { + this.array = array; + } + } + + public static class ClassWithObjects { + public final BagOfPrimitives bag; + public ClassWithObjects() { + this(new BagOfPrimitives()); + } + public ClassWithObjects(BagOfPrimitives bag) { + this.bag = bag; + } + } + + public static class ClassWithSerializedNameFields { + @SerializedName("fooBar") public final int f; + @SerializedName("Another Foo") public final int g; + + public ClassWithSerializedNameFields() { + this(1, 4); + } + public ClassWithSerializedNameFields(int f, int g) { + this.f = f; + this.g = g; + } + + public String getExpectedJson() { + return '{' + "\"fooBar\":" + f + ",\"Another Foo\":" + g + '}'; + } + } + + public static class CrazyLongTypeAdapter + implements JsonSerializer<Long>, JsonDeserializer<Long> { + public static final long DIFFERENCE = 5L; + public JsonElement serialize(Long src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src + DIFFERENCE); + } + + public Long deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return json.getAsLong() - DIFFERENCE; + } +} +}
\ No newline at end of file diff --git a/gson/src/test/java/com/google/gson/functional/ArrayTest.java b/gson/src/test/java/com/google/gson/functional/ArrayTest.java new file mode 100644 index 00000000..11388e90 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ArrayTest.java @@ -0,0 +1,268 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.common.MoreAsserts; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.ClassWithObjects; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +/** + * Functional tests for Json serialization and deserialization of arrays. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ArrayTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testTopLevelArrayOfIntsSerialization() { + int[] target = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + assertEquals("[1,2,3,4,5,6,7,8,9]", gson.toJson(target)); + } + + public void testTopLevelArrayOfIntsDeserialization() { + int[] expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + int[] actual = gson.fromJson("[1,2,3,4,5,6,7,8,9]", int[].class); + MoreAsserts.assertEquals(expected, actual); + } + + public void testInvalidArrayDeserialization() { + String json = "[1, 2 3, 4, 5]"; + try { + gson.fromJson(json, int[].class); + fail("Gson should not deserialize array elements with missing ,"); + } catch (JsonParseException expected) { + } + } + + public void testEmptyArraySerialization() { + int[] target = {}; + assertEquals("[]", gson.toJson(target)); + } + + public void testEmptyArrayDeserialization() { + int[] actualObject = gson.fromJson("[]", int[].class); + assertTrue(actualObject.length == 0); + + Integer[] actualObject2 = gson.fromJson("[]", Integer[].class); + assertTrue(actualObject2.length == 0); + + actualObject = gson.fromJson("[ ]", int[].class); + assertTrue(actualObject.length == 0); + } + + public void testNullsInArraySerialization() { + String[] array = {"foo", null, "bar"}; + String expected = "[\"foo\",null,\"bar\"]"; + String json = gson.toJson(array); + assertEquals(expected, json); + } + + public void testNullsInArrayDeserialization() { + String json = "[\"foo\",null,\"bar\"]"; + String[] expected = {"foo", null, "bar"}; + String[] target = gson.fromJson(json, expected.getClass()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], target[i]); + } + } + + public void testSingleNullInArraySerialization() { + BagOfPrimitives[] array = new BagOfPrimitives[1]; + array[0] = null; + String json = gson.toJson(array); + assertEquals("[null]", json); + } + + public void testSingleNullInArrayDeserialization() { + BagOfPrimitives[] array = gson.fromJson("[null]", BagOfPrimitives[].class); + assertNull(array[0]); + } + + public void testNullsInArrayWithSerializeNullPropertySetSerialization() { + gson = new GsonBuilder().serializeNulls().create(); + String[] array = {"foo", null, "bar"}; + String expected = "[\"foo\",null,\"bar\"]"; + String json = gson.toJson(array); + assertEquals(expected, json); + } + + public void testArrayOfStringsSerialization() { + String[] target = {"Hello", "World"}; + assertEquals("[\"Hello\",\"World\"]", gson.toJson(target)); + } + + public void testArrayOfStringsDeserialization() { + String json = "[\"Hello\",\"World\"]"; + String[] target = gson.fromJson(json, String[].class); + assertEquals("Hello", target[0]); + assertEquals("World", target[1]); + } + + public void testSingleStringArraySerialization() throws Exception { + String[] s = { "hello" }; + String output = gson.toJson(s); + assertEquals("[\"hello\"]", output); + } + + public void testSingleStringArrayDeserialization() throws Exception { + String json = "[\"hello\"]"; + String[] arrayType = gson.fromJson(json, String[].class); + assertEquals(1, arrayType.length); + assertEquals("hello", arrayType[0]); + } + + @SuppressWarnings("unchecked") + public void testArrayOfCollectionSerialization() throws Exception { + StringBuilder sb = new StringBuilder("["); + int arraySize = 3; + + Type typeToSerialize = new TypeToken<Collection<Integer>[]>() {}.getType(); + Collection<Integer>[] arrayOfCollection = new ArrayList[arraySize]; + for (int i = 0; i < arraySize; ++i) { + int startValue = (3 * i) + 1; + sb.append('[').append(startValue).append(',').append(startValue + 1).append(']'); + ArrayList<Integer> tmpList = new ArrayList<Integer>(); + tmpList.add(startValue); + tmpList.add(startValue + 1); + arrayOfCollection[i] = tmpList; + + if (i < arraySize - 1) { + sb.append(','); + } + } + sb.append(']'); + + String json = gson.toJson(arrayOfCollection, typeToSerialize); + assertEquals(sb.toString(), json); + } + + public void testArrayOfCollectionDeserialization() throws Exception { + String json = "[[1,2],[3,4]]"; + Type type = new TypeToken<Collection<Integer>[]>() {}.getType(); + Collection<Integer>[] target = gson.fromJson(json, type); + + assertEquals(2, target.length); + MoreAsserts.assertEquals(new Integer[] { 1, 2 }, target[0].toArray(new Integer[0])); + MoreAsserts.assertEquals(new Integer[] { 3, 4 }, target[1].toArray(new Integer[0])); + } + + public void testArrayOfPrimitivesAsObjectsSerialization() throws Exception { + Object[] objs = new Object[] {1, "abc", 0.3f, 5L}; + String json = gson.toJson(objs); + assertTrue(json.contains("abc")); + assertTrue(json.contains("0.3")); + assertTrue(json.contains("5")); + } + + public void testArrayOfPrimitivesAsObjectsDeserialization() throws Exception { + String json = "[1,'abc',0.3,1.1,5]"; + Object[] objs = gson.fromJson(json, Object[].class); + assertEquals(1, ((Number)objs[0]).intValue()); + assertEquals("abc", objs[1]); + assertEquals(0.3, ((Number)objs[2]).doubleValue()); + assertEquals(new BigDecimal("1.1"), new BigDecimal(objs[3].toString())); + assertEquals(5, ((Number)objs[4]).shortValue()); + } + + public void testObjectArrayWithNonPrimitivesSerialization() throws Exception { + ClassWithObjects classWithObjects = new ClassWithObjects(); + BagOfPrimitives bagOfPrimitives = new BagOfPrimitives(); + String classWithObjectsJson = gson.toJson(classWithObjects); + String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives); + + Object[] objects = new Object[] { classWithObjects, bagOfPrimitives }; + String json = gson.toJson(objects); + + assertTrue(json.contains(classWithObjectsJson)); + assertTrue(json.contains(bagOfPrimitivesJson)); + } + + public void testArrayOfNullSerialization() { + Object[] array = new Object[] {null}; + String json = gson.toJson(array); + assertEquals("[null]", json); + } + + public void testArrayOfNullDeserialization() { + String[] values = gson.fromJson("[null]", String[].class); + assertNull(values[0]); + } + + /** + * Regression tests for Issue 272 + */ + public void testMultidimenstionalArraysSerialization() { + String[][] items = new String[][]{ + {"3m Co", "71.72", "0.02", "0.03", "4/2 12:00am", "Manufacturing"}, + {"Alcoa Inc", "29.01", "0.42", "1.47", "4/1 12:00am", "Manufacturing"} + }; + String json = gson.toJson(items); + assertTrue(json.contains("[[\"3m Co")); + assertTrue(json.contains("Manufacturing\"]]")); + } + + public void testMultiDimenstionalObjectArraysSerialization() { + Object[][] array = new Object[][] { new Object[] { 1, 2 } }; + assertEquals("[[1,2]]", gson.toJson(array)); + } + + /** + * Regression test for Issue 205 + */ + public void testMixingTypesInObjectArraySerialization() { + Object[] array = new Object[] { 1, 2, new Object[] { "one", "two", 3 } }; + assertEquals("[1,2,[\"one\",\"two\",3]]", gson.toJson(array)); + } + + /** + * Regression tests for Issue 272 + */ + public void testMultidimenstionalArraysDeserialization() { + String json = "[['3m Co','71.72','0.02','0.03','4/2 12:00am','Manufacturing']," + + "['Alcoa Inc','29.01','0.42','1.47','4/1 12:00am','Manufacturing']]"; + String[][] items = gson.fromJson(json, String[][].class); + assertEquals("3m Co", items[0][0]); + assertEquals("Manufacturing", items[1][5]); + } + + /** http://code.google.com/p/google-gson/issues/detail?id=342 */ + public void testArrayElementsAreArrays() { + Object[] stringArrays = { + new String[] {"test1", "test2"}, + new String[] {"test3", "test4"} + }; + assertEquals("[[\"test1\",\"test2\"],[\"test3\",\"test4\"]]", + new Gson().toJson(stringArrays)); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java b/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java new file mode 100644 index 00000000..d352e241 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java @@ -0,0 +1,126 @@ +/* + * 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.functional; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.TestTypes.ClassOverridingEquals; + +/** + * Functional tests related to circular reference detection and error reporting. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class CircularReferenceTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testCircularSerialization() throws Exception { + ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); + a.children.add(b); + b.children.add(a); + try { + gson.toJson(a); + fail("Circular types should not get printed!"); + } catch (StackOverflowError expected) { + } + } + + public void testSelfReferenceIgnoredInSerialization() throws Exception { + ClassOverridingEquals objA = new ClassOverridingEquals(); + objA.ref = objA; + + String json = gson.toJson(objA); + assertFalse(json.contains("ref")); // self-reference is ignored + } + + public void testSelfReferenceArrayFieldSerialization() throws Exception { + ClassWithSelfReferenceArray objA = new ClassWithSelfReferenceArray(); + objA.children = new ClassWithSelfReferenceArray[]{objA}; + + try { + gson.toJson(objA); + fail("Circular reference to self can not be serialized!"); + } catch (StackOverflowError expected) { + } + } + + public void testSelfReferenceCustomHandlerSerialization() throws Exception { + ClassWithSelfReference obj = new ClassWithSelfReference(); + obj.child = obj; + Gson gson = new GsonBuilder().registerTypeAdapter(ClassWithSelfReference.class, new JsonSerializer<ClassWithSelfReference>() { + public JsonElement serialize(ClassWithSelfReference src, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("property", "value"); + obj.add("child", context.serialize(src.child)); + return obj; + } + }).create(); + try { + gson.toJson(obj); + fail("Circular reference to self can not be serialized!"); + } catch (StackOverflowError expected) { + } + } + + public void testDirectedAcyclicGraphSerialization() throws Exception { + ContainsReferenceToSelfType a = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType b = new ContainsReferenceToSelfType(); + ContainsReferenceToSelfType c = new ContainsReferenceToSelfType(); + a.children.add(b); + a.children.add(c); + b.children.add(c); + assertNotNull(gson.toJson(a)); + } + + public void testDirectedAcyclicGraphDeserialization() throws Exception { + String json = "{\"children\":[{\"children\":[{\"children\":[]}]},{\"children\":[]}]}"; + ContainsReferenceToSelfType target = gson.fromJson(json, ContainsReferenceToSelfType.class); + assertNotNull(target); + assertEquals(2, target.children.size()); + } + + private static class ContainsReferenceToSelfType { + Collection<ContainsReferenceToSelfType> children = new ArrayList<ContainsReferenceToSelfType>(); + } + + private static class ClassWithSelfReference { + ClassWithSelfReference child; + } + + private static class ClassWithSelfReferenceArray { + @SuppressWarnings("unused") + ClassWithSelfReferenceArray[] children; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/CollectionTest.java b/gson/src/test/java/com/google/gson/functional/CollectionTest.java new file mode 100644 index 00000000..ac6fec93 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CollectionTest.java @@ -0,0 +1,360 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.MoreAsserts; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +/** + * Functional tests for Json serialization and deserialization of collections. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class CollectionTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testTopLevelCollectionOfIntegersSerialization() { + Collection<Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + Type targetType = new TypeToken<Collection<Integer>>() {}.getType(); + String json = gson.toJson(target, targetType); + assertEquals("[1,2,3,4,5,6,7,8,9]", json); + } + + public void testTopLevelCollectionOfIntegersDeserialization() { + String json = "[0,1,2,3,4,5,6,7,8,9]"; + Type collectionType = new TypeToken<Collection<Integer>>() { }.getType(); + Collection<Integer> target = gson.fromJson(json, collectionType); + int[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + MoreAsserts.assertEquals(expected, toIntArray(target)); + } + + public void testTopLevelListOfIntegerCollectionsDeserialization() throws Exception { + String json = "[[1,2,3],[4,5,6],[7,8,9]]"; + Type collectionType = new TypeToken<Collection<Collection<Integer>>>() {}.getType(); + List<Collection<Integer>> target = gson.fromJson(json, collectionType); + int[][] expected = new int[3][3]; + for (int i = 0; i < 3; ++i) { + int start = (3 * i) + 1; + for (int j = 0; j < 3; ++j) { + expected[i][j] = start + j; + } + } + + for (int i = 0; i < 3; i++) { + MoreAsserts.assertEquals(expected[i], toIntArray(target.get(i))); + } + } + + public void testLinkedListSerialization() { + List<String> list = new LinkedList<String>(); + list.add("a1"); + list.add("a2"); + Type linkedListType = new TypeToken<LinkedList<String>>() {}.getType(); + String json = gson.toJson(list, linkedListType); + assertTrue(json.contains("a1")); + assertTrue(json.contains("a2")); + } + + public void testLinkedListDeserialization() { + String json = "['a1','a2']"; + Type linkedListType = new TypeToken<LinkedList<String>>() {}.getType(); + List<String> list = gson.fromJson(json, linkedListType); + assertEquals("a1", list.get(0)); + assertEquals("a2", list.get(1)); + } + + public void testQueueSerialization() { + Queue<String> queue = new LinkedList<String>(); + queue.add("a1"); + queue.add("a2"); + Type queueType = new TypeToken<Queue<String>>() {}.getType(); + String json = gson.toJson(queue, queueType); + assertTrue(json.contains("a1")); + assertTrue(json.contains("a2")); + } + + public void testQueueDeserialization() { + String json = "['a1','a2']"; + Type queueType = new TypeToken<Queue<String>>() {}.getType(); + Queue<String> queue = gson.fromJson(json, queueType); + assertEquals("a1", queue.element()); + queue.remove(); + assertEquals("a2", queue.element()); + } + + public void testNullsInListSerialization() { + List<String> list = new ArrayList<String>(); + list.add("foo"); + list.add(null); + list.add("bar"); + String expected = "[\"foo\",null,\"bar\"]"; + Type typeOfList = new TypeToken<List<String>>() {}.getType(); + String json = gson.toJson(list, typeOfList); + assertEquals(expected, json); + } + + public void testNullsInListDeserialization() { + List<String> expected = new ArrayList<String>(); + expected.add("foo"); + expected.add(null); + expected.add("bar"); + String json = "[\"foo\",null,\"bar\"]"; + Type expectedType = new TypeToken<List<String>>() {}.getType(); + List<String> target = gson.fromJson(json, expectedType); + for (int i = 0; i < expected.size(); ++i) { + assertEquals(expected.get(i), target.get(i)); + } + } + + public void testCollectionOfObjectSerialization() { + List<Object> target = new ArrayList<Object>(); + target.add("Hello"); + target.add("World"); + assertEquals("[\"Hello\",\"World\"]", gson.toJson(target)); + + Type type = new TypeToken<List<Object>>() {}.getType(); + assertEquals("[\"Hello\",\"World\"]", gson.toJson(target, type)); + } + + public void testCollectionOfObjectWithNullSerialization() { + List<Object> target = new ArrayList<Object>(); + target.add("Hello"); + target.add(null); + target.add("World"); + assertEquals("[\"Hello\",null,\"World\"]", gson.toJson(target)); + + Type type = new TypeToken<List<Object>>() {}.getType(); + assertEquals("[\"Hello\",null,\"World\"]", gson.toJson(target, type)); + } + + public void testCollectionOfStringsSerialization() { + List<String> target = new ArrayList<String>(); + target.add("Hello"); + target.add("World"); + assertEquals("[\"Hello\",\"World\"]", gson.toJson(target)); + } + + public void testCollectionOfBagOfPrimitivesSerialization() { + List<BagOfPrimitives> target = new ArrayList<BagOfPrimitives>(); + BagOfPrimitives objA = new BagOfPrimitives(3L, 1, true, "blah"); + BagOfPrimitives objB = new BagOfPrimitives(2L, 6, false, "blahB"); + target.add(objA); + target.add(objB); + + String result = gson.toJson(target); + assertTrue(result.startsWith("[")); + assertTrue(result.endsWith("]")); + for (BagOfPrimitives obj : target) { + assertTrue(result.contains(obj.getExpectedJson())); + } + } + + public void testCollectionOfStringsDeserialization() { + String json = "[\"Hello\",\"World\"]"; + Type collectionType = new TypeToken<Collection<String>>() { }.getType(); + Collection<String> target = gson.fromJson(json, collectionType); + + assertTrue(target.contains("Hello")); + assertTrue(target.contains("World")); + } + + public void testRawCollectionOfIntegersSerialization() { + Collection<Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + assertEquals("[1,2,3,4,5,6,7,8,9]", gson.toJson(target)); + } + + @SuppressWarnings("rawtypes") + public void testRawCollectionSerialization() { + BagOfPrimitives bag1 = new BagOfPrimitives(); + Collection target = Arrays.asList(bag1, bag1); + String json = gson.toJson(target); + assertTrue(json.contains(bag1.getExpectedJson())); + } + + @SuppressWarnings("rawtypes") + public void testRawCollectionDeserializationNotAlllowed() { + String json = "[0,1,2,3,4,5,6,7,8,9]"; + Collection integers = gson.fromJson(json, Collection.class); + // JsonReader converts numbers to double by default so we need a floating point comparison + assertEquals(Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0), integers); + + json = "[\"Hello\", \"World\"]"; + Collection strings = gson.fromJson(json, Collection.class); + assertTrue(strings.contains("Hello")); + assertTrue(strings.contains("World")); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public void testRawCollectionOfBagOfPrimitivesNotAllowed() { + BagOfPrimitives bag = new BagOfPrimitives(10, 20, false, "stringValue"); + String json = '[' + bag.getExpectedJson() + ',' + bag.getExpectedJson() + ']'; + Collection target = gson.fromJson(json, Collection.class); + assertEquals(2, target.size()); + for (Object bag1 : target) { + // Gson 2.0 converts raw objects into maps + Map<String, Object> values = (Map<String, Object>) bag1; + assertTrue(values.containsValue(10.0)); + assertTrue(values.containsValue(20.0)); + assertTrue(values.containsValue("stringValue")); + } + } + + public void testWildcardPrimitiveCollectionSerilaization() throws Exception { + Collection<? extends Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + Type collectionType = new TypeToken<Collection<? extends Integer>>() { }.getType(); + String json = gson.toJson(target, collectionType); + assertEquals("[1,2,3,4,5,6,7,8,9]", json); + + json = gson.toJson(target); + assertEquals("[1,2,3,4,5,6,7,8,9]", json); + } + + public void testWildcardPrimitiveCollectionDeserilaization() throws Exception { + String json = "[1,2,3,4,5,6,7,8,9]"; + Type collectionType = new TypeToken<Collection<? extends Integer>>() { }.getType(); + Collection<? extends Integer> target = gson.fromJson(json, collectionType); + assertEquals(9, target.size()); + assertTrue(target.contains(1)); + assertTrue(target.contains(9)); + } + + public void testWildcardCollectionField() throws Exception { + Collection<BagOfPrimitives> collection = new ArrayList<BagOfPrimitives>(); + BagOfPrimitives objA = new BagOfPrimitives(3L, 1, true, "blah"); + BagOfPrimitives objB = new BagOfPrimitives(2L, 6, false, "blahB"); + collection.add(objA); + collection.add(objB); + + ObjectWithWildcardCollection target = new ObjectWithWildcardCollection(collection); + String json = gson.toJson(target); + assertTrue(json.contains(objA.getExpectedJson())); + assertTrue(json.contains(objB.getExpectedJson())); + + target = gson.fromJson(json, ObjectWithWildcardCollection.class); + Collection<? extends BagOfPrimitives> deserializedCollection = target.getCollection(); + assertEquals(2, deserializedCollection.size()); + assertTrue(deserializedCollection.contains(objA)); + assertTrue(deserializedCollection.contains(objB)); + } + + public void testFieldIsArrayList() { + HasArrayListField object = new HasArrayListField(); + object.longs.add(1L); + object.longs.add(3L); + String json = gson.toJson(object, HasArrayListField.class); + assertEquals("{\"longs\":[1,3]}", json); + HasArrayListField copy = gson.fromJson("{\"longs\":[1,3]}", HasArrayListField.class); + assertEquals(Arrays.asList(1L, 3L), copy.longs); + } + + public void testUserCollectionTypeAdapter() { + Type listOfString = new TypeToken<List<String>>() {}.getType(); + Object stringListSerializer = new JsonSerializer<List<String>>() { + public JsonElement serialize(List<String> src, Type typeOfSrc, + JsonSerializationContext context) { + return new JsonPrimitive(src.get(0) + ";" + src.get(1)); + } + }; + Gson gson = new GsonBuilder() + .registerTypeAdapter(listOfString, stringListSerializer) + .create(); + assertEquals("\"ab;cd\"", gson.toJson(Arrays.asList("ab", "cd"), listOfString)); + } + + static class HasArrayListField { + ArrayList<Long> longs = new ArrayList<Long>(); + } + + @SuppressWarnings("rawtypes") + private static int[] toIntArray(Collection collection) { + int[] ints = new int[collection.size()]; + int i = 0; + for (Iterator iterator = collection.iterator(); iterator.hasNext(); ++i) { + Object obj = iterator.next(); + if (obj instanceof Integer) { + ints[i] = ((Integer)obj).intValue(); + } else if (obj instanceof Long) { + ints[i] = ((Long)obj).intValue(); + } + } + return ints; + } + + private static class ObjectWithWildcardCollection { + private final Collection<? extends BagOfPrimitives> collection; + + public ObjectWithWildcardCollection(Collection<? extends BagOfPrimitives> collection) { + this.collection = collection; + } + + public Collection<? extends BagOfPrimitives> getCollection() { + return collection; + } + } + + private static class Entry { + int value; + Entry(int value) { + this.value = value; + } + } + public void testSetSerialization() { + Set<Entry> set = new HashSet<Entry>(); + set.add(new Entry(1)); + set.add(new Entry(2)); + String json = gson.toJson(set); + assertTrue(json.contains("1")); + assertTrue(json.contains("2")); + } + public void testSetDeserialization() { + String json = "[{value:1},{value:2}]"; + Type type = new TypeToken<Set<Entry>>() {}.getType(); + Set<Entry> set = gson.fromJson(json, type); + assertEquals(2, set.size()); + for (Entry entry : set) { + assertTrue(entry.value == 1 || entry.value == 2); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java b/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java new file mode 100755 index 00000000..2dccf4b6 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java @@ -0,0 +1,140 @@ +/*
+ * 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.functional;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import junit.framework.TestCase;
+
+import com.google.gson.Gson;
+
+/**
+ * Tests for ensuring Gson thread-safety.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ConcurrencyTest extends TestCase {
+ private Gson gson;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ gson = new Gson();
+ }
+
+ /**
+ * Source-code based on
+ * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+ */
+ public void testSingleThreadSerialization() {
+ MyObject myObj = new MyObject();
+ for (int i = 0; i < 10; i++) {
+ gson.toJson(myObj);
+ }
+ }
+
+ /**
+ * Source-code based on
+ * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+ */
+ public void testSingleThreadDeserialization() {
+ for (int i = 0; i < 10; i++) {
+ gson.fromJson("{'a':'hello','b':'world','i':1}", MyObject.class);
+ }
+ }
+
+ /**
+ * Source-code based on
+ * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+ */
+ public void testMultiThreadSerialization() throws InterruptedException {
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch finishedLatch = new CountDownLatch(10);
+ final AtomicBoolean failed = new AtomicBoolean(false);
+ ExecutorService executor = Executors.newFixedThreadPool(10);
+ for (int taskCount = 0; taskCount < 10; taskCount++) {
+ executor.execute(new Runnable() {
+ public void run() {
+ MyObject myObj = new MyObject();
+ try {
+ startLatch.await();
+ for (int i = 0; i < 10; i++) {
+ gson.toJson(myObj);
+ }
+ } catch (Throwable t) {
+ failed.set(true);
+ } finally {
+ finishedLatch.countDown();
+ }
+ }
+ });
+ }
+ startLatch.countDown();
+ finishedLatch.await();
+ assertFalse(failed.get());
+ }
+
+ /**
+ * Source-code based on
+ * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+ */
+ public void testMultiThreadDeserialization() throws InterruptedException {
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch finishedLatch = new CountDownLatch(10);
+ final AtomicBoolean failed = new AtomicBoolean(false);
+ ExecutorService executor = Executors.newFixedThreadPool(10);
+ for (int taskCount = 0; taskCount < 10; taskCount++) {
+ executor.execute(new Runnable() {
+ public void run() {
+ try {
+ startLatch.await();
+ for (int i = 0; i < 10; i++) {
+ gson.fromJson("{'a':'hello','b':'world','i':1}", MyObject.class);
+ }
+ } catch (Throwable t) {
+ failed.set(true);
+ } finally {
+ finishedLatch.countDown();
+ }
+ }
+ });
+ }
+ startLatch.countDown();
+ finishedLatch.await();
+ assertFalse(failed.get());
+ }
+
+ @SuppressWarnings("unused")
+ private static class MyObject {
+ String a;
+ String b;
+ int i;
+
+ MyObject() {
+ this("hello", "world", 42);
+ }
+
+ public MyObject(String a, String b, int i) {
+ this.a = a;
+ this.b = b;
+ this.i = i;
+ }
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java b/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java new file mode 100644 index 00000000..54ecade2 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java @@ -0,0 +1,207 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.common.TestTypes.Base; +import com.google.gson.common.TestTypes.ClassWithBaseField; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; + +/** + * Functional Test exercising custom deserialization only. When test applies to both + * serialization and deserialization then add it to CustomTypeAdapterTest. + * + * @author Joel Leitch + */ +public class CustomDeserializerTest extends TestCase { + private static final String DEFAULT_VALUE = "test123"; + private static final String SUFFIX = "blah"; + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new GsonBuilder().registerTypeAdapter(DataHolder.class, new DataHolderDeserializer()).create(); + } + + public void testDefaultConstructorNotCalledOnObject() throws Exception { + DataHolder data = new DataHolder(DEFAULT_VALUE); + String json = gson.toJson(data); + + DataHolder actual = gson.fromJson(json, DataHolder.class); + assertEquals(DEFAULT_VALUE + SUFFIX, actual.getData()); + } + + public void testDefaultConstructorNotCalledOnField() throws Exception { + DataHolderWrapper dataWrapper = new DataHolderWrapper(new DataHolder(DEFAULT_VALUE)); + String json = gson.toJson(dataWrapper); + + DataHolderWrapper actual = gson.fromJson(json, DataHolderWrapper.class); + assertEquals(DEFAULT_VALUE + SUFFIX, actual.getWrappedData().getData()); + } + + private static class DataHolder { + private final String data; + + // For use by Gson + @SuppressWarnings("unused") + private DataHolder() { + throw new IllegalStateException(); + } + + public DataHolder(String data) { + this.data = data; + } + + public String getData() { + return data; + } + } + + private static class DataHolderWrapper { + private final DataHolder wrappedData; + + // For use by Gson + @SuppressWarnings("unused") + private DataHolderWrapper() { + this(new DataHolder(DEFAULT_VALUE)); + } + + public DataHolderWrapper(DataHolder data) { + this.wrappedData = data; + } + + public DataHolder getWrappedData() { + return wrappedData; + } + } + + private static class DataHolderDeserializer implements JsonDeserializer<DataHolder> { + public DataHolder deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObj = json.getAsJsonObject(); + String dataString = jsonObj.get("data").getAsString(); + return new DataHolder(dataString + SUFFIX); + } + } + + public void testJsonTypeFieldBasedDeserialization() { + String json = "{field1:'abc',field2:'def',__type__:'SUB_TYPE1'}"; + Gson gson = new GsonBuilder().registerTypeAdapter(MyBase.class, new JsonDeserializer<MyBase>() { + public MyBase deserialize(JsonElement json, Type pojoType, + JsonDeserializationContext context) throws JsonParseException { + String type = json.getAsJsonObject().get(MyBase.TYPE_ACCESS).getAsString(); + return context.deserialize(json, SubTypes.valueOf(type).getSubclass()); + } + }).create(); + SubType1 target = (SubType1) gson.fromJson(json, MyBase.class); + assertEquals("abc", target.field1); + } + + private static class MyBase { + static final String TYPE_ACCESS = "__type__"; + } + + private enum SubTypes { + SUB_TYPE1(SubType1.class), + SUB_TYPE2(SubType2.class); + private final Type subClass; + private SubTypes(Type subClass) { + this.subClass = subClass; + } + public Type getSubclass() { + return subClass; + } + } + + private static class SubType1 extends MyBase { + String field1; + } + + private static class SubType2 extends MyBase { + @SuppressWarnings("unused") + String field2; + } + + public void testCustomDeserializerReturnsNullForTopLevelObject() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonDeserializer<Base>() { + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return null; + } + }).create(); + String json = "{baseName:'Base',subName:'SubRevised'}"; + Base target = gson.fromJson(json, Base.class); + assertNull(target); + } + + public void testCustomDeserializerReturnsNull() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonDeserializer<Base>() { + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return null; + } + }).create(); + String json = "{base:{baseName:'Base',subName:'SubRevised'}}"; + ClassWithBaseField target = gson.fromJson(json, ClassWithBaseField.class); + assertNull(target.base); + } + + public void testCustomDeserializerReturnsNullForArrayElements() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonDeserializer<Base>() { + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return null; + } + }).create(); + String json = "[{baseName:'Base'},{baseName:'Base'}]"; + Base[] target = gson.fromJson(json, Base[].class); + assertNull(target[0]); + assertNull(target[1]); + } + + public void testCustomDeserializerReturnsNullForArrayElementsForArrayField() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonDeserializer<Base>() { + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return null; + } + }).create(); + String json = "{bases:[{baseName:'Base'},{baseName:'Base'}]}"; + ClassWithBaseArray target = gson.fromJson(json, ClassWithBaseArray.class); + assertNull(target.bases[0]); + assertNull(target.bases[1]); + } + + private static class ClassWithBaseArray { + Base[] bases; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java b/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java new file mode 100644 index 00000000..c8095463 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java @@ -0,0 +1,102 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.TestTypes.Base; +import com.google.gson.common.TestTypes.BaseSerializer; +import com.google.gson.common.TestTypes.ClassWithBaseArrayField; +import com.google.gson.common.TestTypes.ClassWithBaseField; +import com.google.gson.common.TestTypes.Sub; +import com.google.gson.common.TestTypes.SubSerializer; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; + +/** + * Functional Test exercising custom serialization only. When test applies to both + * serialization and deserialization then add it to CustomTypeAdapterTest. + * + * @author Inderjeet Singh + */ +public class CustomSerializerTest extends TestCase { + + public void testBaseClassSerializerInvokedForBaseClassFields() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new BaseSerializer()) + .registerTypeAdapter(Sub.class, new SubSerializer()) + .create(); + ClassWithBaseField target = new ClassWithBaseField(new Base()); + JsonObject json = (JsonObject) gson.toJsonTree(target); + JsonObject base = json.get("base").getAsJsonObject(); + assertEquals(BaseSerializer.NAME, base.get(Base.SERIALIZER_KEY).getAsString()); + } + + public void testSubClassSerializerInvokedForBaseClassFieldsHoldingSubClassInstances() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new BaseSerializer()) + .registerTypeAdapter(Sub.class, new SubSerializer()) + .create(); + ClassWithBaseField target = new ClassWithBaseField(new Sub()); + JsonObject json = (JsonObject) gson.toJsonTree(target); + JsonObject base = json.get("base").getAsJsonObject(); + assertEquals(SubSerializer.NAME, base.get(Base.SERIALIZER_KEY).getAsString()); + } + + public void testSubClassSerializerInvokedForBaseClassFieldsHoldingArrayOfSubClassInstances() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new BaseSerializer()) + .registerTypeAdapter(Sub.class, new SubSerializer()) + .create(); + ClassWithBaseArrayField target = new ClassWithBaseArrayField(new Base[] {new Sub(), new Sub()}); + JsonObject json = (JsonObject) gson.toJsonTree(target); + JsonArray array = json.get("base").getAsJsonArray(); + for (JsonElement element : array) { + JsonElement serializerKey = element.getAsJsonObject().get(Base.SERIALIZER_KEY); + assertEquals(SubSerializer.NAME, serializerKey.getAsString()); + } + } + + public void testBaseClassSerializerInvokedForBaseClassFieldsHoldingSubClassInstances() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new BaseSerializer()) + .create(); + ClassWithBaseField target = new ClassWithBaseField(new Sub()); + JsonObject json = (JsonObject) gson.toJsonTree(target); + JsonObject base = json.get("base").getAsJsonObject(); + assertEquals(BaseSerializer.NAME, base.get(Base.SERIALIZER_KEY).getAsString()); + } + + public void testSerializerReturnsNull() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonSerializer<Base>() { + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return null; + } + }) + .create(); + JsonElement json = gson.toJsonTree(new Base()); + assertTrue(json.isJsonNull()); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java new file mode 100644 index 00000000..93ec7858 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java @@ -0,0 +1,456 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.ClassWithCustomTypeConverter; +import com.google.gson.reflect.TypeToken; + +import java.util.Date; +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Functional tests for the support of custom serializer and deserializers. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class CustomTypeAdaptersTest extends TestCase { + private GsonBuilder builder; + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new GsonBuilder(); + } + + public void testCustomSerializers() { + Gson gson = builder.registerTypeAdapter( + ClassWithCustomTypeConverter.class, new JsonSerializer<ClassWithCustomTypeConverter>() { + public JsonElement serialize(ClassWithCustomTypeConverter src, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject json = new JsonObject(); + json.addProperty("bag", 5); + json.addProperty("value", 25); + return json; + } + }).create(); + ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter(); + assertEquals("{\"bag\":5,\"value\":25}", gson.toJson(target)); + } + + public void testCustomDeserializers() { + Gson gson = new GsonBuilder().registerTypeAdapter( + ClassWithCustomTypeConverter.class, new JsonDeserializer<ClassWithCustomTypeConverter>() { + public ClassWithCustomTypeConverter deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) { + JsonObject jsonObject = json.getAsJsonObject(); + int value = jsonObject.get("bag").getAsInt(); + return new ClassWithCustomTypeConverter(new BagOfPrimitives(value, + value, false, ""), value); + } + }).create(); + String json = "{\"bag\":5,\"value\":25}"; + ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class); + assertEquals(5, target.getBag().getIntValue()); + } + + public void disable_testCustomSerializersOfSelf() { + Gson gson = createGsonObjectWithFooTypeAdapter(); + Gson basicGson = new Gson(); + Foo newFooObject = new Foo(1, 2L); + String jsonFromCustomSerializer = gson.toJson(newFooObject); + String jsonFromGson = basicGson.toJson(newFooObject); + + assertEquals(jsonFromGson, jsonFromCustomSerializer); + } + + public void disable_testCustomDeserializersOfSelf() { + Gson gson = createGsonObjectWithFooTypeAdapter(); + Gson basicGson = new Gson(); + Foo expectedFoo = new Foo(1, 2L); + String json = basicGson.toJson(expectedFoo); + Foo newFooObject = gson.fromJson(json, Foo.class); + + assertEquals(expectedFoo.key, newFooObject.key); + assertEquals(expectedFoo.value, newFooObject.value); + } + + public void testCustomNestedSerializers() { + Gson gson = new GsonBuilder().registerTypeAdapter( + BagOfPrimitives.class, new JsonSerializer<BagOfPrimitives>() { + public JsonElement serialize(BagOfPrimitives src, Type typeOfSrc, + JsonSerializationContext context) { + return new JsonPrimitive(6); + } + }).create(); + ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter(); + assertEquals("{\"bag\":6,\"value\":10}", gson.toJson(target)); + } + + public void testCustomNestedDeserializers() { + Gson gson = new GsonBuilder().registerTypeAdapter( + BagOfPrimitives.class, new JsonDeserializer<BagOfPrimitives>() { + public BagOfPrimitives deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + int value = json.getAsInt(); + return new BagOfPrimitives(value, value, false, ""); + } + }).create(); + String json = "{\"bag\":7,\"value\":25}"; + ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class); + assertEquals(7, target.getBag().getIntValue()); + } + + public void testCustomTypeAdapterDoesNotAppliesToSubClasses() { + Gson gson = new GsonBuilder().registerTypeAdapter(Base.class, new JsonSerializer<Base> () { + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + json.addProperty("value", src.baseValue); + return json; + } + }).create(); + Base b = new Base(); + String json = gson.toJson(b); + assertTrue(json.contains("value")); + b = new Derived(); + json = gson.toJson(b); + assertTrue(json.contains("derivedValue")); + } + + public void testCustomTypeAdapterAppliesToSubClassesSerializedAsBaseClass() { + Gson gson = new GsonBuilder().registerTypeAdapter(Base.class, new JsonSerializer<Base> () { + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + json.addProperty("value", src.baseValue); + return json; + } + }).create(); + Base b = new Base(); + String json = gson.toJson(b); + assertTrue(json.contains("value")); + b = new Derived(); + json = gson.toJson(b, Base.class); + assertTrue(json.contains("value")); + assertFalse(json.contains("derivedValue")); + } + + private static class Base { + int baseValue = 2; + } + + private static class Derived extends Base { + @SuppressWarnings("unused") + int derivedValue = 3; + } + + + private Gson createGsonObjectWithFooTypeAdapter() { + return new GsonBuilder().registerTypeAdapter(Foo.class, new FooTypeAdapter()).create(); + } + + public static class Foo { + private final int key; + private final long value; + + public Foo() { + this(0, 0L); + } + + public Foo(int key, long value) { + this.key = key; + this.value = value; + } + } + + public static class FooTypeAdapter implements JsonSerializer<Foo>, JsonDeserializer<Foo> { + public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return context.deserialize(json, typeOfT); + } + + public JsonElement serialize(Foo src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src, typeOfSrc); + } + } + + public void testCustomSerializerInvokedForPrimitives() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(boolean.class, new JsonSerializer<Boolean>() { + public JsonElement serialize(Boolean s, Type t, JsonSerializationContext c) { + return new JsonPrimitive(s ? 1 : 0); + } + }) + .create(); + assertEquals("1", gson.toJson(true, boolean.class)); + assertEquals("true", gson.toJson(true, Boolean.class)); + } + + @SuppressWarnings("rawtypes") + public void testCustomDeserializerInvokedForPrimitives() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(boolean.class, new JsonDeserializer() { + public Object deserialize(JsonElement json, Type t, JsonDeserializationContext context) { + return json.getAsInt() != 0; + } + }) + .create(); + assertEquals(Boolean.TRUE, gson.fromJson("1", boolean.class)); + assertEquals(Boolean.TRUE, gson.fromJson("true", Boolean.class)); + } + + public void testCustomByteArraySerializer() { + Gson gson = new GsonBuilder().registerTypeAdapter(byte[].class, new JsonSerializer<byte[]>() { + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + StringBuilder sb = new StringBuilder(src.length); + for (byte b : src) { + sb.append(b); + } + return new JsonPrimitive(sb.toString()); + } + }).create(); + byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + String json = gson.toJson(data); + assertEquals("\"0123456789\"", json); + } + + public void testCustomByteArrayDeserializerAndInstanceCreator() { + GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(byte[].class, + new JsonDeserializer<byte[]>() { + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String str = json.getAsString(); + byte[] data = new byte[str.length()]; + for (int i = 0; i < data.length; ++i) { + data[i] = Byte.parseByte(""+str.charAt(i)); + } + return data; + } + }); + Gson gson = gsonBuilder.create(); + String json = "'0123456789'"; + byte[] actual = gson.fromJson(json, byte[].class); + byte[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + for (int i = 0; i < actual.length; ++i) { + assertEquals(expected[i], actual[i]); + } + } + + private static class StringHolder { + String part1; + String part2; + + public StringHolder(String string) { + String[] parts = string.split(":"); + part1 = parts[0]; + part2 = parts[1]; + } + public StringHolder(String part1, String part2) { + this.part1 = part1; + this.part2 = part2; + } + } + + private static class StringHolderTypeAdapter implements JsonSerializer<StringHolder>, + JsonDeserializer<StringHolder>, InstanceCreator<StringHolder> { + + public StringHolder createInstance(Type type) { + //Fill up with objects that will be thrown away + return new StringHolder("unknown:thing"); + } + + public StringHolder deserialize(JsonElement src, Type type, + JsonDeserializationContext context) { + return new StringHolder(src.getAsString()); + } + + public JsonElement serialize(StringHolder src, Type typeOfSrc, + JsonSerializationContext context) { + String contents = src.part1 + ':' + src.part2; + return new JsonPrimitive(contents); + } + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForCollectionElementSerializationWithType() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + Type setType = new TypeToken<Set<StringHolder>>() {}.getType(); + StringHolder holder = new StringHolder("Jacob", "Tomaw"); + Set<StringHolder> setOfHolders = new HashSet<StringHolder>(); + setOfHolders.add(holder); + String json = gson.toJson(setOfHolders, setType); + assertTrue(json.contains("Jacob:Tomaw")); + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForCollectionElementSerialization() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + StringHolder holder = new StringHolder("Jacob", "Tomaw"); + Set<StringHolder> setOfHolders = new HashSet<StringHolder>(); + setOfHolders.add(holder); + String json = gson.toJson(setOfHolders); + assertTrue(json.contains("Jacob:Tomaw")); + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForCollectionElementDeserialization() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + Type setType = new TypeToken<Set<StringHolder>>() {}.getType(); + Set<StringHolder> setOfHolders = gson.fromJson("['Jacob:Tomaw']", setType); + assertEquals(1, setOfHolders.size()); + StringHolder foo = setOfHolders.iterator().next(); + assertEquals("Jacob", foo.part1); + assertEquals("Tomaw", foo.part2); + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForMapElementSerializationWithType() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + Type mapType = new TypeToken<Map<String,StringHolder>>() {}.getType(); + StringHolder holder = new StringHolder("Jacob", "Tomaw"); + Map<String, StringHolder> mapOfHolders = new HashMap<String, StringHolder>(); + mapOfHolders.put("foo", holder); + String json = gson.toJson(mapOfHolders, mapType); + assertTrue(json.contains("\"foo\":\"Jacob:Tomaw\"")); + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForMapElementSerialization() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + StringHolder holder = new StringHolder("Jacob", "Tomaw"); + Map<String, StringHolder> mapOfHolders = new HashMap<String, StringHolder>(); + mapOfHolders.put("foo", holder); + String json = gson.toJson(mapOfHolders); + assertTrue(json.contains("\"foo\":\"Jacob:Tomaw\"")); + } + + // Test created from Issue 70 + public void testCustomAdapterInvokedForMapElementDeserialization() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter()) + .create(); + Type mapType = new TypeToken<Map<String, StringHolder>>() {}.getType(); + Map<String, StringHolder> mapOfFoo = gson.fromJson("{'foo':'Jacob:Tomaw'}", mapType); + assertEquals(1, mapOfFoo.size()); + StringHolder foo = mapOfFoo.get("foo"); + assertEquals("Jacob", foo.part1); + assertEquals("Tomaw", foo.part2); + } + + public void testEnsureCustomSerializerNotInvokedForNullValues() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(DataHolder.class, new DataHolderSerializer()) + .create(); + DataHolderWrapper target = new DataHolderWrapper(new DataHolder("abc")); + String json = gson.toJson(target); + assertEquals("{\"wrappedData\":{\"myData\":\"abc\"}}", json); + } + + public void testEnsureCustomDeserializerNotInvokedForNullValues() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(DataHolder.class, new DataHolderDeserializer()) + .create(); + String json = "{wrappedData:null}"; + DataHolderWrapper actual = gson.fromJson(json, DataHolderWrapper.class); + assertNull(actual.wrappedData); + } + + // Test created from Issue 352 + public void testRegisterHierarchyAdapterForDate() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Date.class, new DateTypeAdapter()) + .create(); + assertEquals("0", gson.toJson(new Date(0))); + assertEquals("0", gson.toJson(new java.sql.Date(0))); + assertEquals(new Date(0), gson.fromJson("0", Date.class)); + assertEquals(new java.sql.Date(0), gson.fromJson("0", java.sql.Date.class)); + } + + private static class DataHolder { + final String data; + + public DataHolder(String data) { + this.data = data; + } + } + + private static class DataHolderWrapper { + final DataHolder wrappedData; + + public DataHolderWrapper(DataHolder data) { + this.wrappedData = data; + } + } + + private static class DataHolderSerializer implements JsonSerializer<DataHolder> { + public JsonElement serialize(DataHolder src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("myData", src.data); + return obj; + } + } + + private static class DataHolderDeserializer implements JsonDeserializer<DataHolder> { + public DataHolder deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObj = json.getAsJsonObject(); + JsonElement jsonElement = jsonObj.get("data"); + if (jsonElement == null || jsonElement.isJsonNull()) { + return new DataHolder(null); + } + return new DataHolder(jsonElement.getAsString()); + } + } + + private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> { + public Date deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + return typeOfT == Date.class + ? new Date(json.getAsLong()) + : new java.sql.Date(json.getAsLong()); + } + public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getTime()); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java new file mode 100644 index 00000000..2b4db893 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -0,0 +1,724 @@ +/* + * 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.functional; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import java.util.UUID; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Functional test for Json serialization and deserialization for common classes for which default + * support is provided in Gson. The tests for Map types are available in {@link MapTest}. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class DefaultTypeAdaptersTest extends TestCase { + private Gson gson; + private TimeZone oldTimeZone; + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.oldTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); + Locale.setDefault(Locale.US); + gson = new Gson(); + } + + @Override + protected void tearDown() throws Exception { + TimeZone.setDefault(oldTimeZone); + super.tearDown(); + } + + public void testClassSerialization() { + try { + gson.toJson(String.class); + } catch (UnsupportedOperationException expected) {} + // Override with a custom type adapter for class. + gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); + assertEquals("\"java.lang.String\"", gson.toJson(String.class)); + } + + public void testClassDeserialization() { + try { + gson.fromJson("String.class", String.class.getClass()); + } catch (UnsupportedOperationException expected) {} + // Override with a custom type adapter for class. + gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); + assertEquals(String.class, gson.fromJson("java.lang.String", Class.class)); + } + + public void testUrlSerialization() throws Exception { + String urlValue = "http://google.com/"; + URL url = new URL(urlValue); + assertEquals("\"http://google.com/\"", gson.toJson(url)); + } + + public void testUrlDeserialization() { + String urlValue = "http://google.com/"; + String json = "'http:\\/\\/google.com\\/'"; + URL target = gson.fromJson(json, URL.class); + assertEquals(urlValue, target.toExternalForm()); + + gson.fromJson('"' + urlValue + '"', URL.class); + assertEquals(urlValue, target.toExternalForm()); + } + + public void testUrlNullSerialization() throws Exception { + ClassWithUrlField target = new ClassWithUrlField(); + assertEquals("{}", gson.toJson(target)); + } + + public void testUrlNullDeserialization() { + String json = "{}"; + ClassWithUrlField target = gson.fromJson(json, ClassWithUrlField.class); + assertNull(target.url); + } + + private static class ClassWithUrlField { + URL url; + } + + public void testUriSerialization() throws Exception { + String uriValue = "http://google.com/"; + URI uri = new URI(uriValue); + assertEquals("\"http://google.com/\"", gson.toJson(uri)); + } + + public void testUriDeserialization() { + String uriValue = "http://google.com/"; + String json = '"' + uriValue + '"'; + URI target = gson.fromJson(json, URI.class); + assertEquals(uriValue, target.toASCIIString()); + } + + public void testNullSerialization() throws Exception { + testNullSerializationAndDeserialization(Boolean.class); + testNullSerializationAndDeserialization(Byte.class); + testNullSerializationAndDeserialization(Short.class); + testNullSerializationAndDeserialization(Integer.class); + testNullSerializationAndDeserialization(Long.class); + testNullSerializationAndDeserialization(Double.class); + testNullSerializationAndDeserialization(Float.class); + testNullSerializationAndDeserialization(Number.class); + testNullSerializationAndDeserialization(Character.class); + testNullSerializationAndDeserialization(String.class); + testNullSerializationAndDeserialization(StringBuilder.class); + testNullSerializationAndDeserialization(StringBuffer.class); + testNullSerializationAndDeserialization(BigDecimal.class); + testNullSerializationAndDeserialization(BigInteger.class); + testNullSerializationAndDeserialization(TreeSet.class); + testNullSerializationAndDeserialization(ArrayList.class); + testNullSerializationAndDeserialization(HashSet.class); + testNullSerializationAndDeserialization(Properties.class); + testNullSerializationAndDeserialization(URL.class); + testNullSerializationAndDeserialization(URI.class); + testNullSerializationAndDeserialization(UUID.class); + testNullSerializationAndDeserialization(Locale.class); + testNullSerializationAndDeserialization(InetAddress.class); + testNullSerializationAndDeserialization(BitSet.class); + testNullSerializationAndDeserialization(Date.class); + testNullSerializationAndDeserialization(GregorianCalendar.class); + testNullSerializationAndDeserialization(Calendar.class); + testNullSerializationAndDeserialization(Time.class); + testNullSerializationAndDeserialization(Timestamp.class); + testNullSerializationAndDeserialization(java.sql.Date.class); + testNullSerializationAndDeserialization(Enum.class); + testNullSerializationAndDeserialization(Class.class); + } + + private void testNullSerializationAndDeserialization(Class<?> c) { + assertEquals("null", gson.toJson(null, c)); + assertEquals(null, gson.fromJson("null", c)); + } + + public void testUuidSerialization() throws Exception { + String uuidValue = "c237bec1-19ef-4858-a98e-521cf0aad4c0"; + UUID uuid = UUID.fromString(uuidValue); + assertEquals('"' + uuidValue + '"', gson.toJson(uuid)); + } + + public void testUuidDeserialization() { + String uuidValue = "c237bec1-19ef-4858-a98e-521cf0aad4c0"; + String json = '"' + uuidValue + '"'; + UUID target = gson.fromJson(json, UUID.class); + assertEquals(uuidValue, target.toString()); + } + + public void testLocaleSerializationWithLanguage() { + Locale target = new Locale("en"); + assertEquals("\"en\"", gson.toJson(target)); + } + + public void testLocaleDeserializationWithLanguage() { + String json = "\"en\""; + Locale locale = gson.fromJson(json, Locale.class); + assertEquals("en", locale.getLanguage()); + } + + public void testLocaleSerializationWithLanguageCountry() { + Locale target = Locale.CANADA_FRENCH; + assertEquals("\"fr_CA\"", gson.toJson(target)); + } + + public void testLocaleDeserializationWithLanguageCountry() { + String json = "\"fr_CA\""; + Locale locale = gson.fromJson(json, Locale.class); + assertEquals(Locale.CANADA_FRENCH, locale); + } + + public void testLocaleSerializationWithLanguageCountryVariant() { + Locale target = new Locale("de", "DE", "EURO"); + String json = gson.toJson(target); + assertEquals("\"de_DE_EURO\"", json); + } + + public void testLocaleDeserializationWithLanguageCountryVariant() { + String json = "\"de_DE_EURO\""; + Locale locale = gson.fromJson(json, Locale.class); + assertEquals("de", locale.getLanguage()); + assertEquals("DE", locale.getCountry()); + assertEquals("EURO", locale.getVariant()); + } + + public void testBigDecimalFieldSerialization() { + ClassWithBigDecimal target = new ClassWithBigDecimal("-122.01e-21"); + String json = gson.toJson(target); + String actual = json.substring(json.indexOf(':') + 1, json.indexOf('}')); + assertEquals(target.value, new BigDecimal(actual)); + } + + public void testBigDecimalFieldDeserialization() { + ClassWithBigDecimal expected = new ClassWithBigDecimal("-122.01e-21"); + String json = expected.getExpectedJson(); + ClassWithBigDecimal actual = gson.fromJson(json, ClassWithBigDecimal.class); + assertEquals(expected.value, actual.value); + } + + public void testBadValueForBigDecimalDeserialization() { + try { + gson.fromJson("{\"value\"=1.5e-1.0031}", ClassWithBigDecimal.class); + fail("Exponent of a BigDecimal must be an integer value."); + } catch (JsonParseException expected) { } + } + + public void testBigIntegerFieldSerialization() { + ClassWithBigInteger target = new ClassWithBigInteger("23232323215323234234324324324324324324"); + String json = gson.toJson(target); + assertEquals(target.getExpectedJson(), json); + } + + public void testBigIntegerFieldDeserialization() { + ClassWithBigInteger expected = new ClassWithBigInteger("879697697697697697697697697697697697"); + String json = expected.getExpectedJson(); + ClassWithBigInteger actual = gson.fromJson(json, ClassWithBigInteger.class); + assertEquals(expected.value, actual.value); + } + + public void testOverrideBigIntegerTypeAdapter() throws Exception { + gson = new GsonBuilder() + .registerTypeAdapter(BigInteger.class, new NumberAsStringAdapter(BigInteger.class)) + .create(); + assertEquals("\"123\"", gson.toJson(new BigInteger("123"), BigInteger.class)); + assertEquals(new BigInteger("123"), gson.fromJson("\"123\"", BigInteger.class)); + } + + public void testOverrideBigDecimalTypeAdapter() throws Exception { + gson = new GsonBuilder() + .registerTypeAdapter(BigDecimal.class, new NumberAsStringAdapter(BigDecimal.class)) + .create(); + assertEquals("\"1.1\"", gson.toJson(new BigDecimal("1.1"), BigDecimal.class)); + assertEquals(new BigDecimal("1.1"), gson.fromJson("\"1.1\"", BigDecimal.class)); + } + + public void testSetSerialization() throws Exception { + Gson gson = new Gson(); + HashSet<String> s = new HashSet<String>(); + s.add("blah"); + String json = gson.toJson(s); + assertEquals("[\"blah\"]", json); + + json = gson.toJson(s, Set.class); + assertEquals("[\"blah\"]", json); + } + + public void testBitSetSerialization() throws Exception { + Gson gson = new Gson(); + BitSet bits = new BitSet(); + bits.set(1); + bits.set(3, 6); + bits.set(9); + String json = gson.toJson(bits); + assertEquals("[0,1,0,1,1,1,0,0,0,1]", json); + } + + public void testBitSetDeserialization() throws Exception { + BitSet expected = new BitSet(); + expected.set(0); + expected.set(2, 6); + expected.set(8); + + Gson gson = new Gson(); + String json = gson.toJson(expected); + assertEquals(expected, gson.fromJson(json, BitSet.class)); + + json = "[1,0,1,1,1,1,0,0,1,0,0,0]"; + assertEquals(expected, gson.fromJson(json, BitSet.class)); + + json = "[\"1\",\"0\",\"1\",\"1\",\"1\",\"1\",\"0\",\"0\",\"1\"]"; + assertEquals(expected, gson.fromJson(json, BitSet.class)); + + json = "[true,false,true,true,true,true,false,false,true,false,false]"; + assertEquals(expected, gson.fromJson(json, BitSet.class)); + } + + public void testDefaultDateSerialization() { + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + } + + public void testDefaultDateDeserialization() { + String json = "'Dec 13, 2009 07:18:02 AM'"; + Date extracted = gson.fromJson(json, Date.class); + assertEqualsDate(extracted, 2009, 11, 13); + assertEqualsTime(extracted, 7, 18, 2); + } + + // Date can not directly be compared with another instance since the deserialization loses the + // millisecond portion. + @SuppressWarnings("deprecation") + private void assertEqualsDate(Date date, int year, int month, int day) { + assertEquals(year-1900, date.getYear()); + assertEquals(month, date.getMonth()); + assertEquals(day, date.getDate()); + } + + @SuppressWarnings("deprecation") + private void assertEqualsTime(Date date, int hours, int minutes, int seconds) { + assertEquals(hours, date.getHours()); + assertEquals(minutes, date.getMinutes()); + assertEquals(seconds, date.getSeconds()); + } + + public void testDefaultJavaSqlDateSerialization() { + java.sql.Date instant = new java.sql.Date(1259875082000L); + String json = gson.toJson(instant); + assertEquals("\"Dec 3, 2009\"", json); + } + + public void testDefaultJavaSqlDateDeserialization() { + String json = "'Dec 3, 2009'"; + java.sql.Date extracted = gson.fromJson(json, java.sql.Date.class); + assertEqualsDate(extracted, 2009, 11, 3); + } + + public void testDefaultJavaSqlTimestampSerialization() { + Timestamp now = new java.sql.Timestamp(1259875082000L); + String json = gson.toJson(now); + assertEquals("\"Dec 3, 2009 1:18:02 PM\"", json); + } + + public void testDefaultJavaSqlTimestampDeserialization() { + String json = "'Dec 3, 2009 1:18:02 PM'"; + Timestamp extracted = gson.fromJson(json, Timestamp.class); + assertEqualsDate(extracted, 2009, 11, 3); + assertEqualsTime(extracted, 13, 18, 2); + } + + public void testDefaultJavaSqlTimeSerialization() { + Time now = new Time(1259875082000L); + String json = gson.toJson(now); + assertEquals("\"01:18:02 PM\"", json); + } + + public void testDefaultJavaSqlTimeDeserialization() { + String json = "'1:18:02 PM'"; + Time extracted = gson.fromJson(json, Time.class); + assertEqualsTime(extracted, 13, 18, 2); + } + + public void testDefaultDateSerializationUsingBuilder() throws Exception { + Gson gson = new GsonBuilder().create(); + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + } + + public void testDefaultDateDeserializationUsingBuilder() throws Exception { + Gson gson = new GsonBuilder().create(); + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + Date extracted = gson.fromJson(json, Date.class); + assertEquals(now.toString(), extracted.toString()); + } + + public void testDefaultCalendarSerialization() throws Exception { + Gson gson = new GsonBuilder().create(); + String json = gson.toJson(Calendar.getInstance()); + assertTrue(json.contains("year")); + assertTrue(json.contains("month")); + assertTrue(json.contains("dayOfMonth")); + assertTrue(json.contains("hourOfDay")); + assertTrue(json.contains("minute")); + assertTrue(json.contains("second")); + } + + public void testDefaultCalendarDeserialization() throws Exception { + Gson gson = new GsonBuilder().create(); + String json = "{year:2009,month:2,dayOfMonth:11,hourOfDay:14,minute:29,second:23}"; + Calendar cal = gson.fromJson(json, Calendar.class); + assertEquals(2009, cal.get(Calendar.YEAR)); + assertEquals(2, cal.get(Calendar.MONTH)); + assertEquals(11, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(14, cal.get(Calendar.HOUR_OF_DAY)); + assertEquals(29, cal.get(Calendar.MINUTE)); + assertEquals(23, cal.get(Calendar.SECOND)); + } + + public void testDefaultGregorianCalendarSerialization() throws Exception { + Gson gson = new GsonBuilder().create(); + GregorianCalendar cal = new GregorianCalendar(); + String json = gson.toJson(cal); + assertTrue(json.contains("year")); + assertTrue(json.contains("month")); + assertTrue(json.contains("dayOfMonth")); + assertTrue(json.contains("hourOfDay")); + assertTrue(json.contains("minute")); + assertTrue(json.contains("second")); + } + + public void testDefaultGregorianCalendarDeserialization() throws Exception { + Gson gson = new GsonBuilder().create(); + String json = "{year:2009,month:2,dayOfMonth:11,hourOfDay:14,minute:29,second:23}"; + GregorianCalendar cal = gson.fromJson(json, GregorianCalendar.class); + assertEquals(2009, cal.get(Calendar.YEAR)); + assertEquals(2, cal.get(Calendar.MONTH)); + assertEquals(11, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(14, cal.get(Calendar.HOUR_OF_DAY)); + assertEquals(29, cal.get(Calendar.MINUTE)); + assertEquals(23, cal.get(Calendar.SECOND)); + } + + public void testDateSerializationWithPattern() throws Exception { + String pattern = "yyyy-MM-dd"; + Gson gson = new GsonBuilder().setDateFormat(DateFormat.FULL).setDateFormat(pattern).create(); + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + assertEquals("\"2011-09-11\"", json); + } + + @SuppressWarnings("deprecation") + public void testDateDeserializationWithPattern() throws Exception { + String pattern = "yyyy-MM-dd"; + Gson gson = new GsonBuilder().setDateFormat(DateFormat.FULL).setDateFormat(pattern).create(); + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + Date extracted = gson.fromJson(json, Date.class); + assertEquals(now.getYear(), extracted.getYear()); + assertEquals(now.getMonth(), extracted.getMonth()); + assertEquals(now.getDay(), extracted.getDay()); + } + + public void testDateSerializationWithPatternNotOverridenByTypeAdapter() throws Exception { + String pattern = "yyyy-MM-dd"; + Gson gson = new GsonBuilder() + .setDateFormat(pattern) + .registerTypeAdapter(Date.class, new JsonDeserializer<Date>() { + public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return new Date(1315806903103L); + } + }) + .create(); + + Date now = new Date(1315806903103L); + String json = gson.toJson(now); + assertEquals("\"2011-09-11\"", json); + } + + // http://code.google.com/p/google-gson/issues/detail?id=230 + public void testDateSerializationInCollection() throws Exception { + Type listOfDates = new TypeToken<List<Date>>() {}.getType(); + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); + List<Date> dates = Arrays.asList(new Date(0)); + String json = gson.toJson(dates, listOfDates); + assertEquals("[\"1970-01-01\"]", json); + assertEquals(0L, gson.<List<Date>>fromJson("[\"1970-01-01\"]", listOfDates).get(0).getTime()); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + // http://code.google.com/p/google-gson/issues/detail?id=230 + public void testTimestampSerialization() throws Exception { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + Timestamp timestamp = new Timestamp(0L); + Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); + String json = gson.toJson(timestamp, Timestamp.class); + assertEquals("\"1970-01-01\"", json); + assertEquals(0, gson.fromJson("\"1970-01-01\"", Timestamp.class).getTime()); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + // http://code.google.com/p/google-gson/issues/detail?id=230 + public void testSqlDateSerialization() throws Exception { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + java.sql.Date sqlDate = new java.sql.Date(0L); + Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); + String json = gson.toJson(sqlDate, Timestamp.class); + assertEquals("\"1970-01-01\"", json); + assertEquals(0, gson.fromJson("\"1970-01-01\"", java.sql.Date.class).getTime()); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testJsonPrimitiveSerialization() { + assertEquals("5", gson.toJson(new JsonPrimitive(5), JsonElement.class)); + assertEquals("true", gson.toJson(new JsonPrimitive(true), JsonElement.class)); + assertEquals("\"foo\"", gson.toJson(new JsonPrimitive("foo"), JsonElement.class)); + assertEquals("\"a\"", gson.toJson(new JsonPrimitive('a'), JsonElement.class)); + } + + public void testJsonPrimitiveDeserialization() { + assertEquals(new JsonPrimitive(5), gson.fromJson("5", JsonElement.class)); + assertEquals(new JsonPrimitive(5), gson.fromJson("5", JsonPrimitive.class)); + assertEquals(new JsonPrimitive(true), gson.fromJson("true", JsonElement.class)); + assertEquals(new JsonPrimitive(true), gson.fromJson("true", JsonPrimitive.class)); + assertEquals(new JsonPrimitive("foo"), gson.fromJson("\"foo\"", JsonElement.class)); + assertEquals(new JsonPrimitive("foo"), gson.fromJson("\"foo\"", JsonPrimitive.class)); + assertEquals(new JsonPrimitive('a'), gson.fromJson("\"a\"", JsonElement.class)); + assertEquals(new JsonPrimitive('a'), gson.fromJson("\"a\"", JsonPrimitive.class)); + } + + public void testJsonNullSerialization() { + assertEquals("null", gson.toJson(JsonNull.INSTANCE, JsonElement.class)); + assertEquals("null", gson.toJson(JsonNull.INSTANCE, JsonNull.class)); + } + + public void testNullJsonElementSerialization() { + assertEquals("null", gson.toJson(null, JsonElement.class)); + assertEquals("null", gson.toJson(null, JsonNull.class)); + } + + public void testJsonArraySerialization() { + JsonArray array = new JsonArray(); + array.add(new JsonPrimitive(1)); + array.add(new JsonPrimitive(2)); + array.add(new JsonPrimitive(3)); + assertEquals("[1,2,3]", gson.toJson(array, JsonElement.class)); + } + + public void testJsonArrayDeserialization() { + JsonArray array = new JsonArray(); + array.add(new JsonPrimitive(1)); + array.add(new JsonPrimitive(2)); + array.add(new JsonPrimitive(3)); + + String json = "[1,2,3]"; + assertEquals(array, gson.fromJson(json, JsonElement.class)); + assertEquals(array, gson.fromJson(json, JsonArray.class)); + } + + public void testJsonObjectSerialization() { + JsonObject object = new JsonObject(); + object.add("foo", new JsonPrimitive(1)); + object.add("bar", new JsonPrimitive(2)); + assertEquals("{\"foo\":1,\"bar\":2}", gson.toJson(object, JsonElement.class)); + } + + public void testJsonObjectDeserialization() { + JsonObject object = new JsonObject(); + object.add("foo", new JsonPrimitive(1)); + object.add("bar", new JsonPrimitive(2)); + + String json = "{\"foo\":1,\"bar\":2}"; + JsonElement actual = gson.fromJson(json, JsonElement.class); + assertEquals(object, actual); + + JsonObject actualObj = gson.fromJson(json, JsonObject.class); + assertEquals(object, actualObj); + } + + public void testJsonNullDeserialization() { + assertEquals(JsonNull.INSTANCE, gson.fromJson("null", JsonElement.class)); + assertEquals(JsonNull.INSTANCE, gson.fromJson("null", JsonNull.class)); + } + + private static class ClassWithBigDecimal { + BigDecimal value; + ClassWithBigDecimal(String value) { + this.value = new BigDecimal(value); + } + String getExpectedJson() { + return "{\"value\":" + value.toEngineeringString() + "}"; + } + } + + private static class ClassWithBigInteger { + BigInteger value; + ClassWithBigInteger(String value) { + this.value = new BigInteger(value); + } + String getExpectedJson() { + return "{\"value\":" + value + "}"; + } + } + + public void testPropertiesSerialization() { + Properties props = new Properties(); + props.setProperty("foo", "bar"); + String json = gson.toJson(props); + String expected = "{\"foo\":\"bar\"}"; + assertEquals(expected, json); + } + + public void testPropertiesDeserialization() { + String json = "{foo:'bar'}"; + Properties props = gson.fromJson(json, Properties.class); + assertEquals("bar", props.getProperty("foo")); + } + + public void testTreeSetSerialization() { + TreeSet<String> treeSet = new TreeSet<String>(); + treeSet.add("Value1"); + String json = gson.toJson(treeSet); + assertEquals("[\"Value1\"]", json); + } + + public void testTreeSetDeserialization() { + String json = "['Value1']"; + Type type = new TypeToken<TreeSet<String>>() {}.getType(); + TreeSet<String> treeSet = gson.fromJson(json, type); + assertTrue(treeSet.contains("Value1")); + } + + public void testStringBuilderSerialization() { + StringBuilder sb = new StringBuilder("abc"); + String json = gson.toJson(sb); + assertEquals("\"abc\"", json); + } + + public void testStringBuilderDeserialization() { + StringBuilder sb = gson.fromJson("'abc'", StringBuilder.class); + assertEquals("abc", sb.toString()); + } + + public void testStringBufferSerialization() { + StringBuffer sb = new StringBuffer("abc"); + String json = gson.toJson(sb); + assertEquals("\"abc\"", json); + } + + public void testStringBufferDeserialization() { + StringBuffer sb = gson.fromJson("'abc'", StringBuffer.class); + assertEquals("abc", sb.toString()); + } + + @SuppressWarnings("rawtypes") + private static class MyClassTypeAdapter extends TypeAdapter<Class> { + @Override + public void write(JsonWriter out, Class value) throws IOException { + out.value(value.getName()); + } + @Override + public Class read(JsonReader in) throws IOException { + String className = in.nextString(); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IOException(e); + } + } + } + + static class NumberAsStringAdapter extends TypeAdapter<Number> { + private final Constructor<? extends Number> constructor; + NumberAsStringAdapter(Class<? extends Number> type) throws Exception { + this.constructor = type.getConstructor(String.class); + } + @Override public void write(JsonWriter out, Number value) throws IOException { + out.value(value.toString()); + } + @Override public Number read(JsonReader in) throws IOException { + try { + return constructor.newInstance(in.nextString()); + } catch (Exception e) { + throw new AssertionError(e); + } + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java new file mode 100644 index 00000000..885330d8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java @@ -0,0 +1,92 @@ +/* + * 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.functional; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Functional tests for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)} method. + * + * @author Inderjeet Singh + */ +public class DelegateTypeAdapterTest extends TestCase { + + private StatsTypeAdapterFactory stats; + private Gson gson; + protected void setUp() throws Exception { + super.setUp(); + stats = new StatsTypeAdapterFactory(); + gson = new GsonBuilder() + .registerTypeAdapterFactory(stats) + .create(); + } + + public void testDelegateInvoked() { + List<BagOfPrimitives> bags = new ArrayList<BagOfPrimitives>(); + for (int i = 0; i < 10; ++i) { + bags.add(new BagOfPrimitives(i, i, i % 2 == 0, String.valueOf(i))); + } + String json = gson.toJson(bags); + bags = gson.fromJson(json, new TypeToken<List<BagOfPrimitives>>(){}.getType()); + // 11: 1 list object, and 10 entries. stats invoked on all 5 fields + assertEquals(51, stats.numReads); + assertEquals(51, stats.numWrites); + } + + public void testDelegateInvokedOnStrings() { + String[] bags = {"1", "2", "3", "4"}; + String json = gson.toJson(bags); + bags = gson.fromJson(json, String[].class); + // 1 array object with 4 elements. + assertEquals(5, stats.numReads); + assertEquals(5, stats.numWrites); + } + + private static 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>() { + @Override + public void write(JsonWriter out, T value) throws IOException { + ++numWrites; + delegate.write(out, value); + } + + @Override + public T read(JsonReader in) throws IOException { + ++numReads; + return delegate.read(in); + } + }; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/EnumTest.java b/gson/src/test/java/com/google/gson/functional/EnumTest.java new file mode 100644 index 00000000..2c21526d --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/EnumTest.java @@ -0,0 +1,202 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; +import com.google.gson.common.MoreAsserts; +import com.google.gson.reflect.TypeToken; + + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; + +import junit.framework.TestCase; +/** + * Functional tests for Java 5.0 enums. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class EnumTest extends TestCase { + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testTopLevelEnumSerialization() throws Exception { + String result = gson.toJson(MyEnum.VALUE1); + assertEquals('"' + MyEnum.VALUE1.toString() + '"', result); + } + + public void testTopLevelEnumDeserialization() throws Exception { + MyEnum result = gson.fromJson('"' + MyEnum.VALUE1.toString() + '"', MyEnum.class); + assertEquals(MyEnum.VALUE1, result); + } + + public void testCollectionOfEnumsSerialization() { + Type type = new TypeToken<Collection<MyEnum>>() {}.getType(); + Collection<MyEnum> target = new ArrayList<MyEnum>(); + target.add(MyEnum.VALUE1); + target.add(MyEnum.VALUE2); + String expectedJson = "[\"VALUE1\",\"VALUE2\"]"; + String actualJson = gson.toJson(target); + assertEquals(expectedJson, actualJson); + actualJson = gson.toJson(target, type); + assertEquals(expectedJson, actualJson); + } + + public void testCollectionOfEnumsDeserialization() { + Type type = new TypeToken<Collection<MyEnum>>() {}.getType(); + String json = "[\"VALUE1\",\"VALUE2\"]"; + Collection<MyEnum> target = gson.fromJson(json, type); + MoreAsserts.assertContains(target, MyEnum.VALUE1); + MoreAsserts.assertContains(target, MyEnum.VALUE2); + } + + public void testClassWithEnumFieldSerialization() throws Exception { + ClassWithEnumFields target = new ClassWithEnumFields(); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testClassWithEnumFieldDeserialization() throws Exception { + String json = "{value1:'VALUE1',value2:'VALUE2'}"; + ClassWithEnumFields target = gson.fromJson(json, ClassWithEnumFields.class); + assertEquals(MyEnum.VALUE1,target.value1); + assertEquals(MyEnum.VALUE2,target.value2); + } + + private static enum MyEnum { + VALUE1, VALUE2 + } + + private static class ClassWithEnumFields { + private final MyEnum value1 = MyEnum.VALUE1; + private final MyEnum value2 = MyEnum.VALUE2; + public String getExpectedJson() { + return "{\"value1\":\"" + value1 + "\",\"value2\":\"" + value2 + "\"}"; + } + } + + /** + * Test for issue 226. + */ + public void testEnumSubclass() { + assertFalse(Roshambo.class == Roshambo.ROCK.getClass()); + assertEquals("\"ROCK\"", gson.toJson(Roshambo.ROCK)); + assertEquals("[\"ROCK\",\"PAPER\",\"SCISSORS\"]", gson.toJson(EnumSet.allOf(Roshambo.class))); + assertEquals(Roshambo.ROCK, gson.fromJson("\"ROCK\"", Roshambo.class)); + assertEquals(EnumSet.allOf(Roshambo.class), + gson.fromJson("[\"ROCK\",\"PAPER\",\"SCISSORS\"]", new TypeToken<Set<Roshambo>>() {}.getType())); + } + + public void testEnumSubclassWithRegisteredTypeAdapter() { + gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Roshambo.class, new MyEnumTypeAdapter()) + .create(); + assertFalse(Roshambo.class == Roshambo.ROCK.getClass()); + assertEquals("\"123ROCK\"", gson.toJson(Roshambo.ROCK)); + assertEquals("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", gson.toJson(EnumSet.allOf(Roshambo.class))); + assertEquals(Roshambo.ROCK, gson.fromJson("\"123ROCK\"", Roshambo.class)); + assertEquals(EnumSet.allOf(Roshambo.class), + gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken<Set<Roshambo>>() {}.getType())); + } + + public void testEnumSubclassAsParameterizedType() { + Collection<Roshambo> list = new ArrayList<Roshambo>(); + list.add(Roshambo.ROCK); + list.add(Roshambo.PAPER); + + String json = gson.toJson(list); + assertEquals("[\"ROCK\",\"PAPER\"]", json); + + Type collectionType = new TypeToken<Collection<Roshambo>>() {}.getType(); + Collection<Roshambo> actualJsonList = gson.fromJson(json, collectionType); + MoreAsserts.assertContains(actualJsonList, Roshambo.ROCK); + MoreAsserts.assertContains(actualJsonList, Roshambo.PAPER); + } + + public void testEnumCaseMapping() { + assertEquals(Gender.MALE, gson.fromJson("\"boy\"", Gender.class)); + assertEquals("\"boy\"", gson.toJson(Gender.MALE, Gender.class)); + } + + public void testEnumSet() { + EnumSet<Roshambo> foo = EnumSet.of(Roshambo.ROCK, Roshambo.PAPER); + String json = gson.toJson(foo); + Type type = new TypeToken<EnumSet<Roshambo>>() {}.getType(); + EnumSet<Roshambo> bar = gson.fromJson(json, type); + assertTrue(bar.contains(Roshambo.ROCK)); + assertTrue(bar.contains(Roshambo.PAPER)); + assertFalse(bar.contains(Roshambo.SCISSORS)); + } + + public enum Roshambo { + ROCK { + @Override Roshambo defeats() { + return SCISSORS; + } + }, + PAPER { + @Override Roshambo defeats() { + return ROCK; + } + }, + SCISSORS { + @Override Roshambo defeats() { + return PAPER; + } + }; + + abstract Roshambo defeats(); + } + + private static class MyEnumTypeAdapter + implements JsonSerializer<Roshambo>, JsonDeserializer<Roshambo> { + public JsonElement serialize(Roshambo src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("123" + src.name()); + } + + public Roshambo deserialize(JsonElement json, Type classOfT, JsonDeserializationContext context) + throws JsonParseException { + return Roshambo.valueOf(json.getAsString().substring(3)); + } + } + + public enum Gender { + @SerializedName("boy") + MALE, + + @SerializedName("girl") + FEMALE + } +} diff --git a/gson/src/test/java/com/google/gson/functional/EscapingTest.java b/gson/src/test/java/com/google/gson/functional/EscapingTest.java new file mode 100644 index 00000000..1581f451 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/EscapingTest.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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import java.util.ArrayList; +import java.util.List; +import junit.framework.TestCase; + +/** + * Performs some functional test involving JSON output escaping. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class EscapingTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testEscapingQuotesInStringArray() throws Exception { + String[] valueWithQuotes = { "beforeQuote\"afterQuote" }; + String jsonRepresentation = gson.toJson(valueWithQuotes); + String[] target = gson.fromJson(jsonRepresentation, String[].class); + assertEquals(1, target.length); + assertEquals(valueWithQuotes[0], target[0]); + } + + public void testEscapeAllHtmlCharacters() { + List<String> strings = new ArrayList<String>(); + strings.add("<"); + strings.add(">"); + strings.add("="); + strings.add("&"); + strings.add("'"); + strings.add("\""); + assertEquals("[\"\\u003c\",\"\\u003e\",\"\\u003d\",\"\\u0026\",\"\\u0027\",\"\\\"\"]", + gson.toJson(strings)); + } + + public void testEscapingObjectFields() throws Exception { + BagOfPrimitives objWithPrimitives = new BagOfPrimitives(1L, 1, true, "test with\" <script>"); + String jsonRepresentation = gson.toJson(objWithPrimitives); + assertFalse(jsonRepresentation.contains("<")); + assertFalse(jsonRepresentation.contains(">")); + assertTrue(jsonRepresentation.contains("\\\"")); + + BagOfPrimitives expectedObject = gson.fromJson(jsonRepresentation, BagOfPrimitives.class); + assertEquals(objWithPrimitives.getExpectedJson(), expectedObject.getExpectedJson()); + } + + public void testGsonAcceptsEscapedAndNonEscapedJsonDeserialization() throws Exception { + Gson escapeHtmlGson = new GsonBuilder().create(); + Gson noEscapeHtmlGson = new GsonBuilder().disableHtmlEscaping().create(); + + BagOfPrimitives target = new BagOfPrimitives(1L, 1, true, "test' / w'ith\" / \\ <script>"); + String escapedJsonForm = escapeHtmlGson.toJson(target); + String nonEscapedJsonForm = noEscapeHtmlGson.toJson(target); + assertFalse(escapedJsonForm.equals(nonEscapedJsonForm)); + + assertEquals(target, noEscapeHtmlGson.fromJson(escapedJsonForm, BagOfPrimitives.class)); + assertEquals(target, escapeHtmlGson.fromJson(nonEscapedJsonForm, BagOfPrimitives.class)); + } + + public void testGsonDoubleDeserialization() { + BagOfPrimitives expected = new BagOfPrimitives(3L, 4, true, "value1"); + String json = gson.toJson(gson.toJson(expected)); + String value = gson.fromJson(json, String.class); + BagOfPrimitives actual = gson.fromJson(value, BagOfPrimitives.class); + assertEquals(expected, actual); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java new file mode 100644 index 00000000..baeab840 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java @@ -0,0 +1,202 @@ +/* + * 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.functional; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import junit.framework.TestCase; + +/** + * Performs some functional tests when Gson is instantiated with some common user defined + * {@link ExclusionStrategy} objects. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ExclusionStrategyFunctionalTest extends TestCase { + private static final ExclusionStrategy EXCLUDE_SAMPLE_OBJECT_FOR_TEST = new ExclusionStrategy() { + public boolean shouldSkipField(FieldAttributes f) { + return false; + } + public boolean shouldSkipClass(Class<?> clazz) { + return clazz == SampleObjectForTest.class; + } + }; + + private SampleObjectForTest src; + + @Override + protected void setUp() throws Exception { + super.setUp(); + src = new SampleObjectForTest(); + } + + public void testExclusionStrategySerialization() throws Exception { + Gson gson = createGson(new MyExclusionStrategy(String.class), true); + String json = gson.toJson(src); + assertFalse(json.contains("\"stringField\"")); + assertFalse(json.contains("\"annotatedField\"")); + assertTrue(json.contains("\"longField\"")); + } + + public void testExclusionStrategySerializationDoesNotImpactDeserialization() { + String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}"; + Gson gson = createGson(new MyExclusionStrategy(String.class), true); + SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class); + assertEquals(1, value.annotatedField); + assertEquals("x", value.stringField); + assertEquals(2, value.longField); + } + + public void testExclusionStrategyDeserialization() throws Exception { + Gson gson = createGson(new MyExclusionStrategy(String.class), false); + JsonObject json = new JsonObject(); + json.add("annotatedField", new JsonPrimitive(src.annotatedField + 5)); + json.add("stringField", new JsonPrimitive(src.stringField + "blah,blah")); + json.add("longField", new JsonPrimitive(1212311L)); + + SampleObjectForTest target = gson.fromJson(json, SampleObjectForTest.class); + assertEquals(1212311L, target.longField); + + // assert excluded fields are set to the defaults + assertEquals(src.annotatedField, target.annotatedField); + assertEquals(src.stringField, target.stringField); + } + + public void testExclusionStrategySerializationDoesNotImpactSerialization() throws Exception { + Gson gson = createGson(new MyExclusionStrategy(String.class), false); + String json = gson.toJson(src); + assertTrue(json.contains("\"stringField\"")); + assertTrue(json.contains("\"annotatedField\"")); + assertTrue(json.contains("\"longField\"")); + } + + public void testExclusionStrategyWithMode() throws Exception { + SampleObjectForTest testObj = new SampleObjectForTest( + src.annotatedField + 5, src.stringField + "blah,blah", + src.longField + 655L); + + Gson gson = createGson(new MyExclusionStrategy(String.class), false); + JsonObject json = gson.toJsonTree(testObj).getAsJsonObject(); + assertEquals(testObj.annotatedField, json.get("annotatedField").getAsInt()); + assertEquals(testObj.stringField, json.get("stringField").getAsString()); + assertEquals(testObj.longField, json.get("longField").getAsLong()); + + SampleObjectForTest target = gson.fromJson(json, SampleObjectForTest.class); + assertEquals(testObj.longField, target.longField); + + // assert excluded fields are set to the defaults + assertEquals(src.annotatedField, target.annotatedField); + assertEquals(src.stringField, target.stringField); + } + + public void testExcludeTopLevelClassSerialization() { + Gson gson = new GsonBuilder() + .addSerializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST) + .create(); + assertEquals("null", gson.toJson(new SampleObjectForTest(), SampleObjectForTest.class)); + } + + public void testExcludeTopLevelClassSerializationDoesNotImpactDeserialization() { + Gson gson = new GsonBuilder() + .addSerializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST) + .create(); + String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}"; + SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class); + assertEquals(1, value.annotatedField); + assertEquals("x", value.stringField); + assertEquals(2, value.longField); + } + + public void testExcludeTopLevelClassDeserialization() { + Gson gson = new GsonBuilder() + .addDeserializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST) + .create(); + String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}"; + SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class); + assertNull(value); + } + + public void testExcludeTopLevelClassDeserializationDoesNotImpactSerialization() { + Gson gson = new GsonBuilder() + .addDeserializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST) + .create(); + String json = gson.toJson(new SampleObjectForTest(), SampleObjectForTest.class); + assertTrue(json.contains("\"stringField\"")); + assertTrue(json.contains("\"annotatedField\"")); + assertTrue(json.contains("\"longField\"")); + } + + private static Gson createGson(ExclusionStrategy exclusionStrategy, boolean serialization) { + GsonBuilder gsonBuilder = new GsonBuilder(); + if (serialization) { + gsonBuilder.addSerializationExclusionStrategy(exclusionStrategy); + } else { + gsonBuilder.addDeserializationExclusionStrategy(exclusionStrategy); + } + return gsonBuilder + .serializeNulls() + .create(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD}) + private static @interface Foo { + // Field tag only annotation + } + + private static class SampleObjectForTest { + @Foo + private final int annotatedField; + private final String stringField; + private final long longField; + + public SampleObjectForTest() { + this(5, "someDefaultValue", 12345L); + } + + public SampleObjectForTest(int annotatedField, String stringField, long longField) { + this.annotatedField = annotatedField; + this.stringField = stringField; + this.longField = longField; + } + } + + private static class MyExclusionStrategy implements ExclusionStrategy { + private final Class<?> typeToSkip; + + private MyExclusionStrategy(Class<?> typeToSkip) { + this.typeToSkip = typeToSkip; + } + + public boolean shouldSkipClass(Class<?> clazz) { + return (clazz == typeToSkip); + } + + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(Foo.class) != null; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java b/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java new file mode 100644 index 00000000..0ec5c433 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.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.functional; + +import java.lang.reflect.Type; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.annotations.Expose; + +import junit.framework.TestCase; + +/** + * Unit tests for the regarding functional "@Expose" type tests. + * + * @author Joel Leitch + */ +public class ExposeFieldsTest extends TestCase { + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(SomeInterface.class, new SomeInterfaceInstanceCreator()) + .create(); + } + + public void testNullExposeFieldSerialization() throws Exception { + ClassWithExposedFields object = new ClassWithExposedFields(null, 1); + String json = gson.toJson(object); + + assertEquals(object.getExpectedJson(), json); + } + + public void testArrayWithOneNullExposeFieldObjectSerialization() throws Exception { + ClassWithExposedFields object1 = new ClassWithExposedFields(1, 1); + ClassWithExposedFields object2 = new ClassWithExposedFields(null, 1); + ClassWithExposedFields object3 = new ClassWithExposedFields(2, 2); + ClassWithExposedFields[] objects = { object1, object2, object3 }; + + String json = gson.toJson(objects); + String expected = new StringBuilder() + .append('[').append(object1.getExpectedJson()).append(',') + .append(object2.getExpectedJson()).append(',') + .append(object3.getExpectedJson()).append(']') + .toString(); + + assertEquals(expected, json); + } + + public void testExposeAnnotationSerialization() throws Exception { + ClassWithExposedFields target = new ClassWithExposedFields(1, 2); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testExposeAnnotationDeserialization() throws Exception { + String json = "{a:3,b:4,d:20.0}"; + ClassWithExposedFields target = gson.fromJson(json, ClassWithExposedFields.class); + + assertEquals(3, (int) target.a); + assertNull(target.b); + assertFalse(target.d == 20); + } + + public void testNoExposedFieldSerialization() throws Exception { + ClassWithNoExposedFields obj = new ClassWithNoExposedFields(); + String json = gson.toJson(obj); + + assertEquals("{}", json); + } + + public void testNoExposedFieldDeserialization() throws Exception { + String json = "{a:4,b:5}"; + ClassWithNoExposedFields obj = gson.fromJson(json, ClassWithNoExposedFields.class); + + assertEquals(0, obj.a); + assertEquals(1, obj.b); + } + + public void testExposedInterfaceFieldSerialization() throws Exception { + String expected = "{\"interfaceField\":{}}"; + ClassWithInterfaceField target = new ClassWithInterfaceField(new SomeObject()); + String actual = gson.toJson(target); + + assertEquals(expected, actual); + } + + public void testExposedInterfaceFieldDeserialization() throws Exception { + String json = "{\"interfaceField\":{}}"; + ClassWithInterfaceField obj = gson.fromJson(json, ClassWithInterfaceField.class); + + assertNotNull(obj.interfaceField); + } + + private static class ClassWithExposedFields { + @Expose private final Integer a; + private final Integer b; + @Expose(serialize = false) final long c; + @Expose(deserialize = false) final double d; + @Expose(serialize = false, deserialize = false) final char e; + + public ClassWithExposedFields(Integer a, Integer b) { + this(a, b, 1L, 2.0, 'a'); + } + public ClassWithExposedFields(Integer a, Integer b, long c, double d, char e) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder("{"); + if (a != null) { + sb.append("\"a\":").append(a).append(","); + } + sb.append("\"d\":").append(d); + sb.append("}"); + return sb.toString(); + } + } + + private static class ClassWithNoExposedFields { + private final int a = 0; + private final int b = 1; + } + + private static interface SomeInterface { + // Empty interface + } + + private static class SomeObject implements SomeInterface { + // Do nothing + } + + private static class SomeInterfaceInstanceCreator implements InstanceCreator<SomeInterface> { + public SomeInterface createInstance(Type type) { + return new SomeObject(); + } + } + + private static class ClassWithInterfaceField { + @Expose + private final SomeInterface interfaceField; + + public ClassWithInterfaceField(SomeInterface interfaceField) { + this.interfaceField = interfaceField; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java b/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java new file mode 100644 index 00000000..080a8234 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import junit.framework.TestCase; + +/** + * Performs some functional testing to ensure GSON infrastructure properly serializes/deserializes + * fields that either should or should not be included in the output based on the GSON + * configuration. + * + * @author Joel Leitch + */ +public class FieldExclusionTest extends TestCase { + private static final String VALUE = "blah_1234"; + + private Outer outer; + + @Override + protected void setUp() throws Exception { + super.setUp(); + outer = new Outer(); + } + + public void testDefaultInnerClassExclusion() throws Exception { + Gson gson = new Gson(); + Outer.Inner target = outer.new Inner(VALUE); + String result = gson.toJson(target); + assertEquals(target.toJson(), result); + + gson = new GsonBuilder().create(); + target = outer.new Inner(VALUE); + result = gson.toJson(target); + assertEquals(target.toJson(), result); + } + + public void testInnerClassExclusion() throws Exception { + Gson gson = new GsonBuilder().disableInnerClassSerialization().create(); + Outer.Inner target = outer.new Inner(VALUE); + String result = gson.toJson(target); + assertEquals("null", result); + } + + public void testDefaultNestedStaticClassIncluded() throws Exception { + Gson gson = new Gson(); + Outer.Inner target = outer.new Inner(VALUE); + String result = gson.toJson(target); + assertEquals(target.toJson(), result); + + gson = new GsonBuilder().create(); + target = outer.new Inner(VALUE); + result = gson.toJson(target); + assertEquals(target.toJson(), result); + } + + private static class Outer { + private class Inner extends NestedClass { + public Inner(String value) { + super(value); + } + } + + } + + private static class NestedClass { + private final String value; + public NestedClass(String value) { + this.value = value; + } + + public String toJson() { + return "{\"value\":\"" + value + "\"}"; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java b/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java new file mode 100644 index 00000000..5d326af8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java @@ -0,0 +1,89 @@ +/* + * 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.functional; + +import static com.google.gson.FieldNamingPolicy.IDENTITY; +import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_DASHES; +import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; +import static com.google.gson.FieldNamingPolicy.UPPER_CAMEL_CASE; +import static com.google.gson.FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import junit.framework.TestCase; + +public final class FieldNamingTest extends TestCase { + public void testIdentity() { + Gson gson = getGsonWithNamingPolicy(IDENTITY); + assertEquals("{'lowerCamel':1,'UpperCamel':2,'_lowerCamelLeadingUnderscore':3," + + "'_UpperCamelLeadingUnderscore':4,'lower_words':5,'UPPER_WORDS':6," + + "'annotatedName':7,'lowerId':8}", + gson.toJson(new TestNames()).replace('\"', '\'')); + } + + public void testUpperCamelCase() { + Gson gson = getGsonWithNamingPolicy(UPPER_CAMEL_CASE); + assertEquals("{'LowerCamel':1,'UpperCamel':2,'_LowerCamelLeadingUnderscore':3," + + "'_UpperCamelLeadingUnderscore':4,'Lower_words':5,'UPPER_WORDS':6," + + "'annotatedName':7,'LowerId':8}", + gson.toJson(new TestNames()).replace('\"', '\'')); + } + + public void testUpperCamelCaseWithSpaces() { + Gson gson = getGsonWithNamingPolicy(UPPER_CAMEL_CASE_WITH_SPACES); + assertEquals("{'Lower Camel':1,'Upper Camel':2,'_Lower Camel Leading Underscore':3," + + "'_ Upper Camel Leading Underscore':4,'Lower_words':5,'U P P E R_ W O R D S':6," + + "'annotatedName':7,'Lower Id':8}", + gson.toJson(new TestNames()).replace('\"', '\'')); + } + + public void testLowerCaseWithUnderscores() { + Gson gson = getGsonWithNamingPolicy(LOWER_CASE_WITH_UNDERSCORES); + assertEquals("{'lower_camel':1,'upper_camel':2,'_lower_camel_leading_underscore':3," + + "'__upper_camel_leading_underscore':4,'lower_words':5,'u_p_p_e_r__w_o_r_d_s':6," + + "'annotatedName':7,'lower_id':8}", + gson.toJson(new TestNames()).replace('\"', '\'')); + } + + public void testLowerCaseWithDashes() { + Gson gson = getGsonWithNamingPolicy(LOWER_CASE_WITH_DASHES); + assertEquals("{'lower-camel':1,'upper-camel':2,'_lower-camel-leading-underscore':3," + + "'_-upper-camel-leading-underscore':4,'lower_words':5,'u-p-p-e-r_-w-o-r-d-s':6," + + "'annotatedName':7,'lower-id':8}", + gson.toJson(new TestNames()).replace('\"', '\'')); + } + + private Gson getGsonWithNamingPolicy(FieldNamingPolicy fieldNamingPolicy){ + return new GsonBuilder() + .setFieldNamingPolicy(fieldNamingPolicy) + .create(); + } + + @SuppressWarnings("unused") // fields are used reflectively + private static class TestNames { + int lowerCamel = 1; + int UpperCamel = 2; + int _lowerCamelLeadingUnderscore = 3; + int _UpperCamelLeadingUnderscore = 4; + int lower_words = 5; + int UPPER_WORDS = 6; + @SerializedName("annotatedName") int annotated = 7; + int lowerId = 8; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/InheritanceTest.java b/gson/src/test/java/com/google/gson/functional/InheritanceTest.java new file mode 100644 index 00000000..b93ba0b5 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InheritanceTest.java @@ -0,0 +1,277 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.Base; +import com.google.gson.common.TestTypes.ClassWithBaseArrayField; +import com.google.gson.common.TestTypes.ClassWithBaseCollectionField; +import com.google.gson.common.TestTypes.ClassWithBaseField; +import com.google.gson.common.TestTypes.Nested; +import com.google.gson.common.TestTypes.Sub; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Functional tests for Json serialization and deserialization of classes with + * inheritance hierarchies. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class InheritanceTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testSubClassSerialization() throws Exception { + SubTypeOfNested target = new SubTypeOfNested(new BagOfPrimitives(10, 20, false, "stringValue"), + new BagOfPrimitives(30, 40, true, "stringValue")); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testSubClassDeserialization() throws Exception { + String json = "{\"value\":5,\"primitive1\":{\"longValue\":10,\"intValue\":20," + + "\"booleanValue\":false,\"stringValue\":\"stringValue\"},\"primitive2\":" + + "{\"longValue\":30,\"intValue\":40,\"booleanValue\":true," + + "\"stringValue\":\"stringValue\"}}"; + SubTypeOfNested target = gson.fromJson(json, SubTypeOfNested.class); + assertEquals(json, target.getExpectedJson()); + } + + public void testClassWithBaseFieldSerialization() { + ClassWithBaseField sub = new ClassWithBaseField(new Sub()); + JsonObject json = (JsonObject) gson.toJsonTree(sub); + JsonElement base = json.getAsJsonObject().get(ClassWithBaseField.FIELD_KEY); + assertEquals(Sub.SUB_NAME, base.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString()); + } + + public void testClassWithBaseArrayFieldSerialization() { + Base[] baseClasses = new Base[]{ new Sub(), new Sub()}; + ClassWithBaseArrayField sub = new ClassWithBaseArrayField(baseClasses); + JsonObject json = gson.toJsonTree(sub).getAsJsonObject(); + JsonArray bases = json.get(ClassWithBaseArrayField.FIELD_KEY).getAsJsonArray(); + for (JsonElement element : bases) { + assertEquals(Sub.SUB_NAME, element.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString()); + } + } + + public void testClassWithBaseCollectionFieldSerialization() { + Collection<Base> baseClasses = new ArrayList<Base>(); + baseClasses.add(new Sub()); + baseClasses.add(new Sub()); + ClassWithBaseCollectionField sub = new ClassWithBaseCollectionField(baseClasses); + JsonObject json = gson.toJsonTree(sub).getAsJsonObject(); + JsonArray bases = json.get(ClassWithBaseArrayField.FIELD_KEY).getAsJsonArray(); + for (JsonElement element : bases) { + assertEquals(Sub.SUB_NAME, element.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString()); + } + } + + public void testBaseSerializedAsSub() { + Base base = new Sub(); + JsonObject json = gson.toJsonTree(base).getAsJsonObject(); + assertEquals(Sub.SUB_NAME, json.get(Sub.SUB_FIELD_KEY).getAsString()); + } + + public void testBaseSerializedAsSubForToJsonMethod() { + Base base = new Sub(); + String json = gson.toJson(base); + assertTrue(json.contains(Sub.SUB_NAME)); + } + + public void testBaseSerializedAsBaseWhenSpecifiedWithExplicitType() { + Base base = new Sub(); + JsonObject json = gson.toJsonTree(base, Base.class).getAsJsonObject(); + assertEquals(Base.BASE_NAME, json.get(Base.BASE_FIELD_KEY).getAsString()); + assertNull(json.get(Sub.SUB_FIELD_KEY)); + } + + public void testBaseSerializedAsBaseWhenSpecifiedWithExplicitTypeForToJsonMethod() { + Base base = new Sub(); + String json = gson.toJson(base, Base.class); + assertTrue(json.contains(Base.BASE_NAME)); + assertFalse(json.contains(Sub.SUB_FIELD_KEY)); + } + + public void testBaseSerializedAsSubWhenSpecifiedWithExplicitType() { + Base base = new Sub(); + JsonObject json = gson.toJsonTree(base, Sub.class).getAsJsonObject(); + assertEquals(Sub.SUB_NAME, json.get(Sub.SUB_FIELD_KEY).getAsString()); + } + + public void testBaseSerializedAsSubWhenSpecifiedWithExplicitTypeForToJsonMethod() { + Base base = new Sub(); + String json = gson.toJson(base, Sub.class); + assertTrue(json.contains(Sub.SUB_NAME)); + } + + private static class SubTypeOfNested extends Nested { + private final long value = 5; + + public SubTypeOfNested(BagOfPrimitives primitive1, BagOfPrimitives primitive2) { + super(primitive1, primitive2); + } + + @Override + public void appendFields(StringBuilder sb) { + sb.append("\"value\":").append(value).append(","); + super.appendFields(sb); + } + } + + public void testSubInterfacesOfCollectionSerialization() throws Exception { + List<Integer> list = new LinkedList<Integer>(); + list.add(0); + list.add(1); + list.add(2); + list.add(3); + Queue<Long> queue = new LinkedList<Long>(); + queue.add(0L); + queue.add(1L); + queue.add(2L); + queue.add(3L); + Set<Float> set = new TreeSet<Float>(); + set.add(0.1F); + set.add(0.2F); + set.add(0.3F); + set.add(0.4F); + SortedSet<Character> sortedSet = new TreeSet<Character>(); + sortedSet.add('a'); + sortedSet.add('b'); + sortedSet.add('c'); + sortedSet.add('d'); + ClassWithSubInterfacesOfCollection target = + new ClassWithSubInterfacesOfCollection(list, queue, set, sortedSet); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testSubInterfacesOfCollectionDeserialization() throws Exception { + String json = "{\"list\":[0,1,2,3],\"queue\":[0,1,2,3],\"set\":[0.1,0.2,0.3,0.4]," + + "\"sortedSet\":[\"a\",\"b\",\"c\",\"d\"]" + + "}"; + ClassWithSubInterfacesOfCollection target = + gson.fromJson(json, ClassWithSubInterfacesOfCollection.class); + assertTrue(target.listContains(0, 1, 2, 3)); + assertTrue(target.queueContains(0, 1, 2, 3)); + assertTrue(target.setContains(0.1F, 0.2F, 0.3F, 0.4F)); + assertTrue(target.sortedSetContains('a', 'b', 'c', 'd')); + } + + private static class ClassWithSubInterfacesOfCollection { + private List<Integer> list; + private Queue<Long> queue; + private Set<Float> set; + private SortedSet<Character> sortedSet; + + public ClassWithSubInterfacesOfCollection(List<Integer> list, Queue<Long> queue, Set<Float> set, + SortedSet<Character> sortedSet) { + this.list = list; + this.queue = queue; + this.set = set; + this.sortedSet = sortedSet; + } + + boolean listContains(int... values) { + for (int value : values) { + if (!list.contains(value)) { + return false; + } + } + return true; + } + + boolean queueContains(long... values) { + for (long value : values) { + if (!queue.contains(value)) { + return false; + } + } + return true; + } + + boolean setContains(float... values) { + for (float value : values) { + if (!set.contains(value)) { + return false; + } + } + return true; + } + + boolean sortedSetContains(char... values) { + for (char value : values) { + if (!sortedSet.contains(value)) { + return false; + } + } + return true; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"list\":"); + append(sb, list).append(","); + sb.append("\"queue\":"); + append(sb, queue).append(","); + sb.append("\"set\":"); + append(sb, set).append(","); + sb.append("\"sortedSet\":"); + append(sb, sortedSet); + sb.append("}"); + return sb.toString(); + } + + private StringBuilder append(StringBuilder sb, Collection<?> c) { + sb.append("["); + boolean first = true; + for (Object o : c) { + if (!first) { + sb.append(","); + } else { + first = false; + } + if (o instanceof String || o instanceof Character) { + sb.append('\"'); + } + sb.append(o.toString()); + if (o instanceof String || o instanceof Character) { + sb.append('\"'); + } + } + sb.append("]"); + return sb; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java new file mode 100644 index 00000000..0fda10af --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java @@ -0,0 +1,127 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.common.TestTypes.Base; +import com.google.gson.common.TestTypes.ClassWithBaseField; +import com.google.gson.common.TestTypes.Sub; + +import com.google.gson.reflect.TypeToken; +import java.util.ArrayList; +import java.util.List; +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Functional Test exercising custom serialization only. When test applies to both + * serialization and deserialization then add it to CustomTypeAdapterTest. + * + * @author Inderjeet Singh + */ +public class InstanceCreatorTest extends TestCase { + + public void testInstanceCreatorReturnsBaseType() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new InstanceCreator<Base>() { + public Base createInstance(Type type) { + return new Base(); + } + }) + .create(); + String json = "{baseName:'BaseRevised',subName:'Sub'}"; + Base base = gson.fromJson(json, Base.class); + assertEquals("BaseRevised", base.baseName); + } + + public void testInstanceCreatorReturnsSubTypeForTopLevelObject() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new InstanceCreator<Base>() { + public Base createInstance(Type type) { + return new Sub(); + } + }) + .create(); + + String json = "{baseName:'Base',subName:'SubRevised'}"; + Base base = gson.fromJson(json, Base.class); + assertTrue(base instanceof Sub); + + Sub sub = (Sub) base; + assertFalse("SubRevised".equals(sub.subName)); + assertEquals(Sub.SUB_NAME, sub.subName); + } + + public void testInstanceCreatorReturnsSubTypeForField() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new InstanceCreator<Base>() { + public Base createInstance(Type type) { + return new Sub(); + } + }) + .create(); + String json = "{base:{baseName:'Base',subName:'SubRevised'}}"; + ClassWithBaseField target = gson.fromJson(json, ClassWithBaseField.class); + assertTrue(target.base instanceof Sub); + assertEquals(Sub.SUB_NAME, ((Sub)target.base).subName); + } + + // This regressed in Gson 2.0 and 2.1 + public void testInstanceCreatorForCollectionType() { + @SuppressWarnings("serial") + class SubArrayList<T> extends ArrayList<T> {} + InstanceCreator<List<String>> listCreator = new InstanceCreator<List<String>>() { + public List<String> createInstance(Type type) { + return new SubArrayList<String>(); + } + }; + Type listOfStringType = new TypeToken<List<String>>() {}.getType(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(listOfStringType, listCreator) + .create(); + List<String> list = gson.fromJson("[\"a\"]", listOfStringType); + assertEquals(SubArrayList.class, list.getClass()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testInstanceCreatorForParametrizedType() throws Exception { + @SuppressWarnings("serial") + class SubTreeSet<T> extends TreeSet<T> {} + InstanceCreator<SortedSet> sortedSetCreator = new InstanceCreator<SortedSet>() { + public SortedSet createInstance(Type type) { + return new SubTreeSet(); + } + }; + Gson gson = new GsonBuilder() + .registerTypeAdapter(SortedSet.class, sortedSetCreator) + .create(); + + Type sortedSetType = new TypeToken<SortedSet<String>>() {}.getType(); + SortedSet<String> set = gson.fromJson("[\"a\"]", sortedSetType); + assertEquals(set.first(), "a"); + assertEquals(SubTreeSet.class, set.getClass()); + + set = gson.fromJson("[\"b\"]", SortedSet.class); + assertEquals(set.first(), "b"); + assertEquals(SubTreeSet.class, set.getClass()); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/InterfaceTest.java b/gson/src/test/java/com/google/gson/functional/InterfaceTest.java new file mode 100644 index 00000000..6851f1e9 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InterfaceTest.java @@ -0,0 +1,72 @@ +/* + * 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.functional; + +import com.google.gson.Gson; + +import junit.framework.TestCase; + +/** + * Functional tests involving interfaces. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class InterfaceTest extends TestCase { + private static final String OBJ_JSON = "{\"someStringValue\":\"StringValue\"}"; + + private Gson gson; + private TestObject obj; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + obj = new TestObject("StringValue"); + } + + public void testSerializingObjectImplementingInterface() throws Exception { + assertEquals(OBJ_JSON, gson.toJson(obj)); + } + + public void testSerializingInterfaceObjectField() throws Exception { + TestObjectWrapper objWrapper = new TestObjectWrapper(obj); + assertEquals("{\"obj\":" + OBJ_JSON + "}", gson.toJson(objWrapper)); + } + + private static interface TestObjectInterface { + // Holder + } + + private static class TestObject implements TestObjectInterface { + @SuppressWarnings("unused") + private String someStringValue; + + private TestObject(String value) { + this.someStringValue = value; + } + } + + private static class TestObjectWrapper { + @SuppressWarnings("unused") + private TestObjectInterface obj; + + private TestObjectWrapper(TestObjectInterface obj) { + this.obj = obj; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java b/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java new file mode 100644 index 00000000..169c37a5 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java @@ -0,0 +1,71 @@ +/*
+ * 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.functional;
+
+import com.google.gson.Gson;
+
+import junit.framework.TestCase;
+
+/**
+ * Functional tests for internationalized strings.
+ *
+ * @author Inderjeet Singh
+ */
+public class InternationalizationTest extends TestCase {
+ private Gson gson;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ gson = new Gson();
+ }
+
+ /*
+ public void testStringsWithRawChineseCharactersSerialization() throws Exception {
+ String target = "好好好";
+ String json = gson.toJson(target);
+ String expected = "\"\\u597d\\u597d\\u597d\"";
+ assertEquals(expected, json);
+ }
+ */
+
+ public void testStringsWithRawChineseCharactersDeserialization() throws Exception {
+ String expected = "好好好";
+ String json = "\"" + expected + "\"";
+ String actual = gson.fromJson(json, String.class);
+ assertEquals(expected, actual);
+ }
+
+ public void testStringsWithUnicodeChineseCharactersSerialization() throws Exception {
+ String target = "\u597d\u597d\u597d";
+ String json = gson.toJson(target);
+ String expected = "\"\u597d\u597d\u597d\"";
+ assertEquals(expected, json);
+ }
+
+ public void testStringsWithUnicodeChineseCharactersDeserialization() throws Exception {
+ String expected = "\u597d\u597d\u597d";
+ String json = "\"" + expected + "\"";
+ String actual = gson.fromJson(json, String.class);
+ assertEquals(expected, actual);
+ }
+
+ public void testStringsWithUnicodeChineseCharactersEscapedDeserialization() throws Exception {
+ String actual = gson.fromJson("'\\u597d\\u597d\\u597d'", String.class);
+ assertEquals("\u597d\u597d\u597d", actual);
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java new file mode 100644 index 00000000..1b9800d8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java @@ -0,0 +1,229 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +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.util.Locale; +import junit.framework.TestCase; + +/** + * Functional tests for the {@link com.google.gson.annotations.JsonAdapter} annotation on classes. + */ +public final class JsonAdapterAnnotationOnClassesTest extends TestCase { + + public void testJsonAdapterInvoked() { + Gson gson = new Gson(); + String json = gson.toJson(new A("bar")); + assertEquals("\"jsonAdapter\"", json); + + // Also invoke the JsonAdapter javadoc sample + json = gson.toJson(new User("Inderjeet", "Singh")); + assertEquals("{\"name\":\"Inderjeet Singh\"}", json); + User user = gson.fromJson("{'name':'Joel Leitch'}", User.class); + assertEquals("Joel", user.firstName); + assertEquals("Leitch", user.lastName); + + json = gson.toJson(Foo.BAR); + assertEquals("\"bar\"", json); + Foo baz = gson.fromJson("\"baz\"", Foo.class); + assertEquals(Foo.BAZ, baz); + } + + public void testJsonAdapterFactoryInvoked() { + Gson gson = new Gson(); + String json = gson.toJson(new C("bar")); + assertEquals("\"jsonAdapterFactory\"", json); + C c = gson.fromJson("\"bar\"", C.class); + assertEquals("jsonAdapterFactory", c.value); + } + + public void testRegisteredAdapterOverridesJsonAdapter() { + TypeAdapter<A> typeAdapter = new TypeAdapter<A>() { + @Override public void write(JsonWriter out, A value) throws IOException { + out.value("registeredAdapter"); + } + @Override public A read(JsonReader in) throws IOException { + return new A(in.nextString()); + } + }; + Gson gson = new GsonBuilder() + .registerTypeAdapter(A.class, typeAdapter) + .create(); + String json = gson.toJson(new A("abcd")); + assertEquals("\"registeredAdapter\"", json); + } + + /** + * The serializer overrides field adapter, but for deserializer the fieldAdapter is used. + */ + public void testRegisteredSerializerOverridesJsonAdapter() { + JsonSerializer<A> serializer = new JsonSerializer<A>() { + public JsonElement serialize(A src, Type typeOfSrc, + JsonSerializationContext context) { + return new JsonPrimitive("registeredSerializer"); + } + }; + Gson gson = new GsonBuilder() + .registerTypeAdapter(A.class, serializer) + .create(); + String json = gson.toJson(new A("abcd")); + assertEquals("\"registeredSerializer\"", json); + A target = gson.fromJson("abcd", A.class); + assertEquals("jsonAdapter", target.value); + } + + /** + * The deserializer overrides Json adapter, but for serializer the jsonAdapter is used. + */ + public void testRegisteredDeserializerOverridesJsonAdapter() { + JsonDeserializer<A> deserializer = new JsonDeserializer<A>() { + public A deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + return new A("registeredDeserializer"); + } + }; + Gson gson = new GsonBuilder() + .registerTypeAdapter(A.class, deserializer) + .create(); + String json = gson.toJson(new A("abcd")); + assertEquals("\"jsonAdapter\"", json); + A target = gson.fromJson("abcd", A.class); + assertEquals("registeredDeserializer", target.value); + } + + public void testIncorrectTypeAdapterFails() { + try { + String json = new Gson().toJson(new ClassWithIncorrectJsonAdapter("bar")); + fail(json); + } catch (ClassCastException expected) {} + } + + public void testSuperclassTypeAdapterNotInvoked() { + String json = new Gson().toJson(new B("bar")); + assertFalse(json.contains("jsonAdapter")); + } + + @JsonAdapter(A.JsonAdapter.class) + private static class A { + final String value; + A(String value) { + this.value = value; + } + static final class JsonAdapter extends TypeAdapter<A> { + @Override public void write(JsonWriter out, A value) throws IOException { + out.value("jsonAdapter"); + } + @Override public A read(JsonReader in) throws IOException { + in.nextString(); + return new A("jsonAdapter"); + } + } + } + + @JsonAdapter(C.JsonAdapterFactory.class) + private static class C { + final String value; + C(String value) { + this.value = value; + } + static final class JsonAdapterFactory implements TypeAdapterFactory { + public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { + return new TypeAdapter<T>() { + @Override public void write(JsonWriter out, T value) throws IOException { + out.value("jsonAdapterFactory"); + } + @SuppressWarnings("unchecked") + @Override public T read(JsonReader in) throws IOException { + in.nextString(); + return (T) new C("jsonAdapterFactory"); + } + }; + } + } + } + + private static final class B extends A { + B(String value) { + super(value); + } + } + // Note that the type is NOT TypeAdapter<ClassWithIncorrectJsonAdapter> so this + // should cause error + @JsonAdapter(A.JsonAdapter.class) + private static final class ClassWithIncorrectJsonAdapter { + @SuppressWarnings("unused") final String value; + ClassWithIncorrectJsonAdapter(String value) { + this.value = value; + } + } + + // This class is used in JsonAdapter Javadoc as an example + @JsonAdapter(UserJsonAdapter.class) + private static class User { + final String firstName, lastName; + User(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + } + private static 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]); + } + } + + @JsonAdapter(FooJsonAdapter.class) + private static enum Foo { BAR, BAZ } + private static class FooJsonAdapter extends TypeAdapter<Foo> { + @Override public void write(JsonWriter out, Foo value) throws IOException { + out.value(value.name().toLowerCase(Locale.US)); + } + + @Override public Foo read(JsonReader in) throws IOException { + return Foo.valueOf(in.nextString().toUpperCase(Locale.US)); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java new file mode 100644 index 00000000..d3f097ea --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java @@ -0,0 +1,203 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Functional tests for the {@link com.google.gson.annotations.JsonAdapter} annotation on fields. + */ +public final class JsonAdapterAnnotationOnFieldsTest extends TestCase { + public void testClassAnnotationAdapterTakesPrecedenceOverDefault() { + Gson gson = new Gson(); + String json = gson.toJson(new Computer(new User("Inderjeet Singh"))); + assertEquals("{\"user\":\"UserClassAnnotationAdapter\"}", json); + Computer computer = gson.fromJson("{'user':'Inderjeet Singh'}", Computer.class); + assertEquals("UserClassAnnotationAdapter", computer.user.name); + } + + public void testClassAnnotationAdapterFactoryTakesPrecedenceOverDefault() { + Gson gson = new Gson(); + String json = gson.toJson(new Gizmo(new Part("Part"))); + assertEquals("{\"part\":\"GizmoPartTypeAdapterFactory\"}", json); + Gizmo computer = gson.fromJson("{'part':'Part'}", Gizmo.class); + assertEquals("GizmoPartTypeAdapterFactory", computer.part.name); + } + + public void testRegisteredTypeAdapterTakesPrecedenceOverClassAnnotationAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(User.class, new RegisteredUserAdapter()) + .create(); + String json = gson.toJson(new Computer(new User("Inderjeet Singh"))); + assertEquals("{\"user\":\"RegisteredUserAdapter\"}", json); + Computer computer = gson.fromJson("{'user':'Inderjeet Singh'}", Computer.class); + assertEquals("RegisteredUserAdapter", computer.user.name); + } + + public void testFieldAnnotationTakesPrecedenceOverRegisteredTypeAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Part.class, new TypeAdapter<Part>() { + @Override public void write(JsonWriter out, Part part) throws IOException { + throw new AssertionError(); + } + + @Override public Part read(JsonReader in) throws IOException { + throw new AssertionError(); + } + }).create(); + String json = gson.toJson(new Gadget(new Part("screen"))); + assertEquals("{\"part\":\"PartJsonFieldAnnotationAdapter\"}", json); + Gadget gadget = gson.fromJson("{'part':'screen'}", Gadget.class); + assertEquals("PartJsonFieldAnnotationAdapter", gadget.part.name); + } + + public void testFieldAnnotationTakesPrecedenceOverClassAnnotation() { + Gson gson = new Gson(); + String json = gson.toJson(new Computer2(new User("Inderjeet Singh"))); + assertEquals("{\"user\":\"UserFieldAnnotationAdapter\"}", json); + Computer2 target = gson.fromJson("{'user':'Interjeet Singh'}", Computer2.class); + assertEquals("UserFieldAnnotationAdapter", target.user.name); + } + + private static final class Gadget { + @JsonAdapter(PartJsonFieldAnnotationAdapter.class) + final Part part; + Gadget(Part part) { + this.part = part; + } + } + + private static final class Gizmo { + @JsonAdapter(GizmoPartTypeAdapterFactory.class) + final Part part; + Gizmo(Part part) { + this.part = part; + } + } + + private static final class Part { + final String name; + public Part(String name) { + this.name = name; + } + } + + private static class PartJsonFieldAnnotationAdapter extends TypeAdapter<Part> { + @Override public void write(JsonWriter out, Part part) throws IOException { + out.value("PartJsonFieldAnnotationAdapter"); + } + @Override public Part read(JsonReader in) throws IOException { + in.nextString(); + return new Part("PartJsonFieldAnnotationAdapter"); + } + } + + private static class GizmoPartTypeAdapterFactory implements TypeAdapterFactory { + public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { + return new TypeAdapter<T>() { + @Override public void write(JsonWriter out, T value) throws IOException { + out.value("GizmoPartTypeAdapterFactory"); + } + @SuppressWarnings("unchecked") + @Override public T read(JsonReader in) throws IOException { + in.nextString(); + return (T) new Part("GizmoPartTypeAdapterFactory"); + } + }; + } + } + + private static final class Computer { + final User user; + Computer(User user) { + this.user = user; + } + } + + @JsonAdapter(UserClassAnnotationAdapter.class) + private static class User { + public final String name; + private User(String name) { + this.name = name; + } + } + + private static class UserClassAnnotationAdapter extends TypeAdapter<User> { + @Override public void write(JsonWriter out, User user) throws IOException { + out.value("UserClassAnnotationAdapter"); + } + @Override public User read(JsonReader in) throws IOException { + in.nextString(); + return new User("UserClassAnnotationAdapter"); + } + } + + private static final class Computer2 { + // overrides the JsonAdapter annotation of User with this + @JsonAdapter(UserFieldAnnotationAdapter.class) + final User user; + Computer2(User user) { + this.user = user; + } + } + + private static final class UserFieldAnnotationAdapter extends TypeAdapter<User> { + @Override public void write(JsonWriter out, User user) throws IOException { + out.value("UserFieldAnnotationAdapter"); + } + @Override public User read(JsonReader in) throws IOException { + in.nextString(); + return new User("UserFieldAnnotationAdapter"); + } + } + + private static final class RegisteredUserAdapter extends TypeAdapter<User> { + @Override public void write(JsonWriter out, User user) throws IOException { + out.value("RegisteredUserAdapter"); + } + @Override public User read(JsonReader in) throws IOException { + in.nextString(); + return new User("RegisteredUserAdapter"); + } + } + + public void testJsonAdapterInvokedOnlyForAnnotatedFields() { + Gson gson = new Gson(); + String json = "{'part1':'name','part2':{'name':'name2'}}"; + GadgetWithTwoParts gadget = gson.fromJson(json, GadgetWithTwoParts.class); + assertEquals("PartJsonFieldAnnotationAdapter", gadget.part1.name); + assertEquals("name2", gadget.part2.name); + } + + private static final class GadgetWithTwoParts { + @JsonAdapter(PartJsonFieldAnnotationAdapter.class) final Part part1; + final Part part2; // Doesn't have the JsonAdapter annotation + @SuppressWarnings("unused") GadgetWithTwoParts(Part part1, Part part2) { + this.part1 = part1; + this.part2 = part2; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java b/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java new file mode 100644 index 00000000..22a479b8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java @@ -0,0 +1,162 @@ +/* + * 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.functional; + +import com.google.gson.JsonArray; +import junit.framework.TestCase; + +import java.math.BigInteger; + +/** + * Functional tests for adding primitives to a JsonArray. + * + * @author Dillon Dixon + */ +public class JsonArrayTest extends TestCase { + + public void testStringPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add("Hello"); + jsonArray.add("Goodbye"); + jsonArray.add("Thank you"); + jsonArray.add((String) null); + jsonArray.add("Yes"); + + assertEquals("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]", jsonArray.toString()); + } + + public void testIntegerPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + int x = 1; + jsonArray.add(x); + + x = 2; + jsonArray.add(x); + + x = -3; + jsonArray.add(x); + + jsonArray.add((Integer) null); + + x = 4; + jsonArray.add(x); + + x = 0; + jsonArray.add(x); + + assertEquals("[1,2,-3,null,4,0]", jsonArray.toString()); + } + + public void testDoublePrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + double x = 1.0; + jsonArray.add(x); + + x = 2.13232; + jsonArray.add(x); + + x = 0.121; + jsonArray.add(x); + + jsonArray.add((Double) null); + + x = -0.00234; + jsonArray.add(x); + + jsonArray.add((Double) null); + + assertEquals("[1.0,2.13232,0.121,null,-0.00234,null]", jsonArray.toString()); + } + + public void testBooleanPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add(true); + jsonArray.add(true); + jsonArray.add(false); + jsonArray.add(false); + jsonArray.add((Boolean) null); + jsonArray.add(true); + + assertEquals("[true,true,false,false,null,true]", jsonArray.toString()); + } + + public void testCharPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add('a'); + jsonArray.add('e'); + jsonArray.add('i'); + jsonArray.add((char) 111); + jsonArray.add((Character) null); + jsonArray.add('u'); + jsonArray.add("and sometimes Y"); + + assertEquals("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]", jsonArray.toString()); + } + + public void testMixedPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add('a'); + jsonArray.add("apple"); + jsonArray.add(12121); + jsonArray.add((char) 111); + jsonArray.add((Boolean) null); + jsonArray.add((Character) null); + jsonArray.add(12.232); + jsonArray.add(BigInteger.valueOf(2323)); + + assertEquals("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]", jsonArray.toString()); + } + + public void testNullPrimitiveAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add((Character) null); + jsonArray.add((Boolean) null); + jsonArray.add((Integer) null); + jsonArray.add((Double) null); + jsonArray.add((Float) null); + jsonArray.add((BigInteger) null); + jsonArray.add((String) null); + jsonArray.add((Boolean) null); + jsonArray.add((Number) null); + + assertEquals("[null,null,null,null,null,null,null,null,null]", jsonArray.toString()); + } + + public void testSameAddition() { + JsonArray jsonArray = new JsonArray(); + + jsonArray.add('a'); + jsonArray.add('a'); + jsonArray.add(true); + jsonArray.add(true); + jsonArray.add(1212); + jsonArray.add(1212); + jsonArray.add(34.34); + jsonArray.add(34.34); + jsonArray.add((Boolean) null); + jsonArray.add((Boolean) null); + + assertEquals("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]", jsonArray.toString()); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonParserTest.java b/gson/src/test/java/com/google/gson/functional/JsonParserTest.java new file mode 100644 index 00000000..44f4477c --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonParserTest.java @@ -0,0 +1,143 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.Nested; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.io.StringReader; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Functional tests for that use JsonParser and related Gson methods + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class JsonParserTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testParseInvalidJson() { + try { + gson.fromJson("[[]", Object[].class); + fail(); + } catch (JsonSyntaxException expected) { } + } + + public void testDeserializingCustomTree() { + JsonObject obj = new JsonObject(); + obj.addProperty("stringValue", "foo"); + obj.addProperty("intValue", 11); + BagOfPrimitives target = gson.fromJson(obj, BagOfPrimitives.class); + assertEquals(11, target.intValue); + assertEquals("foo", target.stringValue); + } + + public void testBadTypeForDeserializingCustomTree() { + JsonObject obj = new JsonObject(); + obj.addProperty("stringValue", "foo"); + obj.addProperty("intValue", 11); + JsonArray array = new JsonArray(); + array.add(obj); + try { + gson.fromJson(array, BagOfPrimitives.class); + fail("BagOfPrimitives is not an array"); + } catch (JsonParseException expected) { } + } + + public void testBadFieldTypeForCustomDeserializerCustomTree() { + JsonArray array = new JsonArray(); + array.add(new JsonPrimitive("blah")); + JsonObject obj = new JsonObject(); + obj.addProperty("stringValue", "foo"); + obj.addProperty("intValue", 11); + obj.add("longValue", array); + + try { + gson.fromJson(obj, BagOfPrimitives.class); + fail("BagOfPrimitives is not an array"); + } catch (JsonParseException expected) { } + } + + public void testBadFieldTypeForDeserializingCustomTree() { + JsonArray array = new JsonArray(); + array.add(new JsonPrimitive("blah")); + JsonObject primitive1 = new JsonObject(); + primitive1.addProperty("string", "foo"); + primitive1.addProperty("intValue", 11); + + JsonObject obj = new JsonObject(); + obj.add("primitive1", primitive1); + obj.add("primitive2", array); + + try { + gson.fromJson(obj, Nested.class); + fail("Nested has field BagOfPrimitives which is not an array"); + } catch (JsonParseException expected) { } + } + + public void testChangingCustomTreeAndDeserializing() { + StringReader json = + new StringReader("{'stringValue':'no message','intValue':10,'longValue':20}"); + JsonObject obj = (JsonObject) new JsonParser().parse(json); + obj.remove("stringValue"); + obj.addProperty("stringValue", "fooBar"); + BagOfPrimitives target = gson.fromJson(obj, BagOfPrimitives.class); + assertEquals(10, target.intValue); + assertEquals(20, target.longValue); + assertEquals("fooBar", target.stringValue); + } + + public void testExtraCommasInArrays() { + Type type = new TypeToken<List<String>>() {}.getType(); + assertEquals(list("a", null, "b", null, null), gson.fromJson("[a,,b,,]", type)); + assertEquals(list(null, null), gson.fromJson("[,]", type)); + assertEquals(list("a", null), gson.fromJson("[a,]", type)); + } + + public void testExtraCommasInMaps() { + Type type = new TypeToken<Map<String, String>>() {}.getType(); + try { + gson.fromJson("{a:b,}", type); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + private <T> List<T> list(T... elements) { + return Arrays.asList(elements); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java b/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java new file mode 100644 index 00000000..a6479403 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java @@ -0,0 +1,89 @@ +package com.google.gson.functional; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import junit.framework.TestCase; + +/** + * Functional tests for {@link Gson#toJsonTree(Object)} and + * {@link Gson#toJsonTree(Object, java.lang.reflect.Type)} + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class JsonTreeTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testToJsonTree() { + BagOfPrimitives bag = new BagOfPrimitives(10L, 5, false, "foo"); + JsonElement json = gson.toJsonTree(bag); + assertTrue(json.isJsonObject()); + JsonObject obj = json.getAsJsonObject(); + Set<Entry<String, JsonElement>> children = obj.entrySet(); + assertEquals(4, children.size()); + assertContains(obj, new JsonPrimitive(10L)); + assertContains(obj, new JsonPrimitive(5)); + assertContains(obj, new JsonPrimitive(false)); + assertContains(obj, new JsonPrimitive("foo")); + } + + public void testToJsonTreeObjectType() { + SubTypeOfBagOfPrimitives bag = new SubTypeOfBagOfPrimitives(10L, 5, false, "foo", 1.4F); + JsonElement json = gson.toJsonTree(bag, BagOfPrimitives.class); + assertTrue(json.isJsonObject()); + JsonObject obj = json.getAsJsonObject(); + Set<Entry<String, JsonElement>> children = obj.entrySet(); + assertEquals(4, children.size()); + assertContains(obj, new JsonPrimitive(10L)); + assertContains(obj, new JsonPrimitive(5)); + assertContains(obj, new JsonPrimitive(false)); + assertContains(obj, new JsonPrimitive("foo")); + } + + public void testJsonTreeToString() { + SubTypeOfBagOfPrimitives bag = new SubTypeOfBagOfPrimitives(10L, 5, false, "foo", 1.4F); + String json1 = gson.toJson(bag); + JsonElement jsonElement = gson.toJsonTree(bag, SubTypeOfBagOfPrimitives.class); + String json2 = gson.toJson(jsonElement); + assertEquals(json1, json2); + } + + public void testJsonTreeNull() { + BagOfPrimitives bag = new BagOfPrimitives(10L, 5, false, null); + JsonObject jsonElement = (JsonObject) gson.toJsonTree(bag, BagOfPrimitives.class); + assertFalse(jsonElement.has("stringValue")); + } + + private void assertContains(JsonObject json, JsonPrimitive child) { + for (Map.Entry<String, JsonElement> entry : json.entrySet()) { + JsonElement node = entry.getValue(); + if (node.isJsonPrimitive()) { + if (node.getAsJsonPrimitive().equals(child)) { + return; + } + } + } + fail(); + } + + private static class SubTypeOfBagOfPrimitives extends BagOfPrimitives { + @SuppressWarnings("unused") + float f = 1.2F; + public SubTypeOfBagOfPrimitives(long l, int i, boolean b, String string, float f) { + super(l, i, b, string); + this.f = f; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java new file mode 100644 index 00000000..c7cfcdf9 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -0,0 +1,141 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import junit.framework.TestCase; + +public class MapAsArrayTypeAdapterTest extends TestCase { + + public void testSerializeComplexMapWithTypeAdapter() { + Type type = new TypeToken<Map<Point, String>>() {}.getType(); + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .create(); + + Map<Point, String> original = new LinkedHashMap<Point, String>(); + original.put(new Point(5, 5), "a"); + original.put(new Point(8, 8), "b"); + String json = gson.toJson(original, type); + assertEquals("[[{\"x\":5,\"y\":5},\"a\"],[{\"x\":8,\"y\":8},\"b\"]]", json); + assertEquals(original, gson.<Map<Point, String>>fromJson(json, type)); + + // test that registering a type adapter for one map doesn't interfere with others + Map<String, Boolean> otherMap = new LinkedHashMap<String, Boolean>(); + otherMap.put("t", true); + otherMap.put("f", false); + assertEquals("{\"t\":true,\"f\":false}", + gson.toJson(otherMap, Map.class)); + assertEquals("{\"t\":true,\"f\":false}", + gson.toJson(otherMap, new TypeToken<Map<String, Boolean>>() {}.getType())); + assertEquals(otherMap, gson.<Object>fromJson("{\"t\":true,\"f\":false}", + new TypeToken<Map<String, Boolean>>() {}.getType())); + } + + public void disabled_testTwoTypesCollapseToOneSerialize() { + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .create(); + + Map<Number, String> original = new LinkedHashMap<Number, String>(); + original.put(new Double(1.0), "a"); + original.put(new Float(1.0), "b"); + try { + gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType()); + fail(); // we no longer hash keys at serialization time + } catch (JsonSyntaxException expected) { + } + } + + public void testTwoTypesCollapseToOneDeserialize() { + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .create(); + + String s = "[[\"1.00\",\"a\"],[\"1.0\",\"b\"]]"; + try { + gson.fromJson(s, new TypeToken<Map<Double, String>>() {}.getType()); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testMultipleEnableComplexKeyRegistrationHasNoEffect() throws Exception { + Type type = new TypeToken<Map<Point, String>>() {}.getType(); + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .enableComplexMapKeySerialization() + .create(); + + Map<Point, String> original = new LinkedHashMap<Point, String>(); + original.put(new Point(6, 5), "abc"); + original.put(new Point(1, 8), "def"); + String json = gson.toJson(original, type); + assertEquals("[[{\"x\":6,\"y\":5},\"abc\"],[{\"x\":1,\"y\":8},\"def\"]]", json); + assertEquals(original, gson.<Map<Point, String>>fromJson(json, type)); + } + + public void testMapWithTypeVariableSerialization() { + Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create(); + PointWithProperty<Point> map = new PointWithProperty<Point>(); + map.map.put(new Point(2, 3), new Point(4, 5)); + Type type = new TypeToken<PointWithProperty<Point>>(){}.getType(); + String json = gson.toJson(map, type); + assertEquals("{\"map\":[[{\"x\":2,\"y\":3},{\"x\":4,\"y\":5}]]}", json); + } + + public void testMapWithTypeVariableDeserialization() { + Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create(); + String json = "{map:[[{x:2,y:3},{x:4,y:5}]]}"; + Type type = new TypeToken<PointWithProperty<Point>>(){}.getType(); + PointWithProperty<Point> map = gson.fromJson(json, type); + Point key = map.map.keySet().iterator().next(); + Point value = map.map.values().iterator().next(); + assertEquals(new Point(2, 3), key); + assertEquals(new Point(4, 5), value); + } + + static class Point { + int x; + int y; + Point(int x, int y) { + this.x = x; + this.y = y; + } + Point() {} + @Override public boolean equals(Object o) { + return o instanceof Point && ((Point) o).x == x && ((Point) o).y == y; + } + @Override public int hashCode() { + return x * 37 + y; + } + @Override public String toString() { + return "(" + x + "," + y + ")"; + } + } + + static class PointWithProperty<T> { + Map<Point, T> map = new HashMap<Point, T>(); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/src/test/java/com/google/gson/functional/MapTest.java new file mode 100755 index 00000000..c175bae5 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -0,0 +1,582 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; +import com.google.gson.common.TestTypes; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Functional test for Json serialization and deserialization for Maps + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class MapTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testMapSerialization() { + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + map.put("a", 1); + map.put("b", 2); + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + assertTrue(json.contains("\"a\":1")); + assertTrue(json.contains("\"b\":2")); + } + + public void testMapDeserialization() { + String json = "{\"a\":1,\"b\":2}"; + Type typeOfMap = new TypeToken<Map<String,Integer>>(){}.getType(); + Map<String, Integer> target = gson.fromJson(json, typeOfMap); + assertEquals(1, target.get("a").intValue()); + assertEquals(2, target.get("b").intValue()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void testRawMapSerialization() { + Map map = new LinkedHashMap(); + map.put("a", 1); + map.put("b", "string"); + String json = gson.toJson(map); + assertTrue(json.contains("\"a\":1")); + assertTrue(json.contains("\"b\":\"string\"")); + } + + public void testMapSerializationEmpty() { + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + assertEquals("{}", json); + } + + public void testMapDeserializationEmpty() { + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + Map<String, Integer> map = gson.fromJson("{}", typeOfMap); + assertTrue(map.isEmpty()); + } + + public void testMapSerializationWithNullValue() { + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + map.put("abc", null); + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + + // Maps are represented as JSON objects, so ignoring null field + assertEquals("{}", json); + } + + public void testMapDeserializationWithNullValue() { + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + Map<String, Integer> map = gson.fromJson("{\"abc\":null}", typeOfMap); + assertEquals(1, map.size()); + assertNull(map.get("abc")); + } + + public void testMapSerializationWithNullValueButSerializeNulls() { + gson = new GsonBuilder().serializeNulls().create(); + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + map.put("abc", null); + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + + assertEquals("{\"abc\":null}", json); + } + + public void testMapSerializationWithNullKey() { + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + map.put(null, 123); + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + + assertEquals("{\"null\":123}", json); + } + + public void testMapDeserializationWithNullKey() { + Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType(); + Map<String, Integer> map = gson.fromJson("{\"null\":123}", typeOfMap); + assertEquals(1, map.size()); + assertEquals(123, map.get("null").intValue()); + assertNull(map.get(null)); + + map = gson.fromJson("{null:123}", typeOfMap); + assertEquals(1, map.size()); + assertEquals(123, map.get("null").intValue()); + assertNull(map.get(null)); + } + + public void testMapSerializationWithIntegerKeys() { + Map<Integer, String> map = new LinkedHashMap<Integer, String>(); + map.put(123, "456"); + Type typeOfMap = new TypeToken<Map<Integer, String>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + + assertEquals("{\"123\":\"456\"}", json); + } + + public void testMapDeserializationWithIntegerKeys() { + Type typeOfMap = new TypeToken<Map<Integer, String>>() {}.getType(); + Map<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap); + assertEquals(1, map.size()); + assertTrue(map.containsKey(123)); + assertEquals("456", map.get(123)); + } + + public void testHashMapDeserialization() throws Exception { + Type typeOfMap = new TypeToken<HashMap<Integer, String>>() {}.getType(); + HashMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap); + assertEquals(1, map.size()); + assertTrue(map.containsKey(123)); + assertEquals("456", map.get(123)); + } + + public void testSortedMap() throws Exception { + Type typeOfMap = new TypeToken<SortedMap<Integer, String>>() {}.getType(); + SortedMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap); + assertEquals(1, map.size()); + assertTrue(map.containsKey(123)); + assertEquals("456", map.get(123)); + } + + public void testParameterizedMapSubclassSerialization() { + MyParameterizedMap<String, String> map = new MyParameterizedMap<String, String>(10); + map.put("a", "b"); + Type type = new TypeToken<MyParameterizedMap<String, String>>() {}.getType(); + String json = gson.toJson(map, type); + assertTrue(json.contains("\"a\":\"b\"")); + } + + @SuppressWarnings({ "unused", "serial" }) + private static class MyParameterizedMap<K, V> extends LinkedHashMap<K, V> { + final int foo; + MyParameterizedMap(int foo) { + this.foo = foo; + } + } + + public void testMapSubclassSerialization() { + MyMap map = new MyMap(); + map.put("a", "b"); + String json = gson.toJson(map, MyMap.class); + assertTrue(json.contains("\"a\":\"b\"")); + } + + public void testMapStandardSubclassDeserialization() { + String json = "{a:'1',b:'2'}"; + Type type = new TypeToken<LinkedHashMap<String, String>>() {}.getType(); + LinkedHashMap<String, Integer> map = gson.fromJson(json, type); + assertEquals("1", map.get("a")); + assertEquals("2", map.get("b")); + } + + public void testMapSubclassDeserialization() { + Gson gson = new GsonBuilder().registerTypeAdapter(MyMap.class, new InstanceCreator<MyMap>() { + public MyMap createInstance(Type type) { + return new MyMap(); + } + }).create(); + String json = "{\"a\":1,\"b\":2}"; + MyMap map = gson.fromJson(json, MyMap.class); + assertEquals("1", map.get("a")); + assertEquals("2", map.get("b")); + } + + public void testCustomSerializerForSpecificMapType() { + Type type = $Gson$Types.newParameterizedTypeWithOwner( + null, Map.class, String.class, Long.class); + Gson gson = new GsonBuilder() + .registerTypeAdapter(type, new JsonSerializer<Map<String, Long>>() { + public JsonElement serialize(Map<String, Long> src, Type typeOfSrc, + JsonSerializationContext context) { + JsonArray array = new JsonArray(); + for (long value : src.values()) { + array.add(new JsonPrimitive(value)); + } + return array; + } + }).create(); + + Map<String, Long> src = new LinkedHashMap<String, Long>(); + src.put("one", 1L); + src.put("two", 2L); + src.put("three", 3L); + + assertEquals("[1,2,3]", gson.toJson(src, type)); + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 + */ + private static class ClassWithAMap { + Map<String, String> map = new TreeMap<String, String>(); + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 + */ + public void testMapSerializationWithNullValues() { + ClassWithAMap target = new ClassWithAMap(); + target.map.put("name1", null); + target.map.put("name2", "value2"); + String json = gson.toJson(target); + assertFalse(json.contains("name1")); + assertTrue(json.contains("name2")); + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 + */ + public void testMapSerializationWithNullValuesSerialized() { + Gson gson = new GsonBuilder().serializeNulls().create(); + ClassWithAMap target = new ClassWithAMap(); + target.map.put("name1", null); + target.map.put("name2", "value2"); + String json = gson.toJson(target); + assertTrue(json.contains("name1")); + assertTrue(json.contains("name2")); + } + + public void testMapSerializationWithWildcardValues() { + Map<String, ? extends Collection<? extends Integer>> map = + new LinkedHashMap<String, Collection<Integer>>(); + map.put("test", null); + Type typeOfMap = + new TypeToken<Map<String, ? extends Collection<? extends Integer>>>() {}.getType(); + String json = gson.toJson(map, typeOfMap); + + assertEquals("{}", json); + } + + public void testMapDeserializationWithWildcardValues() { + Type typeOfMap = new TypeToken<Map<String, ? extends Long>>() {}.getType(); + Map<String, ? extends Long> map = gson.fromJson("{\"test\":123}", typeOfMap); + assertEquals(1, map.size()); + assertEquals(new Long(123L), map.get("test")); + } + + + private static class MyMap extends LinkedHashMap<String, String> { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + int foo = 10; + } + + /** + * From bug report http://code.google.com/p/google-gson/issues/detail?id=95 + */ + public void testMapOfMapSerialization() { + Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>(); + Map<String, String> nestedMap = new HashMap<String, String>(); + nestedMap.put("1", "1"); + nestedMap.put("2", "2"); + map.put("nestedMap", nestedMap); + String json = gson.toJson(map); + assertTrue(json.contains("nestedMap")); + assertTrue(json.contains("\"1\":\"1\"")); + assertTrue(json.contains("\"2\":\"2\"")); + } + + /** + * From bug report http://code.google.com/p/google-gson/issues/detail?id=95 + */ + public void testMapOfMapDeserialization() { + String json = "{nestedMap:{'2':'2','1':'1'}}"; + Type type = new TypeToken<Map<String, Map<String, String>>>(){}.getType(); + Map<String, Map<String, String>> map = gson.fromJson(json, type); + Map<String, String> nested = map.get("nestedMap"); + assertEquals("1", nested.get("1")); + assertEquals("2", nested.get("2")); + } + + /** + * From bug report http://code.google.com/p/google-gson/issues/detail?id=178 + */ + public void testMapWithQuotes() { + Map<String, String> map = new HashMap<String, String>(); + map.put("a\"b", "c\"d"); + String json = gson.toJson(map); + assertEquals("{\"a\\\"b\":\"c\\\"d\"}", json); + } + + /** + * From issue 227. + */ + public void testWriteMapsWithEmptyStringKey() { + Map<String, Boolean> map = new HashMap<String, Boolean>(); + map.put("", true); + assertEquals("{\"\":true}", gson.toJson(map)); + + } + + public void testReadMapsWithEmptyStringKey() { + Map<String, Boolean> map = gson.fromJson("{\"\":true}", new TypeToken<Map<String, Boolean>>() {}.getType()); + assertEquals(Boolean.TRUE, map.get("")); + } + + /** + * From bug report http://code.google.com/p/google-gson/issues/detail?id=204 + */ + public void testSerializeMaps() { + Map<String, Object> map = new LinkedHashMap<String, Object>(); + map.put("a", 12); + map.put("b", null); + + LinkedHashMap<String, Object> innerMap = new LinkedHashMap<String, Object>(); + innerMap.put("test", 1); + innerMap.put("TestStringArray", new String[] { "one", "two" }); + map.put("c", innerMap); + + assertEquals("{\"a\":12,\"b\":null,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"]}}", + new GsonBuilder().serializeNulls().create().toJson(map)); + assertEquals("{\n \"a\": 12,\n \"b\": null,\n \"c\": " + + "{\n \"test\": 1,\n \"TestStringArray\": " + + "[\n \"one\",\n \"two\"\n ]\n }\n}", + new GsonBuilder().setPrettyPrinting().serializeNulls().create().toJson(map)); + assertEquals("{\"a\":12,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"]}}", + new GsonBuilder().create().toJson(map)); + assertEquals("{\n \"a\": 12,\n \"c\": " + + "{\n \"test\": 1,\n \"TestStringArray\": " + + "[\n \"one\",\n \"two\"\n ]\n }\n}", + new GsonBuilder().setPrettyPrinting().create().toJson(map)); + + innerMap.put("d", "e"); + assertEquals("{\"a\":12,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"],\"d\":\"e\"}}", + new Gson().toJson(map)); + } + + public final void testInterfaceTypeMap() { + MapClass element = new MapClass(); + TestTypes.Sub subType = new TestTypes.Sub(); + element.addBase("Test", subType); + element.addSub("Test", subType); + + String subTypeJson = new Gson().toJson(subType); + String expected = "{\"bases\":{\"Test\":" + subTypeJson + "}," + + "\"subs\":{\"Test\":" + subTypeJson + "}}"; + + Gson gsonWithComplexKeys = new GsonBuilder() + .enableComplexMapKeySerialization() + .create(); + String json = gsonWithComplexKeys.toJson(element); + assertEquals(expected, json); + + Gson gson = new Gson(); + json = gson.toJson(element); + assertEquals(expected, json); + } + + public final void testInterfaceTypeMapWithSerializer() { + MapClass element = new MapClass(); + TestTypes.Sub subType = new TestTypes.Sub(); + element.addBase("Test", subType); + element.addSub("Test", subType); + + Gson tempGson = new Gson(); + String subTypeJson = tempGson.toJson(subType); + final JsonElement baseTypeJsonElement = tempGson.toJsonTree(subType, TestTypes.Base.class); + String baseTypeJson = tempGson.toJson(baseTypeJsonElement); + String expected = "{\"bases\":{\"Test\":" + baseTypeJson + "}," + + "\"subs\":{\"Test\":" + subTypeJson + "}}"; + + JsonSerializer<TestTypes.Base> baseTypeAdapter = new JsonSerializer<TestTypes.Base>() { + public JsonElement serialize(TestTypes.Base src, Type typeOfSrc, + JsonSerializationContext context) { + return baseTypeJsonElement; + } + }; + + Gson gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .registerTypeAdapter(TestTypes.Base.class, baseTypeAdapter) + .create(); + String json = gson.toJson(element); + assertEquals(expected, json); + + gson = new GsonBuilder() + .registerTypeAdapter(TestTypes.Base.class, baseTypeAdapter) + .create(); + json = gson.toJson(element); + assertEquals(expected, json); + } + + public void testGeneralMapField() throws Exception { + MapWithGeneralMapParameters map = new MapWithGeneralMapParameters(); + map.map.put("string", "testString"); + map.map.put("stringArray", new String[]{"one", "two"}); + map.map.put("objectArray", new Object[]{1, 2L, "three"}); + + String expected = "{\"map\":{\"string\":\"testString\",\"stringArray\":" + + "[\"one\",\"two\"],\"objectArray\":[1,2,\"three\"]}}"; + assertEquals(expected, gson.toJson(map)); + + gson = new GsonBuilder() + .enableComplexMapKeySerialization() + .create(); + assertEquals(expected, gson.toJson(map)); + } + + public void testComplexKeysSerialization() { + Map<Point, String> map = new LinkedHashMap<Point, String>(); + map.put(new Point(2, 3), "a"); + map.put(new Point(5, 7), "b"); + String json = "{\"2,3\":\"a\",\"5,7\":\"b\"}"; + assertEquals(json, gson.toJson(map, new TypeToken<Map<Point, String>>() {}.getType())); + assertEquals(json, gson.toJson(map, Map.class)); + } + + public void testComplexKeysDeserialization() { + String json = "{'2,3':'a','5,7':'b'}"; + try { + gson.fromJson(json, new TypeToken<Map<Point, String>>() {}.getType()); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testStringKeyDeserialization() { + String json = "{'2,3':'a','5,7':'b'}"; + Map<String, String> map = new LinkedHashMap<String, String>(); + map.put("2,3", "a"); + map.put("5,7", "b"); + assertEquals(map, gson.fromJson(json, new TypeToken<Map<String, String>>() {}.getType())); + } + + public void testNumberKeyDeserialization() { + String json = "{'2.3':'a','5.7':'b'}"; + Map<Double, String> map = new LinkedHashMap<Double, String>(); + map.put(2.3, "a"); + map.put(5.7, "b"); + assertEquals(map, gson.fromJson(json, new TypeToken<Map<Double, String>>() {}.getType())); + } + + public void testBooleanKeyDeserialization() { + String json = "{'true':'a','false':'b'}"; + Map<Boolean, String> map = new LinkedHashMap<Boolean, String>(); + map.put(true, "a"); + map.put(false, "b"); + assertEquals(map, gson.fromJson(json, new TypeToken<Map<Boolean, String>>() {}.getType())); + } + + public void testMapDeserializationWithDuplicateKeys() { + try { + gson.fromJson("{'a':1,'a':2}", new TypeToken<Map<String, Integer>>() {}.getType()); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testSerializeMapOfMaps() { + Type type = new TypeToken<Map<String, Map<String, String>>>() {}.getType(); + Map<String, Map<String, String>> map = newMap( + "a", newMap("ka1", "va1", "ka2", "va2"), + "b", newMap("kb1", "vb1", "kb2", "vb2")); + assertEquals("{'a':{'ka1':'va1','ka2':'va2'},'b':{'kb1':'vb1','kb2':'vb2'}}", + gson.toJson(map, type).replace('"', '\'')); + } + + public void testDeerializeMapOfMaps() { + Type type = new TypeToken<Map<String, Map<String, String>>>() {}.getType(); + Map<String, Map<String, String>> map = newMap( + "a", newMap("ka1", "va1", "ka2", "va2"), + "b", newMap("kb1", "vb1", "kb2", "vb2")); + String json = "{'a':{'ka1':'va1','ka2':'va2'},'b':{'kb1':'vb1','kb2':'vb2'}}"; + assertEquals(map, gson.fromJson(json, type)); + } + + private <K, V> Map<K, V> newMap(K key1, V value1, K key2, V value2) { + Map<K, V> result = new LinkedHashMap<K, V>(); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + + public void testMapNamePromotionWithJsonElementReader() { + String json = "{'2.3':'a'}"; + Map<Double, String> map = new LinkedHashMap<Double, String>(); + map.put(2.3, "a"); + JsonElement tree = new JsonParser().parse(json); + assertEquals(map, gson.fromJson(tree, new TypeToken<Map<Double, String>>() {}.getType())); + } + + static class Point { + private final int x; + private final int y; + + Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override public boolean equals(Object o) { + return o instanceof Point && x == ((Point) o).x && y == ((Point) o).y; + } + + @Override public int hashCode() { + return x * 37 + y; + } + + @Override public String toString() { + return x + "," + y; + } + } + + static final class MapClass { + private final Map<String, TestTypes.Base> bases = new HashMap<String, TestTypes.Base>(); + private final Map<String, TestTypes.Sub> subs = new HashMap<String, TestTypes.Sub>(); + + public final void addBase(String name, TestTypes.Base value) { + bases.put(name, value); + } + + public final void addSub(String name, TestTypes.Sub value) { + subs.put(name, value); + } + } + + static final class MapWithGeneralMapParameters { + @SuppressWarnings({"rawtypes", "unchecked"}) + final Map<String, Object> map = new LinkedHashMap(); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java new file mode 100644 index 00000000..7ecbffc8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java @@ -0,0 +1,178 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Tests for Gson serialization of a sub-class object while encountering a base-class type + * + * @author Inderjeet Singh + */ +@SuppressWarnings("unused") +public class MoreSpecificTypeSerializationTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testSubclassFields() { + ClassWithBaseFields target = new ClassWithBaseFields(new Sub(1, 2)); + String json = gson.toJson(target); + assertTrue(json.contains("\"b\":1")); + assertTrue(json.contains("\"s\":2")); + } + + public void testListOfSubclassFields() { + Collection<Base> list = new ArrayList<Base>(); + list.add(new Base(1)); + list.add(new Sub(2, 3)); + ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(list, null); + String json = gson.toJson(target); + assertTrue(json, json.contains("{\"b\":1}")); + assertTrue(json, json.contains("{\"s\":3,\"b\":2}")); + } + + public void testMapOfSubclassFields() { + Map<String, Base> map = new HashMap<String, Base>(); + map.put("base", new Base(1)); + map.put("sub", new Sub(2, 3)); + ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(null, map); + JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject(); + assertEquals(1, json.get("base").getAsJsonObject().get("b").getAsInt()); + JsonObject sub = json.get("sub").getAsJsonObject(); + assertEquals(2, sub.get("b").getAsInt()); + assertEquals(3, sub.get("s").getAsInt()); + } + + /** + * For parameterized type, Gson ignores the more-specific type and sticks to the declared type + */ + public void testParameterizedSubclassFields() { + ClassWithParameterizedBaseFields target = new ClassWithParameterizedBaseFields( + new ParameterizedSub<String>("one", "two")); + String json = gson.toJson(target); + assertTrue(json.contains("\"t\":\"one\"")); + assertFalse(json.contains("\"s\"")); + } + + /** + * For parameterized type in a List, Gson ignores the more-specific type and sticks to + * the declared type + */ + public void testListOfParameterizedSubclassFields() { + Collection<ParameterizedBase<String>> list = new ArrayList<ParameterizedBase<String>>(); + list.add(new ParameterizedBase<String>("one")); + list.add(new ParameterizedSub<String>("two", "three")); + ClassWithContainersOfParameterizedBaseFields target = + new ClassWithContainersOfParameterizedBaseFields(list, null); + String json = gson.toJson(target); + assertTrue(json, json.contains("{\"t\":\"one\"}")); + assertFalse(json, json.contains("\"s\":")); + } + + /** + * For parameterized type in a map, Gson ignores the more-specific type and sticks to the + * declared type + */ + public void testMapOfParameterizedSubclassFields() { + Map<String, ParameterizedBase<String>> map = new HashMap<String, ParameterizedBase<String>>(); + map.put("base", new ParameterizedBase<String>("one")); + map.put("sub", new ParameterizedSub<String>("two", "three")); + ClassWithContainersOfParameterizedBaseFields target = + new ClassWithContainersOfParameterizedBaseFields(null, map); + JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject(); + assertEquals("one", json.get("base").getAsJsonObject().get("t").getAsString()); + JsonObject sub = json.get("sub").getAsJsonObject(); + assertEquals("two", sub.get("t").getAsString()); + assertNull(sub.get("s")); + } + + private static class Base { + int b; + Base(int b) { + this.b = b; + } + } + + private static class Sub extends Base { + int s; + Sub(int b, int s) { + super(b); + this.s = s; + } + } + + private static class ClassWithBaseFields { + Base b; + ClassWithBaseFields(Base b) { + this.b = b; + } + } + + private static class ClassWithContainersOfBaseFields { + Collection<Base> collection; + Map<String, Base> map; + ClassWithContainersOfBaseFields(Collection<Base> collection, Map<String, Base> map) { + this.collection = collection; + this.map = map; + } + } + + private static class ParameterizedBase<T> { + T t; + ParameterizedBase(T t) { + this.t = t; + } + } + + private static class ParameterizedSub<T> extends ParameterizedBase<T> { + T s; + ParameterizedSub(T t, T s) { + super(t); + this.s = s; + } + } + + private static class ClassWithParameterizedBaseFields { + ParameterizedBase<String> b; + ClassWithParameterizedBaseFields(ParameterizedBase<String> b) { + this.b = b; + } + } + + private static class ClassWithContainersOfParameterizedBaseFields { + Collection<ParameterizedBase<String>> collection; + Map<String, ParameterizedBase<String>> map; + ClassWithContainersOfParameterizedBaseFields(Collection<ParameterizedBase<String>> collection, + Map<String, ParameterizedBase<String>> map) { + this.collection = collection; + this.map = map; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java new file mode 100644 index 00000000..8975c155 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java @@ -0,0 +1,188 @@ +/* + * 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.functional; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.FieldNamingStrategy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import com.google.gson.common.TestTypes.ClassWithSerializedNameFields; +import com.google.gson.common.TestTypes.StringWrapper; + +import junit.framework.TestCase; + +import java.lang.reflect.Field; + +/** + * Functional tests for naming policies. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class NamingPolicyTest extends TestCase { + private GsonBuilder builder; + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new GsonBuilder(); + } + + public void testGsonWithNonDefaultFieldNamingPolicySerialization() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); + StringWrapper target = new StringWrapper("blah"); + assertEquals("{\"SomeConstantStringInstanceField\":\"" + + target.someConstantStringInstanceField + "\"}", gson.toJson(target)); + } + + public void testGsonWithNonDefaultFieldNamingPolicyDeserialiation() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); + String target = "{\"SomeConstantStringInstanceField\":\"someValue\"}"; + StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class); + assertEquals("someValue", deserializedObject.someConstantStringInstanceField); + } + + public void testGsonWithLowerCaseDashPolicySerialization() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES).create(); + StringWrapper target = new StringWrapper("blah"); + assertEquals("{\"some-constant-string-instance-field\":\"" + + target.someConstantStringInstanceField + "\"}", gson.toJson(target)); + } + + public void testGsonWithLowerCaseDashPolicyDeserialiation() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES).create(); + String target = "{\"some-constant-string-instance-field\":\"someValue\"}"; + StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class); + assertEquals("someValue", deserializedObject.someConstantStringInstanceField); + } + + public void testGsonWithLowerCaseUnderscorePolicySerialization() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + StringWrapper target = new StringWrapper("blah"); + assertEquals("{\"some_constant_string_instance_field\":\"" + + target.someConstantStringInstanceField + "\"}", gson.toJson(target)); + } + + public void testGsonWithLowerCaseUnderscorePolicyDeserialiation() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + String target = "{\"some_constant_string_instance_field\":\"someValue\"}"; + StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class); + assertEquals("someValue", deserializedObject.someConstantStringInstanceField); + } + + public void testGsonWithSerializedNameFieldNamingPolicySerialization() { + Gson gson = builder.create(); + ClassWithSerializedNameFields expected = new ClassWithSerializedNameFields(5, 6); + String actual = gson.toJson(expected); + assertEquals(expected.getExpectedJson(), actual); + } + + public void testGsonWithSerializedNameFieldNamingPolicyDeserialization() { + Gson gson = builder.create(); + ClassWithSerializedNameFields expected = new ClassWithSerializedNameFields(5, 7); + ClassWithSerializedNameFields actual = + gson.fromJson(expected.getExpectedJson(), ClassWithSerializedNameFields.class); + assertEquals(expected.f, actual.f); + } + + public void testGsonDuplicateNameUsingSerializedNameFieldNamingPolicySerialization() { + Gson gson = builder.create(); + try { + ClassWithDuplicateFields target = new ClassWithDuplicateFields(10); + gson.toJson(target); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testGsonWithUpperCamelCaseSpacesPolicySerialiation() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES) + .create(); + StringWrapper target = new StringWrapper("blah"); + assertEquals("{\"Some Constant String Instance Field\":\"" + + target.someConstantStringInstanceField + "\"}", gson.toJson(target)); + } + + public void testGsonWithUpperCamelCaseSpacesPolicyDeserialiation() { + Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES) + .create(); + String target = "{\"Some Constant String Instance Field\":\"someValue\"}"; + StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class); + assertEquals("someValue", deserializedObject.someConstantStringInstanceField); + } + + public void testDeprecatedNamingStrategy() throws Exception { + Gson gson = builder.setFieldNamingStrategy(new UpperCaseNamingStrategy()).create(); + ClassWithDuplicateFields target = new ClassWithDuplicateFields(10); + String actual = gson.toJson(target); + assertEquals("{\"A\":10}", actual); + } + + public void testComplexFieldNameStrategy() throws Exception { + Gson gson = new Gson(); + String json = gson.toJson(new ClassWithComplexFieldName(10)); + String escapedFieldName = "@value\\\"_s$\\\\"; + assertEquals("{\"" + escapedFieldName + "\":10}", json); + + ClassWithComplexFieldName obj = gson.fromJson(json, ClassWithComplexFieldName.class); + assertEquals(10, obj.value); + } + + /** http://code.google.com/p/google-gson/issues/detail?id=349 */ + public void testAtSignInSerializedName() { + assertEquals("{\"@foo\":\"bar\"}", new Gson().toJson(new AtName())); + } + + static class AtName { + @SerializedName("@foo") String f = "bar"; + } + + private static class UpperCaseNamingStrategy implements FieldNamingStrategy { + public String translateName(Field f) { + return f.getName().toUpperCase(); + } + } + + @SuppressWarnings("unused") + private static class ClassWithDuplicateFields { + public Integer a; + @SerializedName("a") public Double b; + + public ClassWithDuplicateFields(Integer a) { + this(a, null); + } + + public ClassWithDuplicateFields(Double b) { + this(null, b); + } + + public ClassWithDuplicateFields(Integer a, Double b) { + this.a = a; + this.b = b; + } + } + + private static class ClassWithComplexFieldName { + @SerializedName("@value\"_s$\\") public final long value; + + ClassWithComplexFieldName(long value) { + this.value = value; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java b/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java new file mode 100755 index 00000000..742ee221 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java @@ -0,0 +1,240 @@ +/*
+ * 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.functional;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.ClassWithObjects;
+
+import junit.framework.TestCase;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+/**
+ * Functional tests for the different cases for serializing (or ignoring) null fields and object.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class NullObjectAndFieldTest extends TestCase {
+ private GsonBuilder gsonBuilder;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ gsonBuilder = new GsonBuilder().serializeNulls();
+ }
+
+ public void testTopLevelNullObjectSerialization() {
+ Gson gson = gsonBuilder.create();
+ String actual = gson.toJson(null);
+ assertEquals("null", actual);
+
+ actual = gson.toJson(null, String.class);
+ assertEquals("null", actual);
+ }
+
+ public void testTopLevelNullObjectDeserialization() throws Exception {
+ Gson gson = gsonBuilder.create();
+ String actual = gson.fromJson("null", String.class);
+ assertNull(actual);
+ }
+
+ public void testExplicitSerializationOfNulls() {
+ Gson gson = gsonBuilder.create();
+ ClassWithObjects target = new ClassWithObjects(null);
+ String actual = gson.toJson(target);
+ String expected = "{\"bag\":null}";
+ assertEquals(expected, actual);
+ }
+
+ public void testExplicitDeserializationOfNulls() throws Exception {
+ Gson gson = gsonBuilder.create();
+ ClassWithObjects target = gson.fromJson("{\"bag\":null}", ClassWithObjects.class);
+ assertNull(target.bag);
+ }
+
+ public void testExplicitSerializationOfNullArrayMembers() {
+ Gson gson = gsonBuilder.create();
+ ClassWithMembers target = new ClassWithMembers();
+ String json = gson.toJson(target);
+ assertTrue(json.contains("\"array\":null"));
+ }
+
+ /**
+ * Added to verify http://code.google.com/p/google-gson/issues/detail?id=68
+ */
+ public void testNullWrappedPrimitiveMemberSerialization() {
+ Gson gson = gsonBuilder.serializeNulls().create();
+ ClassWithNullWrappedPrimitive target = new ClassWithNullWrappedPrimitive();
+ String json = gson.toJson(target);
+ assertTrue(json.contains("\"value\":null"));
+ }
+
+ /**
+ * Added to verify http://code.google.com/p/google-gson/issues/detail?id=68
+ */
+ public void testNullWrappedPrimitiveMemberDeserialization() {
+ Gson gson = gsonBuilder.create();
+ String json = "{'value':null}";
+ ClassWithNullWrappedPrimitive target = gson.fromJson(json, ClassWithNullWrappedPrimitive.class);
+ assertNull(target.value);
+ }
+
+ public void testExplicitSerializationOfNullCollectionMembers() {
+ Gson gson = gsonBuilder.create();
+ ClassWithMembers target = new ClassWithMembers();
+ String json = gson.toJson(target);
+ assertTrue(json.contains("\"col\":null"));
+ }
+
+ public void testExplicitSerializationOfNullStringMembers() {
+ Gson gson = gsonBuilder.create();
+ ClassWithMembers target = new ClassWithMembers();
+ String json = gson.toJson(target);
+ assertTrue(json.contains("\"str\":null"));
+ }
+
+ public void testCustomSerializationOfNulls() {
+ gsonBuilder.registerTypeAdapter(ClassWithObjects.class, new ClassWithObjectsSerializer());
+ Gson gson = gsonBuilder.create();
+ ClassWithObjects target = new ClassWithObjects(new BagOfPrimitives());
+ String actual = gson.toJson(target);
+ String expected = "{\"bag\":null}";
+ assertEquals(expected, actual);
+ }
+
+ public void testPrintPrintingObjectWithNulls() throws Exception {
+ gsonBuilder = new GsonBuilder();
+ Gson gson = gsonBuilder.create();
+ String result = gson.toJson(new ClassWithMembers());
+ assertEquals("{}", result);
+
+ gson = gsonBuilder.serializeNulls().create();
+ result = gson.toJson(new ClassWithMembers());
+ assertTrue(result.contains("\"str\":null"));
+ }
+
+ public void testPrintPrintingArraysWithNulls() throws Exception {
+ gsonBuilder = new GsonBuilder();
+ Gson gson = gsonBuilder.create();
+ String result = gson.toJson(new String[] { "1", null, "3" });
+ assertEquals("[\"1\",null,\"3\"]", result);
+
+ gson = gsonBuilder.serializeNulls().create();
+ result = gson.toJson(new String[] { "1", null, "3" });
+ assertEquals("[\"1\",null,\"3\"]", result);
+ }
+
+ // test for issue 389
+ public void testAbsentJsonElementsAreSetToNull() {
+ Gson gson = new Gson();
+ ClassWithInitializedMembers target =
+ gson.fromJson("{array:[1,2,3]}", ClassWithInitializedMembers.class);
+ assertTrue(target.array.length == 3 && target.array[1] == 2);
+ assertEquals(ClassWithInitializedMembers.MY_STRING_DEFAULT, target.str1);
+ assertNull(target.str2);
+ assertEquals(ClassWithInitializedMembers.MY_INT_DEFAULT, target.int1);
+ assertEquals(0, target.int2); // test the default value of a primitive int field per JVM spec
+ assertEquals(ClassWithInitializedMembers.MY_BOOLEAN_DEFAULT, target.bool1);
+ assertFalse(target.bool2); // test the default value of a primitive boolean field per JVM spec
+ }
+
+ public static class ClassWithInitializedMembers {
+ // Using a mix of no-args constructor and field initializers
+ // Also, some fields are intialized and some are not (so initialized per JVM spec)
+ public static final String MY_STRING_DEFAULT = "string";
+ private static final int MY_INT_DEFAULT = 2;
+ private static final boolean MY_BOOLEAN_DEFAULT = true;
+ int[] array;
+ String str1, str2;
+ int int1 = MY_INT_DEFAULT;
+ int int2;
+ boolean bool1 = MY_BOOLEAN_DEFAULT;
+ boolean bool2;
+ public ClassWithInitializedMembers() {
+ str1 = MY_STRING_DEFAULT;
+ }
+ }
+
+ private static class ClassWithNullWrappedPrimitive {
+ private Long value;
+ }
+
+ @SuppressWarnings("unused")
+ private static class ClassWithMembers {
+ String str;
+ int[] array;
+ Collection<String> col;
+ }
+
+ private static class ClassWithObjectsSerializer implements JsonSerializer<ClassWithObjects> {
+ public JsonElement serialize(ClassWithObjects src, Type typeOfSrc,
+ JsonSerializationContext context) {
+ JsonObject obj = new JsonObject();
+ obj.add("bag", JsonNull.INSTANCE);
+ return obj;
+ }
+ }
+
+ public void testExplicitNullSetsFieldToNullDuringDeserialization() {
+ Gson gson = new Gson();
+ String json = "{value:null}";
+ ObjectWithField obj = gson.fromJson(json, ObjectWithField.class);
+ assertNull(obj.value);
+ }
+
+ public void testCustomTypeAdapterPassesNullSerialization() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(ObjectWithField.class, new JsonSerializer<ObjectWithField>() {
+ public JsonElement serialize(ObjectWithField src, Type typeOfSrc,
+ JsonSerializationContext context) {
+ return context.serialize(null);
+ }
+ }).create();
+ ObjectWithField target = new ObjectWithField();
+ target.value = "value1";
+ String json = gson.toJson(target);
+ assertFalse(json.contains("value1"));
+ }
+
+ public void testCustomTypeAdapterPassesNullDesrialization() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(ObjectWithField.class, new JsonDeserializer<ObjectWithField>() {
+ public ObjectWithField deserialize(JsonElement json, Type type,
+ JsonDeserializationContext context) {
+ return context.deserialize(null, type);
+ }
+ }).create();
+ String json = "{value:'value1'}";
+ ObjectWithField target = gson.fromJson(json, ObjectWithField.class);
+ assertNull(target);
+ }
+
+ private static class ObjectWithField {
+ String value = "";
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java new file mode 100644 index 00000000..de1219a6 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -0,0 +1,501 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.common.TestTypes.ArrayOfObjects; +import com.google.gson.common.TestTypes.BagOfPrimitiveWrappers; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.ClassWithArray; +import com.google.gson.common.TestTypes.ClassWithNoFields; +import com.google.gson.common.TestTypes.ClassWithObjects; +import com.google.gson.common.TestTypes.ClassWithTransientFields; +import com.google.gson.common.TestTypes.Nested; +import com.google.gson.common.TestTypes.PrimitiveArray; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import junit.framework.TestCase; + +/** + * Functional tests for Json serialization and deserialization of regular classes. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ObjectTest extends TestCase { + private Gson gson; + private TimeZone oldTimeZone = TimeZone.getDefault(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); + Locale.setDefault(Locale.US); + } + + @Override + protected void tearDown() throws Exception { + TimeZone.setDefault(oldTimeZone); + super.tearDown(); + } + public void testJsonInSingleQuotesDeserialization() { + String json = "{'stringValue':'no message','intValue':10,'longValue':20}"; + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals("no message", target.stringValue); + assertEquals(10, target.intValue); + assertEquals(20, target.longValue); + } + + public void testJsonInMixedQuotesDeserialization() { + String json = "{\"stringValue\":'no message','intValue':10,'longValue':20}"; + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals("no message", target.stringValue); + assertEquals(10, target.intValue); + assertEquals(20, target.longValue); + } + + public void testBagOfPrimitivesSerialization() throws Exception { + BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue"); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testBagOfPrimitivesDeserialization() throws Exception { + BagOfPrimitives src = new BagOfPrimitives(10, 20, false, "stringValue"); + String json = src.getExpectedJson(); + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(json, target.getExpectedJson()); + } + + public void testBagOfPrimitiveWrappersSerialization() throws Exception { + BagOfPrimitiveWrappers target = new BagOfPrimitiveWrappers(10L, 20, false); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testBagOfPrimitiveWrappersDeserialization() throws Exception { + BagOfPrimitiveWrappers target = new BagOfPrimitiveWrappers(10L, 20, false); + String jsonString = target.getExpectedJson(); + target = gson.fromJson(jsonString, BagOfPrimitiveWrappers.class); + assertEquals(jsonString, target.getExpectedJson()); + } + + public void testClassWithTransientFieldsSerialization() throws Exception { + ClassWithTransientFields<Long> target = new ClassWithTransientFields<Long>(1L); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + @SuppressWarnings("rawtypes") + public void testClassWithTransientFieldsDeserialization() throws Exception { + String json = "{\"longValue\":[1]}"; + ClassWithTransientFields target = gson.fromJson(json, ClassWithTransientFields.class); + assertEquals(json, target.getExpectedJson()); + } + + @SuppressWarnings("rawtypes") + public void testClassWithTransientFieldsDeserializationTransientFieldsPassedInJsonAreIgnored() + throws Exception { + String json = "{\"transientLongValue\":1,\"longValue\":[1]}"; + ClassWithTransientFields target = gson.fromJson(json, ClassWithTransientFields.class); + assertFalse(target.transientLongValue != 1); + } + + public void testClassWithNoFieldsSerialization() throws Exception { + assertEquals("{}", gson.toJson(new ClassWithNoFields())); + } + + public void testClassWithNoFieldsDeserialization() throws Exception { + String json = "{}"; + ClassWithNoFields target = gson.fromJson(json, ClassWithNoFields.class); + ClassWithNoFields expected = new ClassWithNoFields(); + assertEquals(expected, target); + } + + public void testNestedSerialization() throws Exception { + Nested target = new Nested(new BagOfPrimitives(10, 20, false, "stringValue"), + new BagOfPrimitives(30, 40, true, "stringValue")); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testNestedDeserialization() throws Exception { + String json = "{\"primitive1\":{\"longValue\":10,\"intValue\":20,\"booleanValue\":false," + + "\"stringValue\":\"stringValue\"},\"primitive2\":{\"longValue\":30,\"intValue\":40," + + "\"booleanValue\":true,\"stringValue\":\"stringValue\"}}"; + Nested target = gson.fromJson(json, Nested.class); + assertEquals(json, target.getExpectedJson()); + } + public void testNullSerialization() throws Exception { + assertEquals("null", gson.toJson(null)); + } + + public void testEmptyStringDeserialization() throws Exception { + Object object = gson.fromJson("", Object.class); + assertNull(object); + } + + public void testTruncatedDeserialization() { + try { + gson.fromJson("[\"a\", \"b\",", new TypeToken<List<String>>() {}.getType()); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testNullDeserialization() throws Exception { + String myNullObject = null; + Object object = gson.fromJson(myNullObject, Object.class); + assertNull(object); + } + + public void testNullFieldsSerialization() throws Exception { + Nested target = new Nested(new BagOfPrimitives(10, 20, false, "stringValue"), null); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testNullFieldsDeserialization() throws Exception { + String json = "{\"primitive1\":{\"longValue\":10,\"intValue\":20,\"booleanValue\":false" + + ",\"stringValue\":\"stringValue\"}}"; + Nested target = gson.fromJson(json, Nested.class); + assertEquals(json, target.getExpectedJson()); + } + + public void testArrayOfObjectsSerialization() throws Exception { + ArrayOfObjects target = new ArrayOfObjects(); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testArrayOfObjectsDeserialization() throws Exception { + String json = new ArrayOfObjects().getExpectedJson(); + ArrayOfObjects target = gson.fromJson(json, ArrayOfObjects.class); + assertEquals(json, target.getExpectedJson()); + } + + public void testArrayOfArraysSerialization() throws Exception { + ArrayOfArrays target = new ArrayOfArrays(); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testArrayOfArraysDeserialization() throws Exception { + String json = new ArrayOfArrays().getExpectedJson(); + ArrayOfArrays target = gson.fromJson(json, ArrayOfArrays.class); + assertEquals(json, target.getExpectedJson()); + } + + public void testArrayOfObjectsAsFields() throws Exception { + ClassWithObjects classWithObjects = new ClassWithObjects(); + BagOfPrimitives bagOfPrimitives = new BagOfPrimitives(); + String stringValue = "someStringValueInArray"; + String classWithObjectsJson = gson.toJson(classWithObjects); + String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives); + + ClassWithArray classWithArray = new ClassWithArray( + new Object[] { stringValue, classWithObjects, bagOfPrimitives }); + String json = gson.toJson(classWithArray); + + assertTrue(json.contains(classWithObjectsJson)); + assertTrue(json.contains(bagOfPrimitivesJson)); + assertTrue(json.contains("\"" + stringValue + "\"")); + } + + /** + * Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 + */ + public void testNullArraysDeserialization() throws Exception { + String json = "{\"array\": null}"; + ClassWithArray target = gson.fromJson(json, ClassWithArray.class); + assertNull(target.array); + } + + /** + * Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 + */ + public void testNullObjectFieldsDeserialization() throws Exception { + String json = "{\"bag\": null}"; + ClassWithObjects target = gson.fromJson(json, ClassWithObjects.class); + assertNull(target.bag); + } + + public void testEmptyCollectionInAnObjectDeserialization() throws Exception { + String json = "{\"children\":[]}"; + ClassWithCollectionField target = gson.fromJson(json, ClassWithCollectionField.class); + assertNotNull(target); + assertTrue(target.children.isEmpty()); + } + + private static class ClassWithCollectionField { + Collection<String> children = new ArrayList<String>(); + } + + public void testPrimitiveArrayInAnObjectDeserialization() throws Exception { + String json = "{\"longArray\":[0,1,2,3,4,5,6,7,8,9]}"; + PrimitiveArray target = gson.fromJson(json, PrimitiveArray.class); + assertEquals(json, target.getExpectedJson()); + } + + /** + * Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 + */ + public void testNullPrimitiveFieldsDeserialization() throws Exception { + String json = "{\"longValue\":null}"; + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(BagOfPrimitives.DEFAULT_VALUE, target.longValue); + } + + public void testEmptyCollectionInAnObjectSerialization() throws Exception { + ClassWithCollectionField target = new ClassWithCollectionField(); + assertEquals("{\"children\":[]}", gson.toJson(target)); + } + + public void testPrivateNoArgConstructorDeserialization() throws Exception { + ClassWithPrivateNoArgsConstructor target = + gson.fromJson("{\"a\":20}", ClassWithPrivateNoArgsConstructor.class); + assertEquals(20, target.a); + } + + public void testAnonymousLocalClassesSerialization() throws Exception { + assertEquals("null", gson.toJson(new ClassWithNoFields() { + // empty anonymous class + })); + } + + public void testAnonymousLocalClassesCustomSerialization() throws Exception { + gson = new GsonBuilder() + .registerTypeHierarchyAdapter(ClassWithNoFields.class, + new JsonSerializer<ClassWithNoFields>() { + public JsonElement serialize( + ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonObject(); + } + }).create(); + + assertEquals("null", gson.toJson(new ClassWithNoFields() { + // empty anonymous class + })); + } + + public void testPrimitiveArrayFieldSerialization() { + PrimitiveArray target = new PrimitiveArray(new long[] { 1L, 2L, 3L }); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + /** + * Tests that a class field with type Object can be serialized properly. + * See issue 54 + */ + public void testClassWithObjectFieldSerialization() { + ClassWithObjectField obj = new ClassWithObjectField(); + obj.member = "abc"; + String json = gson.toJson(obj); + assertTrue(json.contains("abc")); + } + + private static class ClassWithObjectField { + @SuppressWarnings("unused") + Object member; + } + + public void testInnerClassSerialization() { + Parent p = new Parent(); + Parent.Child c = p.new Child(); + String json = gson.toJson(c); + assertTrue(json.contains("value2")); + assertFalse(json.contains("value1")); + } + + public void testInnerClassDeserialization() { + final Parent p = new Parent(); + Gson gson = new GsonBuilder().registerTypeAdapter( + Parent.Child.class, new InstanceCreator<Parent.Child>() { + public Parent.Child createInstance(Type type) { + return p.new Child(); + } + }).create(); + String json = "{'value2':3}"; + Parent.Child c = gson.fromJson(json, Parent.Child.class); + assertEquals(3, c.value2); + } + + private static class Parent { + @SuppressWarnings("unused") + int value1 = 1; + private class Child { + int value2 = 2; + } + } + + private static class ArrayOfArrays { + private final BagOfPrimitives[][] elements; + public ArrayOfArrays() { + elements = new BagOfPrimitives[3][2]; + for (int i = 0; i < elements.length; ++i) { + BagOfPrimitives[] row = elements[i]; + for (int j = 0; j < row.length; ++j) { + row[j] = new BagOfPrimitives(i+j, i*j, false, i+"_"+j); + } + } + } + public String getExpectedJson() { + StringBuilder sb = new StringBuilder("{\"elements\":["); + boolean first = true; + for (BagOfPrimitives[] row : elements) { + if (first) { + first = false; + } else { + sb.append(","); + } + boolean firstOfRow = true; + sb.append("["); + for (BagOfPrimitives element : row) { + if (firstOfRow) { + firstOfRow = false; + } else { + sb.append(","); + } + sb.append(element.getExpectedJson()); + } + sb.append("]"); + } + sb.append("]}"); + return sb.toString(); + } + } + + private static class ClassWithPrivateNoArgsConstructor { + public int a; + private ClassWithPrivateNoArgsConstructor() { + a = 10; + } + } + + /** + * In response to Issue 41 http://code.google.com/p/google-gson/issues/detail?id=41 + */ + public void testObjectFieldNamesWithoutQuotesDeserialization() { + String json = "{longValue:1,'booleanValue':true,\"stringValue\":'bar'}"; + BagOfPrimitives bag = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(1, bag.longValue); + assertTrue(bag.booleanValue); + assertEquals("bar", bag.stringValue); + } + + public void testStringFieldWithNumberValueDeserialization() { + String json = "{\"stringValue\":1}"; + BagOfPrimitives bag = gson.fromJson(json, BagOfPrimitives.class); + assertEquals("1", bag.stringValue); + + json = "{\"stringValue\":1.5E+6}"; + bag = gson.fromJson(json, BagOfPrimitives.class); + assertEquals("1.5E+6", bag.stringValue); + + json = "{\"stringValue\":true}"; + bag = gson.fromJson(json, BagOfPrimitives.class); + assertEquals("true", bag.stringValue); + } + + /** + * Created to reproduce issue 140 + */ + public void testStringFieldWithEmptyValueSerialization() { + ClassWithEmptyStringFields target = new ClassWithEmptyStringFields(); + target.a = "5794749"; + String json = gson.toJson(target); + assertTrue(json.contains("\"a\":\"5794749\"")); + assertTrue(json.contains("\"b\":\"\"")); + assertTrue(json.contains("\"c\":\"\"")); + } + + /** + * Created to reproduce issue 140 + */ + public void testStringFieldWithEmptyValueDeserialization() { + String json = "{a:\"5794749\",b:\"\",c:\"\"}"; + ClassWithEmptyStringFields target = gson.fromJson(json, ClassWithEmptyStringFields.class); + assertEquals("5794749", target.a); + assertEquals("", target.b); + assertEquals("", target.c); + } + + private static class ClassWithEmptyStringFields { + String a = ""; + String b = ""; + String c = ""; + } + + public void testJsonObjectSerialization() { + Gson gson = new GsonBuilder().serializeNulls().create(); + JsonObject obj = new JsonObject(); + String json = gson.toJson(obj); + assertEquals("{}", json); + } + + /** + * Test for issue 215. + */ + public void testSingletonLists() { + Gson gson = new Gson(); + Product product = new Product(); + assertEquals("{\"attributes\":[],\"departments\":[]}", + gson.toJson(product)); + gson.fromJson(gson.toJson(product), Product.class); + + product.departments.add(new Department()); + assertEquals("{\"attributes\":[],\"departments\":[{\"name\":\"abc\",\"code\":\"123\"}]}", + gson.toJson(product)); + gson.fromJson(gson.toJson(product), Product.class); + + product.attributes.add("456"); + assertEquals("{\"attributes\":[\"456\"],\"departments\":[{\"name\":\"abc\",\"code\":\"123\"}]}", + gson.toJson(product)); + gson.fromJson(gson.toJson(product), Product.class); + } + + // http://code.google.com/p/google-gson/issues/detail?id=270 + public void testDateAsMapObjectField() { + HasObjectMap a = new HasObjectMap(); + a.map.put("date", new Date(0)); + assertEquals("{\"map\":{\"date\":\"Dec 31, 1969 4:00:00 PM\"}}", gson.toJson(a)); + } + + public class HasObjectMap { + Map<String, Object> map = new HashMap<String, Object>(); + } + + static final class Department { + public String name = "abc"; + public String code = "123"; + } + + static final class Product { + private List<String> attributes = new ArrayList<String>(); + private List<Department> departments = new ArrayList<Department>(); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java new file mode 100644 index 00000000..190603de --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java @@ -0,0 +1,503 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ParameterizedTypeFixtures.MyParameterizedType; +import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeAdapter; +import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeInstanceCreator; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Functional tests for the serialization and deserialization of parameterized types in Gson. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ParameterizedTypesTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testParameterizedTypesSerialization() throws Exception { + MyParameterizedType<Integer> src = new MyParameterizedType<Integer>(10); + Type typeOfSrc = new TypeToken<MyParameterizedType<Integer>>() {}.getType(); + String json = gson.toJson(src, typeOfSrc); + assertEquals(src.getExpectedJson(), json); + } + + public void testParameterizedTypeDeserialization() throws Exception { + BagOfPrimitives bag = new BagOfPrimitives(); + MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<BagOfPrimitives>(bag); + Type expectedType = new TypeToken<MyParameterizedType<BagOfPrimitives>>() {}.getType(); + BagOfPrimitives bagDefaultInstance = new BagOfPrimitives(); + Gson gson = new GsonBuilder().registerTypeAdapter( + expectedType, new MyParameterizedTypeInstanceCreator<BagOfPrimitives>(bagDefaultInstance)) + .create(); + + String json = expected.getExpectedJson(); + MyParameterizedType<BagOfPrimitives> actual = gson.fromJson(json, expectedType); + assertEquals(expected, actual); + } + + public void testTypesWithMultipleParametersSerialization() throws Exception { + MultiParameters<Integer, Float, Double, String, BagOfPrimitives> src = + new MultiParameters<Integer, Float, Double, String, BagOfPrimitives>(10, 1.0F, 2.1D, + "abc", new BagOfPrimitives()); + Type typeOfSrc = new TypeToken<MultiParameters<Integer, Float, Double, String, + BagOfPrimitives>>() {}.getType(); + String json = gson.toJson(src, typeOfSrc); + String expected = "{\"a\":10,\"b\":1.0,\"c\":2.1,\"d\":\"abc\"," + + "\"e\":{\"longValue\":0,\"intValue\":0,\"booleanValue\":false,\"stringValue\":\"\"}}"; + assertEquals(expected, json); + } + + public void testTypesWithMultipleParametersDeserialization() throws Exception { + Type typeOfTarget = new TypeToken<MultiParameters<Integer, Float, Double, String, + BagOfPrimitives>>() {}.getType(); + String json = "{\"a\":10,\"b\":1.0,\"c\":2.1,\"d\":\"abc\"," + + "\"e\":{\"longValue\":0,\"intValue\":0,\"booleanValue\":false,\"stringValue\":\"\"}}"; + MultiParameters<Integer, Float, Double, String, BagOfPrimitives> target = + gson.fromJson(json, typeOfTarget); + MultiParameters<Integer, Float, Double, String, BagOfPrimitives> expected = + new MultiParameters<Integer, Float, Double, String, BagOfPrimitives>(10, 1.0F, 2.1D, + "abc", new BagOfPrimitives()); + assertEquals(expected, target); + } + + public void testParameterizedTypeWithCustomSerializer() { + Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType(); + Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(ptIntegerType, new MyParameterizedTypeAdapter<Integer>()) + .registerTypeAdapter(ptStringType, new MyParameterizedTypeAdapter<String>()) + .create(); + MyParameterizedType<Integer> intTarget = new MyParameterizedType<Integer>(10); + String json = gson.toJson(intTarget, ptIntegerType); + assertEquals(MyParameterizedTypeAdapter.<Integer>getExpectedJson(intTarget), json); + + MyParameterizedType<String> stringTarget = new MyParameterizedType<String>("abc"); + json = gson.toJson(stringTarget, ptStringType); + assertEquals(MyParameterizedTypeAdapter.<String>getExpectedJson(stringTarget), json); + } + + public void testParameterizedTypesWithCustomDeserializer() { + Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType(); + Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType(); + Gson gson = new GsonBuilder().registerTypeAdapter( + ptIntegerType, new MyParameterizedTypeAdapter<Integer>()) + .registerTypeAdapter(ptStringType, new MyParameterizedTypeAdapter<String>()) + .registerTypeAdapter(ptStringType, new MyParameterizedTypeInstanceCreator<String>("")) + .registerTypeAdapter(ptIntegerType, + new MyParameterizedTypeInstanceCreator<Integer>(new Integer(0))) + .create(); + + MyParameterizedType<Integer> src = new MyParameterizedType<Integer>(10); + String json = MyParameterizedTypeAdapter.<Integer>getExpectedJson(src); + MyParameterizedType<Integer> intTarget = gson.fromJson(json, ptIntegerType); + assertEquals(10, intTarget.value.intValue()); + + MyParameterizedType<String> srcStr = new MyParameterizedType<String>("abc"); + json = MyParameterizedTypeAdapter.<String>getExpectedJson(srcStr); + MyParameterizedType<String> stringTarget = gson.fromJson(json, ptStringType); + assertEquals("abc", stringTarget.value); + } + + public void testParameterizedTypesWithWriterSerialization() throws Exception { + Writer writer = new StringWriter(); + MyParameterizedType<Integer> src = new MyParameterizedType<Integer>(10); + Type typeOfSrc = new TypeToken<MyParameterizedType<Integer>>() {}.getType(); + gson.toJson(src, typeOfSrc, writer); + assertEquals(src.getExpectedJson(), writer.toString()); + } + + public void testParameterizedTypeWithReaderDeserialization() throws Exception { + BagOfPrimitives bag = new BagOfPrimitives(); + MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<BagOfPrimitives>(bag); + Type expectedType = new TypeToken<MyParameterizedType<BagOfPrimitives>>() {}.getType(); + BagOfPrimitives bagDefaultInstance = new BagOfPrimitives(); + Gson gson = new GsonBuilder().registerTypeAdapter( + expectedType, new MyParameterizedTypeInstanceCreator<BagOfPrimitives>(bagDefaultInstance)) + .create(); + + Reader json = new StringReader(expected.getExpectedJson()); + MyParameterizedType<Integer> actual = gson.fromJson(json, expectedType); + assertEquals(expected, actual); + } + + @SuppressWarnings("unchecked") + public void testVariableTypeFieldsAndGenericArraysSerialization() throws Exception { + Integer obj = 0; + Integer[] array = { 1, 2, 3 }; + List<Integer> list = new ArrayList<Integer>(); + list.add(4); + list.add(5); + List<Integer>[] arrayOfLists = new List[] { list, list }; + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(obj, array, list, arrayOfLists, list, arrayOfLists); + String json = gson.toJson(objToSerialize, typeOfSrc); + + assertEquals(objToSerialize.getExpectedJson(), json); + } + + @SuppressWarnings("unchecked") + public void testVariableTypeFieldsAndGenericArraysDeserialization() throws Exception { + Integer obj = 0; + Integer[] array = { 1, 2, 3 }; + List<Integer> list = new ArrayList<Integer>(); + list.add(4); + list.add(5); + List<Integer>[] arrayOfLists = new List[] { list, list }; + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(obj, array, list, arrayOfLists, list, arrayOfLists); + String json = gson.toJson(objToSerialize, typeOfSrc); + ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc); + + assertEquals(objAfterDeserialization.getExpectedJson(), json); + } + + public void testVariableTypeDeserialization() throws Exception { + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(0, null, null, null, null, null); + String json = gson.toJson(objToSerialize, typeOfSrc); + ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc); + + assertEquals(objAfterDeserialization.getExpectedJson(), json); + } + + public void testVariableTypeArrayDeserialization() throws Exception { + Integer[] array = { 1, 2, 3 }; + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(null, array, null, null, null, null); + String json = gson.toJson(objToSerialize, typeOfSrc); + ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc); + + assertEquals(objAfterDeserialization.getExpectedJson(), json); + } + + public void testParameterizedTypeWithVariableTypeDeserialization() throws Exception { + List<Integer> list = new ArrayList<Integer>(); + list.add(4); + list.add(5); + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(null, null, list, null, null, null); + String json = gson.toJson(objToSerialize, typeOfSrc); + ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc); + + assertEquals(objAfterDeserialization.getExpectedJson(), json); + } + + @SuppressWarnings("unchecked") + public void testParameterizedTypeGenericArraysSerialization() throws Exception { + List<Integer> list = new ArrayList<Integer>(); + list.add(1); + list.add(2); + List<Integer>[] arrayOfLists = new List[] { list, list }; + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(null, null, null, arrayOfLists, null, null); + String json = gson.toJson(objToSerialize, typeOfSrc); + assertEquals("{\"arrayOfListOfTypeParameters\":[[1,2],[1,2]]}", json); + } + + @SuppressWarnings("unchecked") + public void testParameterizedTypeGenericArraysDeserialization() throws Exception { + List<Integer> list = new ArrayList<Integer>(); + list.add(1); + list.add(2); + List<Integer>[] arrayOfLists = new List[] { list, list }; + + Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType(); + ObjectWithTypeVariables<Integer> objToSerialize = + new ObjectWithTypeVariables<Integer>(null, null, null, arrayOfLists, null, null); + String json = gson.toJson(objToSerialize, typeOfSrc); + ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc); + + assertEquals(objAfterDeserialization.getExpectedJson(), json); + } + + /** + * An test object that has fields that are type variables. + * + * @param <T> Enforce T to be a string to make writing the "toExpectedJson" method easier. + */ + private static class ObjectWithTypeVariables<T extends Number> { + private final T typeParameterObj; + private final T[] typeParameterArray; + private final List<T> listOfTypeParameters; + private final List<T>[] arrayOfListOfTypeParameters; + private final List<? extends T> listOfWildcardTypeParameters; + private final List<? extends T>[] arrayOfListOfWildcardTypeParameters; + + // For use by Gson + @SuppressWarnings("unused") + private ObjectWithTypeVariables() { + this(null, null, null, null, null, null); + } + + public ObjectWithTypeVariables(T obj, T[] array, List<T> list, List<T>[] arrayOfList, + List<? extends T> wildcardList, List<? extends T>[] arrayOfWildcardList) { + this.typeParameterObj = obj; + this.typeParameterArray = array; + this.listOfTypeParameters = list; + this.arrayOfListOfTypeParameters = arrayOfList; + this.listOfWildcardTypeParameters = wildcardList; + this.arrayOfListOfWildcardTypeParameters = arrayOfWildcardList; + } + + public String getExpectedJson() { + StringBuilder sb = new StringBuilder().append("{"); + + boolean needsComma = false; + if (typeParameterObj != null) { + sb.append("\"typeParameterObj\":").append(toString(typeParameterObj)); + needsComma = true; + } + + if (typeParameterArray != null) { + if (needsComma) { + sb.append(','); + } + sb.append("\"typeParameterArray\":["); + appendObjectsToBuilder(sb, Arrays.asList(typeParameterArray)); + sb.append(']'); + needsComma = true; + } + + if (listOfTypeParameters != null) { + if (needsComma) { + sb.append(','); + } + sb.append("\"listOfTypeParameters\":["); + appendObjectsToBuilder(sb, listOfTypeParameters); + sb.append(']'); + needsComma = true; + } + + if (arrayOfListOfTypeParameters != null) { + if (needsComma) { + sb.append(','); + } + sb.append("\"arrayOfListOfTypeParameters\":["); + appendObjectsToBuilder(sb, arrayOfListOfTypeParameters); + sb.append(']'); + needsComma = true; + } + + if (listOfWildcardTypeParameters != null) { + if (needsComma) { + sb.append(','); + } + sb.append("\"listOfWildcardTypeParameters\":["); + appendObjectsToBuilder(sb, listOfWildcardTypeParameters); + sb.append(']'); + needsComma = true; + } + + if (arrayOfListOfWildcardTypeParameters != null) { + if (needsComma) { + sb.append(','); + } + sb.append("\"arrayOfListOfWildcardTypeParameters\":["); + appendObjectsToBuilder(sb, arrayOfListOfWildcardTypeParameters); + sb.append(']'); + needsComma = true; + } + sb.append('}'); + return sb.toString(); + } + + private void appendObjectsToBuilder(StringBuilder sb, Iterable<? extends T> iterable) { + boolean isFirst = true; + for (T obj : iterable) { + if (!isFirst) { + sb.append(','); + } + isFirst = false; + sb.append(toString(obj)); + } + } + + private void appendObjectsToBuilder(StringBuilder sb, List<? extends T>[] arrayOfList) { + boolean isFirst = true; + for (List<? extends T> list : arrayOfList) { + if (!isFirst) { + sb.append(','); + } + isFirst = false; + if (list != null) { + sb.append('['); + appendObjectsToBuilder(sb, list); + sb.append(']'); + } else { + sb.append("null"); + } + } + } + + public String toString(T obj) { + return obj.toString(); + } + } + + private static class MultiParameters<A, B, C, D, E> { + A a; + B b; + C c; + D d; + E e; + // For use by Gson + @SuppressWarnings("unused") + private MultiParameters() { + } + MultiParameters(A a, B b, C c, D d, E e) { + super(); + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((a == null) ? 0 : a.hashCode()); + result = prime * result + ((b == null) ? 0 : b.hashCode()); + result = prime * result + ((c == null) ? 0 : c.hashCode()); + result = prime * result + ((d == null) ? 0 : d.hashCode()); + result = prime * result + ((e == null) ? 0 : e.hashCode()); + return result; + } + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MultiParameters<A, B, C, D, E> other = (MultiParameters<A, B, C, D, E>) obj; + if (a == null) { + if (other.a != null) { + return false; + } + } else if (!a.equals(other.a)) { + return false; + } + if (b == null) { + if (other.b != null) { + return false; + } + } else if (!b.equals(other.b)) { + return false; + } + if (c == null) { + if (other.c != null) { + return false; + } + } else if (!c.equals(other.c)) { + return false; + } + if (d == null) { + if (other.d != null) { + return false; + } + } else if (!d.equals(other.d)) { + return false; + } + if (e == null) { + if (other.e != null) { + return false; + } + } else if (!e.equals(other.e)) { + return false; + } + return true; + } + } + + // Begin: tests to reproduce issue 103 + private static class Quantity { + @SuppressWarnings("unused") + int q = 10; + } + private static class MyQuantity extends Quantity { + @SuppressWarnings("unused") + int q2 = 20; + } + private interface Measurable<T> { + } + private interface Field<T> { + } + private interface Immutable { + } + + public static final class Amount<Q extends Quantity> + implements Measurable<Q>, Field<Amount<?>>, Serializable, Immutable { + private static final long serialVersionUID = -7560491093120970437L; + + int value = 30; + } + + public void testDeepParameterizedTypeSerialization() { + Amount<MyQuantity> amount = new Amount<MyQuantity>(); + String json = gson.toJson(amount); + assertTrue(json.contains("value")); + assertTrue(json.contains("30")); + } + + public void testDeepParameterizedTypeDeserialization() { + String json = "{value:30}"; + Type type = new TypeToken<Amount<MyQuantity>>() {}.getType(); + Amount<MyQuantity> amount = gson.fromJson(json, type); + assertEquals(30, amount.value); + } + // End: tests to reproduce issue 103 +} diff --git a/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java new file mode 100644 index 00000000..0aacc9e2 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java @@ -0,0 +1,123 @@ +/* + * 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.functional; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.common.TestTypes.ArrayOfObjects; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.reflect.TypeToken; + +/** + * Functional tests for pretty printing option. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class PrettyPrintingTest extends TestCase { + + private static final boolean DEBUG = false; + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new GsonBuilder().setPrettyPrinting().create(); + } + + public void testPrettyPrintList() { + BagOfPrimitives b = new BagOfPrimitives(); + List<BagOfPrimitives> listOfB = new LinkedList<BagOfPrimitives>(); + for (int i = 0; i < 15; ++i) { + listOfB.add(b); + } + Type typeOfSrc = new TypeToken<List<BagOfPrimitives>>() {}.getType(); + String json = gson.toJson(listOfB, typeOfSrc); + print(json); + } + + public void testPrettyPrintArrayOfObjects() { + ArrayOfObjects target = new ArrayOfObjects(); + String json = gson.toJson(target); + print(json); + } + + public void testPrettyPrintArrayOfPrimitives() { + int[] ints = new int[] { 1, 2, 3, 4, 5 }; + String json = gson.toJson(ints); + assertEquals("[\n 1,\n 2,\n 3,\n 4,\n 5\n]", json); + } + + public void testPrettyPrintArrayOfPrimitiveArrays() { + int[][] ints = new int[][] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 }, + { 9, 0 }, { 10 } }; + String json = gson.toJson(ints); + assertEquals("[\n [\n 1,\n 2\n ],\n [\n 3,\n 4\n ],\n [\n 5,\n 6\n ]," + + "\n [\n 7,\n 8\n ],\n [\n 9,\n 0\n ],\n [\n 10\n ]\n]", json); + } + + public void testPrettyPrintListOfPrimitiveArrays() { + List<Integer[]> list = Arrays.asList(new Integer[][] { { 1, 2 }, { 3, 4 }, + { 5, 6 }, { 7, 8 }, { 9, 0 }, { 10 } }); + String json = gson.toJson(list); + assertEquals("[\n [\n 1,\n 2\n ],\n [\n 3,\n 4\n ],\n [\n 5,\n 6\n ]," + + "\n [\n 7,\n 8\n ],\n [\n 9,\n 0\n ],\n [\n 10\n ]\n]", json); + } + + public void testMap() { + Map<String, Integer> map = new LinkedHashMap<String, Integer>(); + map.put("abc", 1); + map.put("def", 5); + String json = gson.toJson(map); + assertEquals("{\n \"abc\": 1,\n \"def\": 5\n}", json); + } + + // In response to bug 153 + public void testEmptyMapField() { + ClassWithMap obj = new ClassWithMap(); + obj.map = new LinkedHashMap<String, Integer>(); + String json = gson.toJson(obj); + assertTrue(json.contains("{\n \"map\": {},\n \"value\": 2\n}")); + } + + @SuppressWarnings("unused") + private static class ClassWithMap { + Map<String, Integer> map; + int value = 2; + } + + public void testMultipleArrays() { + int[][][] ints = new int[][][] { { { 1 }, { 2 } } }; + String json = gson.toJson(ints); + assertEquals("[\n [\n [\n 1\n ],\n [\n 2\n ]\n ]\n]", json); + } + + private void print(String msg) { + if (DEBUG) { + System.out.println(msg); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java new file mode 100644 index 00000000..69ff1f3f --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java @@ -0,0 +1,55 @@ +/* + * 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.functional; + +import junit.framework.TestCase; + +import com.google.gson.Gson; + +/** + * Functional tests for Java Character values. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class PrimitiveCharacterTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testPrimitiveCharacterAutoboxedSerialization() { + assertEquals("\"A\"", gson.toJson('A')); + assertEquals("\"A\"", gson.toJson('A', char.class)); + assertEquals("\"A\"", gson.toJson('A', Character.class)); + } + + public void testPrimitiveCharacterAutoboxedDeserialization() { + char expected = 'a'; + char actual = gson.fromJson("a", char.class); + assertEquals(expected, actual); + + actual = gson.fromJson("\"a\"", char.class); + assertEquals(expected, actual); + + actual = gson.fromJson("a", Character.class); + assertEquals(expected, actual); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java new file mode 100644 index 00000000..bb28ed1e --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java @@ -0,0 +1,821 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.LongSerializationPolicy; +import com.google.gson.reflect.TypeToken; +import java.io.Serializable; +import java.io.StringReader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; + +/** + * Functional tests for Json primitive values: integers, and floating point numbers. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class PrimitiveTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testPrimitiveIntegerAutoboxedSerialization() { + assertEquals("1", gson.toJson(1)); + } + + public void testPrimitiveIntegerAutoboxedDeserialization() { + int expected = 1; + int actual = gson.fromJson("1", int.class); + assertEquals(expected, actual); + + actual = gson.fromJson("1", Integer.class); + assertEquals(expected, actual); + } + + public void testByteSerialization() { + assertEquals("1", gson.toJson(1, byte.class)); + assertEquals("1", gson.toJson(1, Byte.class)); + } + + public void testShortSerialization() { + assertEquals("1", gson.toJson(1, short.class)); + assertEquals("1", gson.toJson(1, Short.class)); + } + + public void testByteDeserialization() { + Byte target = gson.fromJson("1", Byte.class); + assertEquals(1, (byte)target); + byte primitive = gson.fromJson("1", byte.class); + assertEquals(1, primitive); + } + + public void testPrimitiveIntegerAutoboxedInASingleElementArraySerialization() { + int target[] = {-9332}; + assertEquals("[-9332]", gson.toJson(target)); + assertEquals("[-9332]", gson.toJson(target, int[].class)); + assertEquals("[-9332]", gson.toJson(target, Integer[].class)); + } + + public void testReallyLongValuesSerialization() { + long value = 333961828784581L; + assertEquals("333961828784581", gson.toJson(value)); + } + + public void testReallyLongValuesDeserialization() { + String json = "333961828784581"; + long value = gson.fromJson(json, Long.class); + assertEquals(333961828784581L, value); + } + + public void testPrimitiveLongAutoboxedSerialization() { + assertEquals("1", gson.toJson(1L, long.class)); + assertEquals("1", gson.toJson(1L, Long.class)); + } + + public void testPrimitiveLongAutoboxedDeserialization() { + long expected = 1L; + long actual = gson.fromJson("1", long.class); + assertEquals(expected, actual); + + actual = gson.fromJson("1", Long.class); + assertEquals(expected, actual); + } + + public void testPrimitiveLongAutoboxedInASingleElementArraySerialization() { + long[] target = {-23L}; + assertEquals("[-23]", gson.toJson(target)); + assertEquals("[-23]", gson.toJson(target, long[].class)); + assertEquals("[-23]", gson.toJson(target, Long[].class)); + } + + public void testPrimitiveBooleanAutoboxedSerialization() { + assertEquals("true", gson.toJson(true)); + assertEquals("false", gson.toJson(false)); + } + + public void testBooleanDeserialization() { + boolean value = gson.fromJson("false", boolean.class); + assertEquals(false, value); + value = gson.fromJson("true", boolean.class); + assertEquals(true, value); + } + + public void testPrimitiveBooleanAutoboxedInASingleElementArraySerialization() { + boolean target[] = {false}; + assertEquals("[false]", gson.toJson(target)); + assertEquals("[false]", gson.toJson(target, boolean[].class)); + assertEquals("[false]", gson.toJson(target, Boolean[].class)); + } + + public void testNumberSerialization() { + Number expected = 1L; + String json = gson.toJson(expected); + assertEquals(expected.toString(), json); + + json = gson.toJson(expected, Number.class); + assertEquals(expected.toString(), json); + } + + public void testNumberDeserialization() { + String json = "1"; + Number expected = new Integer(json); + Number actual = gson.fromJson(json, Number.class); + assertEquals(expected.intValue(), actual.intValue()); + + json = String.valueOf(Long.MAX_VALUE); + expected = new Long(json); + actual = gson.fromJson(json, Number.class); + assertEquals(expected.longValue(), actual.longValue()); + + json = "1.0"; + actual = gson.fromJson(json, Number.class); + assertEquals(1L, actual.longValue()); + } + + public void testPrimitiveDoubleAutoboxedSerialization() { + assertEquals("-122.08234335", gson.toJson(-122.08234335)); + assertEquals("122.08112002", gson.toJson(new Double(122.08112002))); + } + + public void testPrimitiveDoubleAutoboxedDeserialization() { + double actual = gson.fromJson("-122.08858585", double.class); + assertEquals(-122.08858585, actual); + + actual = gson.fromJson("122.023900008000", Double.class); + assertEquals(122.023900008, actual); + } + + public void testPrimitiveDoubleAutoboxedInASingleElementArraySerialization() { + double[] target = {-122.08D}; + assertEquals("[-122.08]", gson.toJson(target)); + assertEquals("[-122.08]", gson.toJson(target, double[].class)); + assertEquals("[-122.08]", gson.toJson(target, Double[].class)); + } + + public void testDoubleAsStringRepresentationDeserialization() { + String doubleValue = "1.0043E+5"; + Double expected = Double.valueOf(doubleValue); + Double actual = gson.fromJson(doubleValue, Double.class); + assertEquals(expected, actual); + + double actual1 = gson.fromJson(doubleValue, double.class); + assertEquals(expected.doubleValue(), actual1); + } + + public void testDoubleNoFractAsStringRepresentationDeserialization() { + String doubleValue = "1E+5"; + Double expected = Double.valueOf(doubleValue); + Double actual = gson.fromJson(doubleValue, Double.class); + assertEquals(expected, actual); + + double actual1 = gson.fromJson(doubleValue, double.class); + assertEquals(expected.doubleValue(), actual1); + } + + public void testDoubleArrayDeserialization() { + String json = "[0.0, 0.004761904761904762, 3.4013606962703525E-4, 7.936508173034305E-4," + + "0.0011904761904761906, 0.0]"; + double[] values = gson.fromJson(json, double[].class); + assertEquals(6, values.length); + assertEquals(0.0, values[0]); + assertEquals(0.004761904761904762, values[1]); + assertEquals(3.4013606962703525E-4, values[2]); + assertEquals(7.936508173034305E-4, values[3]); + assertEquals(0.0011904761904761906, values[4]); + assertEquals(0.0, values[5]); + } + + public void testLargeDoubleDeserialization() { + String doubleValue = "1.234567899E8"; + Double expected = Double.valueOf(doubleValue); + Double actual = gson.fromJson(doubleValue, Double.class); + assertEquals(expected, actual); + + double actual1 = gson.fromJson(doubleValue, double.class); + assertEquals(expected.doubleValue(), actual1); + } + + public void testBigDecimalSerialization() { + BigDecimal target = new BigDecimal("-122.0e-21"); + String json = gson.toJson(target); + assertEquals(target, new BigDecimal(json)); + } + + public void testBigDecimalDeserialization() { + BigDecimal target = new BigDecimal("-122.0e-21"); + String json = "-122.0e-21"; + assertEquals(target, gson.fromJson(json, BigDecimal.class)); + } + + public void testBigDecimalInASingleElementArraySerialization() { + BigDecimal[] target = {new BigDecimal("-122.08e-21")}; + String json = gson.toJson(target); + String actual = extractElementFromArray(json); + assertEquals(target[0], new BigDecimal(actual)); + + json = gson.toJson(target, BigDecimal[].class); + actual = extractElementFromArray(json); + assertEquals(target[0], new BigDecimal(actual)); + } + + public void testSmallValueForBigDecimalSerialization() { + BigDecimal target = new BigDecimal("1.55"); + String actual = gson.toJson(target); + assertEquals(target.toString(), actual); + } + + public void testSmallValueForBigDecimalDeserialization() { + BigDecimal expected = new BigDecimal("1.55"); + BigDecimal actual = gson.fromJson("1.55", BigDecimal.class); + assertEquals(expected, actual); + } + + public void testBigDecimalPreservePrecisionSerialization() { + String expectedValue = "1.000"; + BigDecimal obj = new BigDecimal(expectedValue); + String actualValue = gson.toJson(obj); + + assertEquals(expectedValue, actualValue); + } + + public void testBigDecimalPreservePrecisionDeserialization() { + String json = "1.000"; + BigDecimal expected = new BigDecimal(json); + BigDecimal actual = gson.fromJson(json, BigDecimal.class); + + assertEquals(expected, actual); + } + + public void testBigDecimalAsStringRepresentationDeserialization() { + String doubleValue = "0.05E+5"; + BigDecimal expected = new BigDecimal(doubleValue); + BigDecimal actual = gson.fromJson(doubleValue, BigDecimal.class); + assertEquals(expected, actual); + } + + public void testBigDecimalNoFractAsStringRepresentationDeserialization() { + String doubleValue = "5E+5"; + BigDecimal expected = new BigDecimal(doubleValue); + BigDecimal actual = gson.fromJson(doubleValue, BigDecimal.class); + assertEquals(expected, actual); + } + + public void testBigIntegerSerialization() { + BigInteger target = new BigInteger("12121211243123245845384534687435634558945453489543985435"); + assertEquals(target.toString(), gson.toJson(target)); + } + + public void testBigIntegerDeserialization() { + String json = "12121211243123245845384534687435634558945453489543985435"; + BigInteger target = new BigInteger(json); + assertEquals(target, gson.fromJson(json, BigInteger.class)); + } + + public void testBigIntegerInASingleElementArraySerialization() { + BigInteger[] target = {new BigInteger("1212121243434324323254365345367456456456465464564564")}; + String json = gson.toJson(target); + String actual = extractElementFromArray(json); + assertEquals(target[0], new BigInteger(actual)); + + json = gson.toJson(target, BigInteger[].class); + actual = extractElementFromArray(json); + assertEquals(target[0], new BigInteger(actual)); + } + + public void testSmallValueForBigIntegerSerialization() { + BigInteger target = new BigInteger("15"); + String actual = gson.toJson(target); + assertEquals(target.toString(), actual); + } + + public void testSmallValueForBigIntegerDeserialization() { + BigInteger expected = new BigInteger("15"); + BigInteger actual = gson.fromJson("15", BigInteger.class); + assertEquals(expected, actual); + } + + public void testBadValueForBigIntegerDeserialization() { + try { + gson.fromJson("15.099", BigInteger.class); + fail("BigInteger can not be decimal values."); + } catch (JsonSyntaxException expected) { } + } + + public void testMoreSpecificSerialization() { + Gson gson = new Gson(); + String expected = "This is a string"; + String expectedJson = gson.toJson(expected); + + Serializable serializableString = expected; + String actualJson = gson.toJson(serializableString, Serializable.class); + assertFalse(expectedJson.equals(actualJson)); + } + + private String extractElementFromArray(String json) { + return json.substring(json.indexOf('[') + 1, json.indexOf(']')); + } + + public void testDoubleNaNSerializationNotSupportedByDefault() { + try { + double nan = Double.NaN; + gson.toJson(nan); + fail("Gson should not accept NaN for serialization"); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Double.NaN); + fail("Gson should not accept NaN for serialization"); + } catch (IllegalArgumentException expected) { + } + } + + public void testDoubleNaNSerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + double nan = Double.NaN; + assertEquals("NaN", gson.toJson(nan)); + assertEquals("NaN", gson.toJson(Double.NaN)); + } + + public void testDoubleNaNDeserialization() { + assertTrue(Double.isNaN(gson.fromJson("NaN", Double.class))); + assertTrue(Double.isNaN(gson.fromJson("NaN", double.class))); + } + + public void testFloatNaNSerializationNotSupportedByDefault() { + try { + float nan = Float.NaN; + gson.toJson(nan); + fail("Gson should not accept NaN for serialization"); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Float.NaN); + fail("Gson should not accept NaN for serialization"); + } catch (IllegalArgumentException expected) { + } + } + + public void testFloatNaNSerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + float nan = Float.NaN; + assertEquals("NaN", gson.toJson(nan)); + assertEquals("NaN", gson.toJson(Float.NaN)); + } + + public void testFloatNaNDeserialization() { + assertTrue(Float.isNaN(gson.fromJson("NaN", Float.class))); + assertTrue(Float.isNaN(gson.fromJson("NaN", float.class))); + } + + public void testBigDecimalNaNDeserializationNotSupported() { + try { + gson.fromJson("NaN", BigDecimal.class); + fail("Gson should not accept NaN for deserialization by default."); + } catch (JsonSyntaxException expected) { + } + } + + public void testDoubleInfinitySerializationNotSupportedByDefault() { + try { + double infinity = Double.POSITIVE_INFINITY; + gson.toJson(infinity); + fail("Gson should not accept positive infinity for serialization by default."); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Double.POSITIVE_INFINITY); + fail("Gson should not accept positive infinity for serialization by default."); + } catch (IllegalArgumentException expected) { + } + } + + public void testDoubleInfinitySerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + double infinity = Double.POSITIVE_INFINITY; + assertEquals("Infinity", gson.toJson(infinity)); + assertEquals("Infinity", gson.toJson(Double.POSITIVE_INFINITY)); + } + + public void testDoubleInfinityDeserialization() { + assertTrue(Double.isInfinite(gson.fromJson("Infinity", Double.class))); + assertTrue(Double.isInfinite(gson.fromJson("Infinity", double.class))); + } + + public void testFloatInfinitySerializationNotSupportedByDefault() { + try { + float infinity = Float.POSITIVE_INFINITY; + gson.toJson(infinity); + fail("Gson should not accept positive infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Float.POSITIVE_INFINITY); + fail("Gson should not accept positive infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + } + + public void testFloatInfinitySerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + float infinity = Float.POSITIVE_INFINITY; + assertEquals("Infinity", gson.toJson(infinity)); + assertEquals("Infinity", gson.toJson(Float.POSITIVE_INFINITY)); + } + + public void testFloatInfinityDeserialization() { + assertTrue(Float.isInfinite(gson.fromJson("Infinity", Float.class))); + assertTrue(Float.isInfinite(gson.fromJson("Infinity", float.class))); + } + + public void testBigDecimalInfinityDeserializationNotSupported() { + try { + gson.fromJson("Infinity", BigDecimal.class); + fail("Gson should not accept positive infinity for deserialization with BigDecimal"); + } catch (JsonSyntaxException expected) { + } + } + + public void testNegativeInfinitySerializationNotSupportedByDefault() { + try { + double negativeInfinity = Double.NEGATIVE_INFINITY; + gson.toJson(negativeInfinity); + fail("Gson should not accept negative infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Double.NEGATIVE_INFINITY); + fail("Gson should not accept negative infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + } + + public void testNegativeInfinitySerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + double negativeInfinity = Double.NEGATIVE_INFINITY; + assertEquals("-Infinity", gson.toJson(negativeInfinity)); + assertEquals("-Infinity", gson.toJson(Double.NEGATIVE_INFINITY)); + } + + public void testNegativeInfinityDeserialization() { + assertTrue(Double.isInfinite(gson.fromJson("-Infinity", double.class))); + assertTrue(Double.isInfinite(gson.fromJson("-Infinity", Double.class))); + } + + public void testNegativeInfinityFloatSerializationNotSupportedByDefault() { + try { + float negativeInfinity = Float.NEGATIVE_INFINITY; + gson.toJson(negativeInfinity); + fail("Gson should not accept negative infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + try { + gson.toJson(Float.NEGATIVE_INFINITY); + fail("Gson should not accept negative infinity for serialization by default"); + } catch (IllegalArgumentException expected) { + } + } + + public void testNegativeInfinityFloatSerialization() { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + float negativeInfinity = Float.NEGATIVE_INFINITY; + assertEquals("-Infinity", gson.toJson(negativeInfinity)); + assertEquals("-Infinity", gson.toJson(Float.NEGATIVE_INFINITY)); + } + + public void testNegativeInfinityFloatDeserialization() { + assertTrue(Float.isInfinite(gson.fromJson("-Infinity", float.class))); + assertTrue(Float.isInfinite(gson.fromJson("-Infinity", Float.class))); + } + + public void testBigDecimalNegativeInfinityDeserializationNotSupported() { + try { + gson.fromJson("-Infinity", BigDecimal.class); + fail("Gson should not accept positive infinity for deserialization"); + } catch (JsonSyntaxException expected) { + } + } + + public void testLongAsStringSerialization() throws Exception { + gson = new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create(); + String result = gson.toJson(15L); + assertEquals("\"15\"", result); + + // Test with an integer and ensure its still a number + result = gson.toJson(2); + assertEquals("2", result); + } + + public void testLongAsStringDeserialization() throws Exception { + long value = gson.fromJson("\"15\"", long.class); + assertEquals(15, value); + + gson = new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create(); + value = gson.fromJson("\"25\"", long.class); + assertEquals(25, value); + } + + public void testQuotedStringSerializationAndDeserialization() throws Exception { + String value = "String Blah Blah Blah...1, 2, 3"; + String serializedForm = gson.toJson(value); + assertEquals("\"" + value + "\"", serializedForm); + + String actual = gson.fromJson(serializedForm, String.class); + assertEquals(value, actual); + } + + public void testUnquotedStringDeserializationFails() throws Exception { + assertEquals("UnquotedSingleWord", gson.fromJson("UnquotedSingleWord", String.class)); + + String value = "String Blah Blah Blah...1, 2, 3"; + try { + gson.fromJson(value, String.class); + fail(); + } catch (JsonSyntaxException expected) { } + } + + public void testHtmlCharacterSerialization() throws Exception { + String target = "<script>var a = 12;</script>"; + String result = gson.toJson(target); + assertFalse(result.equals('"' + target + '"')); + + gson = new GsonBuilder().disableHtmlEscaping().create(); + result = gson.toJson(target); + assertTrue(result.equals('"' + target + '"')); + } + + public void testDeserializePrimitiveWrapperAsObjectField() { + String json = "{i:10}"; + ClassWithIntegerField target = gson.fromJson(json, ClassWithIntegerField.class); + assertEquals(10, target.i.intValue()); + } + + private static class ClassWithIntegerField { + Integer i; + } + + public void testPrimitiveClassLiteral() { + assertEquals(1, gson.fromJson("1", int.class).intValue()); + assertEquals(1, gson.fromJson(new StringReader("1"), int.class).intValue()); + assertEquals(1, gson.fromJson(new JsonPrimitive(1), int.class).intValue()); + } + + public void testDeserializeJsonObjectAsLongPrimitive() { + try { + gson.fromJson("{'abc':1}", long.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsLongWrapper() { + try { + gson.fromJson("[1,2,3]", Long.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsInt() { + try { + gson.fromJson("[1, 2, 3, 4]", int.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsInteger() { + try { + gson.fromJson("{}", Integer.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsShortPrimitive() { + try { + gson.fromJson("{'abc':1}", short.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsShortWrapper() { + try { + gson.fromJson("['a','b']", Short.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsDoublePrimitive() { + try { + gson.fromJson("[1,2]", double.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsDoubleWrapper() { + try { + gson.fromJson("{'abc':1}", Double.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsFloatPrimitive() { + try { + gson.fromJson("{'abc':1}", float.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsFloatWrapper() { + try { + gson.fromJson("[1,2,3]", Float.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsBytePrimitive() { + try { + gson.fromJson("{'abc':1}", byte.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsByteWrapper() { + try { + gson.fromJson("[1,2,3,4]", Byte.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsBooleanPrimitive() { + try { + gson.fromJson("{'abc':1}", boolean.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsBooleanWrapper() { + try { + gson.fromJson("[1,2,3,4]", Boolean.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsBigDecimal() { + try { + gson.fromJson("[1,2,3,4]", BigDecimal.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsBigDecimal() { + try { + gson.fromJson("{'a':1}", BigDecimal.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsBigInteger() { + try { + gson.fromJson("[1,2,3,4]", BigInteger.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsBigInteger() { + try { + gson.fromJson("{'c':2}", BigInteger.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonArrayAsNumber() { + try { + gson.fromJson("[1,2,3,4]", Number.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializeJsonObjectAsNumber() { + try { + gson.fromJson("{'c':2}", Number.class); + fail(); + } catch (JsonSyntaxException expected) {} + } + + public void testDeserializingDecimalPointValueZeroSucceeds() { + assertEquals(1, (int) gson.fromJson("1.0", Integer.class)); + } + + public void testDeserializingNonZeroDecimalPointValuesAsIntegerFails() { + try { + gson.fromJson("1.02", Byte.class); + fail(); + } catch (JsonSyntaxException expected) { + } + try { + gson.fromJson("1.02", Short.class); + fail(); + } catch (JsonSyntaxException expected) { + } + try { + gson.fromJson("1.02", Integer.class); + fail(); + } catch (JsonSyntaxException expected) { + } + try { + gson.fromJson("1.02", Long.class); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testDeserializingBigDecimalAsIntegerFails() { + try { + gson.fromJson("-122.08e-213", Integer.class); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testDeserializingBigIntegerAsInteger() { + try { + gson.fromJson("12121211243123245845384534687435634558945453489543985435", Integer.class); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testDeserializingBigIntegerAsLong() { + try { + gson.fromJson("12121211243123245845384534687435634558945453489543985435", Long.class); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testValueVeryCloseToZeroIsZero() { + assertEquals(0, (byte) gson.fromJson("-122.08e-2132", byte.class)); + assertEquals(0, (short) gson.fromJson("-122.08e-2132", short.class)); + assertEquals(0, (int) gson.fromJson("-122.08e-2132", int.class)); + assertEquals(0, (long) gson.fromJson("-122.08e-2132", long.class)); + assertEquals(-0.0f, gson.fromJson("-122.08e-2132", float.class)); + assertEquals(-0.0, gson.fromJson("-122.08e-2132", double.class)); + assertEquals(0.0f, gson.fromJson("122.08e-2132", float.class)); + assertEquals(0.0, gson.fromJson("122.08e-2132", double.class)); + } + + public void testDeserializingBigDecimalAsFloat() { + String json = "-122.08e-2132332"; + float actual = gson.fromJson(json, float.class); + assertEquals(-0.0f, actual); + } + + public void testDeserializingBigDecimalAsDouble() { + String json = "-122.08e-2132332"; + double actual = gson.fromJson(json, double.class); + assertEquals(-0.0d, actual); + } + + public void testDeserializingBigDecimalAsBigIntegerFails() { + try { + gson.fromJson("-122.08e-213", BigInteger.class); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testDeserializingBigIntegerAsBigDecimal() { + BigDecimal actual = + gson.fromJson("12121211243123245845384534687435634558945453489543985435", BigDecimal.class); + assertEquals("12121211243123245845384534687435634558945453489543985435", actual.toPlainString()); + } + + public void testStringsAsBooleans() { + String json = "['true', 'false', 'TRUE', 'yes', '1']"; + assertEquals(Arrays.asList(true, false, true, false, false), + gson.<List<Boolean>>fromJson(json, new TypeToken<List<Boolean>>() {}.getType())); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java b/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java new file mode 100644 index 00000000..7dcbc23c --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java @@ -0,0 +1,84 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.ClassWithTransientFields; +import com.google.gson.common.TestTypes.Nested; +import com.google.gson.common.TestTypes.PrimitiveArray; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +/** + * Functional tests for print formatting. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class PrintFormattingTest extends TestCase { + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void testCompactFormattingLeavesNoWhiteSpace() { + List list = new ArrayList(); + list.add(new BagOfPrimitives()); + list.add(new Nested()); + list.add(new PrimitiveArray()); + list.add(new ClassWithTransientFields()); + + String json = gson.toJson(list); + assertContainsNoWhiteSpace(json); + } + + public void testJsonObjectWithNullValues() { + JsonObject obj = new JsonObject(); + obj.addProperty("field1", "value1"); + obj.addProperty("field2", (String) null); + String json = gson.toJson(obj); + assertTrue(json.contains("field1")); + assertFalse(json.contains("field2")); + } + + public void testJsonObjectWithNullValuesSerialized() { + gson = new GsonBuilder().serializeNulls().create(); + JsonObject obj = new JsonObject(); + obj.addProperty("field1", "value1"); + obj.addProperty("field2", (String) null); + String json = gson.toJson(obj); + assertTrue(json.contains("field1")); + assertTrue(json.contains("field2")); + } + + private static void assertContainsNoWhiteSpace(String str) { + for (char c : str.toCharArray()) { + assertFalse(Character.isWhitespace(c)); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java b/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java new file mode 100644 index 00000000..d5e8883e --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java @@ -0,0 +1,101 @@ +/* + * 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.functional; + +import java.util.Arrays; +import java.util.Collection; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * Unit tests to validate serialization of parameterized types without explicit types + * + * @author Inderjeet Singh + */ +public class RawSerializationTest extends TestCase { + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testCollectionOfPrimitives() { + Collection<Integer> ints = Arrays.asList(1, 2, 3, 4, 5); + String json = gson.toJson(ints); + assertEquals("[1,2,3,4,5]", json); + } + + public void testCollectionOfObjects() { + Collection<Foo> foos = Arrays.asList(new Foo(1), new Foo(2)); + String json = gson.toJson(foos); + assertEquals("[{\"b\":1},{\"b\":2}]", json); + } + + public void testParameterizedObject() { + Bar<Foo> bar = new Bar<Foo>(new Foo(1)); + String expectedJson = "{\"t\":{\"b\":1}}"; + // Ensure that serialization works without specifying the type explicitly + String json = gson.toJson(bar); + assertEquals(expectedJson, json); + // Ensure that serialization also works when the type is specified explicitly + json = gson.toJson(bar, new TypeToken<Bar<Foo>>(){}.getType()); + assertEquals(expectedJson, json); + } + + public void testTwoLevelParameterizedObject() { + Bar<Bar<Foo>> bar = new Bar<Bar<Foo>>(new Bar<Foo>(new Foo(1))); + String expectedJson = "{\"t\":{\"t\":{\"b\":1}}}"; + // Ensure that serialization works without specifying the type explicitly + String json = gson.toJson(bar); + assertEquals(expectedJson, json); + // Ensure that serialization also works when the type is specified explicitly + json = gson.toJson(bar, new TypeToken<Bar<Bar<Foo>>>(){}.getType()); + assertEquals(expectedJson, json); + } + + public void testThreeLevelParameterizedObject() { + Bar<Bar<Bar<Foo>>> bar = new Bar<Bar<Bar<Foo>>>(new Bar<Bar<Foo>>(new Bar<Foo>(new Foo(1)))); + String expectedJson = "{\"t\":{\"t\":{\"t\":{\"b\":1}}}}"; + // Ensure that serialization works without specifying the type explicitly + String json = gson.toJson(bar); + assertEquals(expectedJson, json); + // Ensure that serialization also works when the type is specified explicitly + json = gson.toJson(bar, new TypeToken<Bar<Bar<Bar<Foo>>>>(){}.getType()); + assertEquals(expectedJson, json); + } + + private static class Foo { + @SuppressWarnings("unused") + int b; + Foo(int b) { + this.b = b; + } + } + + private static class Bar<T> { + @SuppressWarnings("unused") + T t; + Bar(T t) { + this.t = t; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java b/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java new file mode 100644 index 00000000..e21fb903 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java @@ -0,0 +1,135 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonStreamParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.common.TestTypes.BagOfPrimitives; + +import com.google.gson.reflect.TypeToken; +import java.util.Map; +import junit.framework.TestCase; + +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * Functional tests for the support of {@link Reader}s and {@link Writer}s. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class ReadersWritersTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testWriterForSerialization() throws Exception { + Writer writer = new StringWriter(); + BagOfPrimitives src = new BagOfPrimitives(); + gson.toJson(src, writer); + assertEquals(src.getExpectedJson(), writer.toString()); + } + + public void testReaderForDeserialization() throws Exception { + BagOfPrimitives expected = new BagOfPrimitives(); + Reader json = new StringReader(expected.getExpectedJson()); + BagOfPrimitives actual = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(expected, actual); + } + + public void testTopLevelNullObjectSerializationWithWriter() { + StringWriter writer = new StringWriter(); + gson.toJson(null, writer); + assertEquals("null", writer.toString()); + } + + public void testTopLevelNullObjectDeserializationWithReader() { + StringReader reader = new StringReader("null"); + Integer nullIntObject = gson.fromJson(reader, Integer.class); + assertNull(nullIntObject); + } + + public void testTopLevelNullObjectSerializationWithWriterAndSerializeNulls() { + Gson gson = new GsonBuilder().serializeNulls().create(); + StringWriter writer = new StringWriter(); + gson.toJson(null, writer); + assertEquals("null", writer.toString()); + } + + public void testTopLevelNullObjectDeserializationWithReaderAndSerializeNulls() { + Gson gson = new GsonBuilder().serializeNulls().create(); + StringReader reader = new StringReader("null"); + Integer nullIntObject = gson.fromJson(reader, Integer.class); + assertNull(nullIntObject); + } + + public void testReadWriteTwoStrings() throws IOException { + Gson gson= new Gson(); + CharArrayWriter writer= new CharArrayWriter(); + writer.write(gson.toJson("one").toCharArray()); + writer.write(gson.toJson("two").toCharArray()); + CharArrayReader reader = new CharArrayReader(writer.toCharArray()); + JsonStreamParser parser = new JsonStreamParser(reader); + String actualOne = gson.fromJson(parser.next(), String.class); + assertEquals("one", actualOne); + String actualTwo = gson.fromJson(parser.next(), String.class); + assertEquals("two", actualTwo); + } + + public void testReadWriteTwoObjects() throws IOException { + Gson gson= new Gson(); + CharArrayWriter writer= new CharArrayWriter(); + BagOfPrimitives expectedOne = new BagOfPrimitives(1, 1, true, "one"); + writer.write(gson.toJson(expectedOne).toCharArray()); + BagOfPrimitives expectedTwo = new BagOfPrimitives(2, 2, false, "two"); + writer.write(gson.toJson(expectedTwo).toCharArray()); + CharArrayReader reader = new CharArrayReader(writer.toCharArray()); + JsonStreamParser parser = new JsonStreamParser(reader); + BagOfPrimitives actualOne = gson.fromJson(parser.next(), BagOfPrimitives.class); + assertEquals("one", actualOne.stringValue); + BagOfPrimitives actualTwo = gson.fromJson(parser.next(), BagOfPrimitives.class); + assertEquals("two", actualTwo.stringValue); + assertFalse(parser.hasNext()); + } + + public void testTypeMismatchThrowsJsonSyntaxExceptionForStrings() { + try { + gson.fromJson("true", new TypeToken<Map<String, String>>() {}.getType()); + fail(); + } catch (JsonSyntaxException expected) { + } + } + + public void testTypeMismatchThrowsJsonSyntaxExceptionForReaders() { + try { + gson.fromJson(new StringReader("true"), new TypeToken<Map<String, String>>() {}.getType()); + fail(); + } catch (JsonSyntaxException expected) { + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java new file mode 100644 index 00000000..c3b0898d --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java @@ -0,0 +1,205 @@ +/* + * 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.functional; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Functional tests for the RuntimeTypeAdapterFactory feature in extras. + */ +public final class RuntimeTypeAdapterFactoryFunctionalTest extends TestCase { + + private final Gson gson = new Gson(); + + /** + * This test also ensures that {@link TypeAdapterFactory} registered through {@link JsonAdapter} + * work correctly for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)}. + */ + public void testSubclassesAutomaticallySerialzed() throws Exception { + Shape shape = new Circle(25); + String json = gson.toJson(shape); + shape = gson.fromJson(json, Shape.class); + assertEquals(25, ((Circle)shape).radius); + + shape = new Square(15); + json = gson.toJson(shape); + shape = gson.fromJson(json, Shape.class); + assertEquals(15, ((Square)shape).side); + assertEquals(ShapeType.SQUARE, shape.type); + } + + @JsonAdapter(Shape.JsonAdapterFactory.class) + static class Shape { + final ShapeType type; + Shape(ShapeType type) { this.type = type; } + private static final class JsonAdapterFactory extends RuntimeTypeAdapterFactory<Shape> { + public JsonAdapterFactory() { + super(Shape.class, "type"); + registerSubtype(Circle.class, ShapeType.CIRCLE.toString()); + registerSubtype(Square.class, ShapeType.SQUARE.toString()); + } + } + } + + public enum ShapeType { + SQUARE, CIRCLE + } + + private static final class Circle extends Shape { + final int radius; + Circle(int radius) { super(ShapeType.CIRCLE); this.radius = radius; } + } + + private static final class Square extends Shape { + final int side; + Square(int side) { super(ShapeType.SQUARE); this.side = side; } + } + + // Copied from the extras package + static class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { + private final Class<?> baseType; + private final String typeFieldName; + private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>(); + private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>(); + + protected RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { + return new RuntimeTypeAdapterFactory<T>(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { + return registerSubtype(type, type.getSimpleName()); + } + + public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map<String, TypeAdapter<?>> labelToDelegate + = new LinkedHashMap<String, TypeAdapter<?>>(); + final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate + = new LinkedHashMap<Class<?>, TypeAdapter<?>>(); + for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { + TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter<R>() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class<?> srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (!jsonObject.has(typeFieldName)) { + JsonObject clone = new JsonObject(); + clone.add(typeFieldName, new JsonPrimitive(label)); + for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonObject = clone; + } + Streams.write(jsonObject, out); + } + }; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/SecurityTest.java b/gson/src/test/java/com/google/gson/functional/SecurityTest.java new file mode 100644 index 00000000..aa1c2d45 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/SecurityTest.java @@ -0,0 +1,86 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.common.TestTypes.BagOfPrimitives; + +import junit.framework.TestCase; + +/** + * Tests for security-related aspects of Gson + * + * @author Inderjeet Singh + */ +public class SecurityTest extends TestCase { + /** + * Keep this in sync with Gson.JSON_NON_EXECUTABLE_PREFIX + */ + private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n"; + + private GsonBuilder gsonBuilder; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gsonBuilder = new GsonBuilder(); + } + + public void testNonExecutableJsonSerialization() { + Gson gson = gsonBuilder.generateNonExecutableJson().create(); + String json = gson.toJson(new BagOfPrimitives()); + assertTrue(json.startsWith(JSON_NON_EXECUTABLE_PREFIX)); + } + + public void testNonExecutableJsonDeserialization() { + String json = JSON_NON_EXECUTABLE_PREFIX + "{longValue:1}"; + Gson gson = gsonBuilder.create(); + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(1, target.longValue); + } + + public void testJsonWithNonExectuableTokenSerialization() { + Gson gson = gsonBuilder.generateNonExecutableJson().create(); + String json = gson.toJson(JSON_NON_EXECUTABLE_PREFIX); + assertTrue(json.contains(")]}'\n")); + } + + /** + * Gson should be able to deserialize a stream with non-exectuable token even if it is created + * without {@link GsonBuilder#generateNonExecutableJson()}. + */ + public void testJsonWithNonExectuableTokenWithRegularGsonDeserialization() { + Gson gson = gsonBuilder.create(); + String json = JSON_NON_EXECUTABLE_PREFIX + "{stringValue:')]}\\u0027\\n'}"; + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(")]}'\n", target.stringValue); + } + + /** + * Gson should be able to deserialize a stream with non-exectuable token if it is created + * with {@link GsonBuilder#generateNonExecutableJson()}. + */ + public void testJsonWithNonExectuableTokenWithConfiguredGsonDeserialization() { + // Gson should be able to deserialize a stream with non-exectuable token even if it is created + Gson gson = gsonBuilder.generateNonExecutableJson().create(); + String json = JSON_NON_EXECUTABLE_PREFIX + "{intValue:2,stringValue:')]}\\u0027\\n'}"; + BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(")]}'\n", target.stringValue); + assertEquals(2, target.intValue); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java b/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java new file mode 100644 index 00000000..38ad8242 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 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.functional; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import junit.framework.TestCase; + +public final class SerializedNameTest extends TestCase { + private final Gson gson = new Gson(); + + public void testFirstNameIsChosenForSerialization() { + MyClass target = new MyClass("v1", "v2"); + // Ensure name1 occurs exactly once, and name2 and name3 dont appear + assertEquals("{\"name\":\"v1\",\"name1\":\"v2\"}", gson.toJson(target)); + } + + public void testMultipleNamesDeserializedCorrectly() { + assertEquals("v1", gson.fromJson("{'name':'v1'}", MyClass.class).a); + + // Both name1 and name2 gets deserialized to b + assertEquals("v11", gson.fromJson("{'name1':'v11'}", MyClass.class).b); + assertEquals("v2", gson.fromJson("{'name2':'v2'}", MyClass.class).b); + assertEquals("v3", gson.fromJson("{'name3':'v3'}", MyClass.class).b); + } + + public void testMultipleNamesInTheSameString() { + // The last value takes precedence + assertEquals("v3", gson.fromJson("{'name1':'v1','name2':'v2','name3':'v3'}", MyClass.class).b); + } + + private static final class MyClass { + @SerializedName("name") String a; + @SerializedName(value="name1", alternate={"name2", "name3"}) String b; + MyClass(String a, String b) { + this.a = a; + this.b = b; + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java new file mode 100644 index 00000000..551ceffc --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java @@ -0,0 +1,261 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +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.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import junit.framework.TestCase; + +public final class StreamingTypeAdaptersTest extends TestCase { + private Gson miniGson = new GsonBuilder().create(); + private TypeAdapter<Truck> truckAdapter = miniGson.getAdapter(Truck.class); + private TypeAdapter<Map<String, Double>> mapAdapter + = miniGson.getAdapter(new TypeToken<Map<String, Double>>() {}); + + public void testSerialize() throws IOException { + Truck truck = new Truck(); + truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)); + truck.horsePower = 300; + + assertEquals("{'horsePower':300.0," + + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}", + toJson(truckAdapter, truck).replace('\"', '\'')); + } + + public void testDeserialize() throws IOException { + String json = "{'horsePower':300.0," + + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}"; + Truck truck = fromJson(truckAdapter, json); + assertEquals(300.0, truck.horsePower); + assertEquals(Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)), truck.passengers); + } + + public void testSerializeNullField() throws IOException { + Truck truck = new Truck(); + truck.passengers = null; + assertEquals("{'horsePower':0.0,'passengers':null}", + toJson(truckAdapter, truck).replace('\"', '\'')); + } + + public void testDeserializeNullField() throws IOException { + Truck truck = fromJson(truckAdapter, "{'horsePower':0.0,'passengers':null}"); + assertNull(truck.passengers); + } + + public void testSerializeNullObject() throws IOException { + Truck truck = new Truck(); + truck.passengers = Arrays.asList((Person) null); + assertEquals("{'horsePower':0.0,'passengers':[null]}", + toJson(truckAdapter, truck).replace('\"', '\'')); + } + + public void testDeserializeNullObject() throws IOException { + Truck truck = fromJson(truckAdapter, "{'horsePower':0.0,'passengers':[null]}"); + assertEquals(Arrays.asList((Person) null), truck.passengers); + } + + public void testSerializeWithCustomTypeAdapter() throws IOException { + usePersonNameAdapter(); + Truck truck = new Truck(); + truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)); + assertEquals("{'horsePower':0.0,'passengers':['Jesse','Jodie']}", + toJson(truckAdapter, truck).replace('\"', '\'')); + } + + public void testDeserializeWithCustomTypeAdapter() throws IOException { + usePersonNameAdapter(); + Truck truck = fromJson(truckAdapter, "{'horsePower':0.0,'passengers':['Jesse','Jodie']}"); + assertEquals(Arrays.asList(new Person("Jesse", -1), new Person("Jodie", -1)), truck.passengers); + } + + private void usePersonNameAdapter() { + TypeAdapter<Person> personNameAdapter = new TypeAdapter<Person>() { + @Override public Person read(JsonReader in) throws IOException { + String name = in.nextString(); + return new Person(name, -1); + } + @Override public void write(JsonWriter out, Person value) throws IOException { + out.value(value.name); + } + }; + miniGson = new GsonBuilder().registerTypeAdapter(Person.class, personNameAdapter).create(); + truckAdapter = miniGson.getAdapter(Truck.class); + } + + public void testSerializeMap() throws IOException { + Map<String, Double> map = new LinkedHashMap<String, Double>(); + map.put("a", 5.0); + map.put("b", 10.0); + assertEquals("{'a':5.0,'b':10.0}", toJson(mapAdapter, map).replace('"', '\'')); + } + + public void testDeserializeMap() throws IOException { + Map<String, Double> map = new LinkedHashMap<String, Double>(); + map.put("a", 5.0); + map.put("b", 10.0); + assertEquals(map, fromJson(mapAdapter, "{'a':5.0,'b':10.0}")); + } + + public void testSerialize1dArray() throws IOException { + TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {}); + assertEquals("[1.0,2.0,3.0]", toJson(arrayAdapter, new double[]{1.0, 2.0, 3.0})); + } + + public void testDeserialize1dArray() throws IOException { + TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {}); + double[] array = fromJson(arrayAdapter, "[1.0,2.0,3.0]"); + assertTrue(Arrays.toString(array), Arrays.equals(new double[]{1.0, 2.0, 3.0}, array)); + } + + public void testSerialize2dArray() throws IOException { + TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {}); + double[][] array = { {1.0, 2.0 }, { 3.0 } }; + assertEquals("[[1.0,2.0],[3.0]]", toJson(arrayAdapter, array)); + } + + public void testDeserialize2dArray() throws IOException { + TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {}); + double[][] array = fromJson(arrayAdapter, "[[1.0,2.0],[3.0]]"); + double[][] expected = { {1.0, 2.0 }, { 3.0 } }; + assertTrue(Arrays.toString(array), Arrays.deepEquals(expected, array)); + } + + public void testNullSafe() { + TypeAdapter<Person> typeAdapter = new TypeAdapter<Person>() { + @Override public Person read(JsonReader in) throws IOException { + String[] values = in.nextString().split(","); + return new Person(values[0], Integer.parseInt(values[1])); + } + public void write(JsonWriter out, Person person) throws IOException { + out.value(person.name + "," + person.age); + } + }; + Gson gson = new GsonBuilder().registerTypeAdapter( + Person.class, typeAdapter).create(); + Truck truck = new Truck(); + truck.horsePower = 1.0D; + truck.passengers = new ArrayList<Person>(); + truck.passengers.add(null); + truck.passengers.add(new Person("jesse", 30)); + try { + gson.toJson(truck, Truck.class); + fail(); + } catch (NullPointerException expected) {} + String json = "{horsePower:1.0,passengers:[null,'jesse,30']}"; + try { + gson.fromJson(json, Truck.class); + fail(); + } catch (JsonSyntaxException expected) {} + gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter.nullSafe()).create(); + assertEquals("{\"horsePower\":1.0,\"passengers\":[null,\"jesse,30\"]}", + gson.toJson(truck, Truck.class)); + truck = gson.fromJson(json, Truck.class); + assertEquals(1.0D, truck.horsePower); + assertNull(truck.passengers.get(0)); + assertEquals("jesse", truck.passengers.get(1).name); + } + + public void testSerializeRecursive() throws IOException { + TypeAdapter<Node> nodeAdapter = miniGson.getAdapter(Node.class); + Node root = new Node("root"); + root.left = new Node("left"); + root.right = new Node("right"); + assertEquals("{'label':'root'," + + "'left':{'label':'left','left':null,'right':null}," + + "'right':{'label':'right','left':null,'right':null}}", + toJson(nodeAdapter, root).replace('"', '\'')); + } + + public void testFromJsonTree() { + JsonObject truckObject = new JsonObject(); + truckObject.add("horsePower", new JsonPrimitive(300)); + JsonArray passengersArray = new JsonArray(); + JsonObject jesseObject = new JsonObject(); + jesseObject.add("age", new JsonPrimitive(30)); + jesseObject.add("name", new JsonPrimitive("Jesse")); + passengersArray.add(jesseObject); + truckObject.add("passengers", passengersArray); + + Truck truck = truckAdapter.fromJsonTree(truckObject); + assertEquals(300.0, truck.horsePower); + assertEquals(Arrays.asList(new Person("Jesse", 30)), truck.passengers); + } + + static class Truck { + double horsePower; + List<Person> passengers = Collections.emptyList(); + } + + static class Person { + int age; + String name; + Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override public boolean equals(Object o) { + return o instanceof Person + && ((Person) o).name.equals(name) + && ((Person) o).age == age; + } + @Override public int hashCode() { + return name.hashCode() ^ age; + } + } + + static class Node { + String label; + Node left; + Node right; + Node(String label) { + this.label = label; + } + } + + // TODO: remove this when TypeAdapter.toJson() is public + private static <T> String toJson(TypeAdapter<T> typeAdapter, T value) throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + typeAdapter.write(writer, value); + return stringWriter.toString(); + } + + // TODO: remove this when TypeAdapter.fromJson() is public + private <T> T fromJson(TypeAdapter<T> typeAdapter, String json) throws IOException { + JsonReader reader = new JsonReader(new StringReader(json)); + reader.setLenient(true); // TODO: non-lenient? + return typeAdapter.read(reader); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/StringTest.java b/gson/src/test/java/com/google/gson/functional/StringTest.java new file mode 100644 index 00000000..7dcf6f0f --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/StringTest.java @@ -0,0 +1,140 @@ +package com.google.gson.functional; + +import com.google.gson.Gson; + +import junit.framework.TestCase; + +/** + * Functional tests for Json serialization and deserialization of strings. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class StringTest extends TestCase { + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testStringValueSerialization() throws Exception { + String value = "someRandomStringValue"; + assertEquals('"' + value + '"', gson.toJson(value)); + } + + public void testStringValueDeserialization() throws Exception { + String value = "someRandomStringValue"; + String actual = gson.fromJson("\"" + value + "\"", String.class); + assertEquals(value, actual); + } + + public void testSingleQuoteInStringSerialization() throws Exception { + String valueWithQuotes = "beforeQuote'afterQuote"; + String jsonRepresentation = gson.toJson(valueWithQuotes); + assertEquals(valueWithQuotes, gson.fromJson(jsonRepresentation, String.class)); + } + + public void testEscapedCtrlNInStringSerialization() throws Exception { + String value = "a\nb"; + String json = gson.toJson(value); + assertEquals("\"a\\nb\"", json); + } + + public void testEscapedCtrlNInStringDeserialization() throws Exception { + String json = "'a\\nb'"; + String actual = gson.fromJson(json, String.class); + assertEquals("a\nb", actual); + } + + public void testEscapedCtrlRInStringSerialization() throws Exception { + String value = "a\rb"; + String json = gson.toJson(value); + assertEquals("\"a\\rb\"", json); + } + + public void testEscapedCtrlRInStringDeserialization() throws Exception { + String json = "'a\\rb'"; + String actual = gson.fromJson(json, String.class); + assertEquals("a\rb", actual); + } + + public void testEscapedBackslashInStringSerialization() throws Exception { + String value = "a\\b"; + String json = gson.toJson(value); + assertEquals("\"a\\\\b\"", json); + } + + public void testEscapedBackslashInStringDeserialization() throws Exception { + String actual = gson.fromJson("'a\\\\b'", String.class); + assertEquals("a\\b", actual); + } + + public void testSingleQuoteInStringDeserialization() throws Exception { + String value = "beforeQuote'afterQuote"; + String actual = gson.fromJson("\"" + value + "\"", String.class); + assertEquals(value, actual); + } + + public void testEscapingQuotesInStringSerialization() throws Exception { + String valueWithQuotes = "beforeQuote\"afterQuote"; + String jsonRepresentation = gson.toJson(valueWithQuotes); + String target = gson.fromJson(jsonRepresentation, String.class); + assertEquals(valueWithQuotes, target); + } + + public void testEscapingQuotesInStringDeserialization() throws Exception { + String value = "beforeQuote\\\"afterQuote"; + String actual = gson.fromJson("\"" + value + "\"", String.class); + String expected = "beforeQuote\"afterQuote"; + assertEquals(expected, actual); + } + + public void testStringValueAsSingleElementArraySerialization() throws Exception { + String[] target = {"abc"}; + assertEquals("[\"abc\"]", gson.toJson(target)); + assertEquals("[\"abc\"]", gson.toJson(target, String[].class)); + } + + public void testStringWithEscapedSlashDeserialization() { + String value = "/"; + String json = "'\\/'"; + String actual = gson.fromJson(json, String.class); + assertEquals(value, actual); + } + + /** + * Created in response to http://groups.google.com/group/google-gson/browse_thread/thread/2431d4a3d0d6cb23 + */ + public void testAssignmentCharSerialization() { + String value = "abc="; + String json = gson.toJson(value); + assertEquals("\"abc\\u003d\"", json); + } + + /** + * Created in response to http://groups.google.com/group/google-gson/browse_thread/thread/2431d4a3d0d6cb23 + */ + public void testAssignmentCharDeserialization() { + String json = "\"abc=\""; + String value = gson.fromJson(json, String.class); + assertEquals("abc=", value); + + json = "'abc\u003d'"; + value = gson.fromJson(json, String.class); + assertEquals("abc=", value); + } + + public void testJavascriptKeywordsInStringSerialization() { + String value = "null true false function"; + String json = gson.toJson(value); + assertEquals("\"" + value + "\"", json); + } + + public void testJavascriptKeywordsInStringDeserialization() { + String json = "'null true false function'"; + String value = gson.fromJson(json, String.class); + assertEquals(json.substring(1, json.length() - 1), value); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/ThrowableFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/ThrowableFunctionalTest.java new file mode 100644 index 00000000..f6ae748a --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ThrowableFunctionalTest.java @@ -0,0 +1,65 @@ +// Copyright (C) 2014 Trymph Inc. +package com.google.gson.functional; + +import java.io.IOException; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +@SuppressWarnings("serial") +public final class ThrowableFunctionalTest extends TestCase { + private final Gson gson = new Gson(); + + public void testExceptionWithoutCause() { + RuntimeException e = new RuntimeException("hello"); + String json = gson.toJson(e); + assertTrue(json.contains("hello")); + + e = gson.fromJson("{'detailMessage':'hello'}", RuntimeException.class); + assertEquals("hello", e.getMessage()); + } + + public void testExceptionWithCause() { + Exception e = new Exception("top level", new IOException("io error")); + String json = gson.toJson(e); + assertTrue(json.contains("{\"detailMessage\":\"top level\",\"cause\":{\"detailMessage\":\"io error\"")); + + e = gson.fromJson("{'detailMessage':'top level','cause':{'detailMessage':'io error'}}", Exception.class); + assertEquals("top level", e.getMessage()); + assertTrue(e.getCause() instanceof Throwable); // cause is not parameterized so type info is lost + assertEquals("io error", e.getCause().getMessage()); + } + + public void testSerializedNameOnExceptionFields() { + MyException e = new MyException(); + String json = gson.toJson(e); + assertTrue(json.contains("{\"my_custom_name\":\"myCustomMessageValue\"")); + } + + public void testErrorWithoutCause() { + OutOfMemoryError e = new OutOfMemoryError("hello"); + String json = gson.toJson(e); + assertTrue(json.contains("hello")); + + e = gson.fromJson("{'detailMessage':'hello'}", OutOfMemoryError.class); + assertEquals("hello", e.getMessage()); + } + + public void testErrornWithCause() { + Error e = new Error("top level", new IOException("io error")); + String json = gson.toJson(e); + assertTrue(json.contains("top level")); + assertTrue(json.contains("io error")); + + e = gson.fromJson("{'detailMessage':'top level','cause':{'detailMessage':'io error'}}", Error.class); + assertEquals("top level", e.getMessage()); + assertTrue(e.getCause() instanceof Throwable); // cause is not parameterized so type info is lost + assertEquals("io error", e.getCause().getMessage()); + } + + private static final class MyException extends Throwable { + @SerializedName("my_custom_name") String myCustomMessage = "myCustomMessageValue"; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java new file mode 100644 index 00000000..53d1c5cf --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java @@ -0,0 +1,176 @@ +/* + * 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.functional; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.reflect.TypeToken; + +/** + * Collection of functional tests for DOM tree based type adapters. + */ +public class TreeTypeAdaptersTest extends TestCase { + private static final Id<Student> STUDENT1_ID = new Id<Student>("5", Student.class); + private static final Id<Student> STUDENT2_ID = new Id<Student>("6", Student.class); + private static final Student STUDENT1 = new Student(STUDENT1_ID, "first"); + private static final Student STUDENT2 = new Student(STUDENT2_ID, "second"); + private static final Type TYPE_COURSE_HISTORY = + new TypeToken<Course<HistoryCourse>>(){}.getType(); + private static final Id<Course<HistoryCourse>> COURSE_ID = + new Id<Course<HistoryCourse>>("10", TYPE_COURSE_HISTORY); + + private Gson gson; + private Course<HistoryCourse> course; + + @Override + protected void setUp() { + gson = new GsonBuilder() + .registerTypeAdapter(Id.class, new IdTreeTypeAdapter()) + .create(); + course = new Course<HistoryCourse>(COURSE_ID, 4, + new Assignment<HistoryCourse>(null, null), createList(STUDENT1, STUDENT2)); + } + + public void testSerializeId() { + String json = gson.toJson(course, TYPE_COURSE_HISTORY); + assertTrue(json.contains(String.valueOf(COURSE_ID.getValue()))); + assertTrue(json.contains(String.valueOf(STUDENT1_ID.getValue()))); + assertTrue(json.contains(String.valueOf(STUDENT2_ID.getValue()))); + } + + public void testDeserializeId() { + String json = "{courseId:1,students:[{id:1,name:'first'},{id:6,name:'second'}]," + + "numAssignments:4,assignment:{}}"; + Course<HistoryCourse> target = gson.fromJson(json, TYPE_COURSE_HISTORY); + assertEquals("1", target.getStudents().get(0).id.getValue()); + assertEquals("6", target.getStudents().get(1).id.getValue()); + assertEquals("1", target.getId().getValue()); + } + + private static final class Id<R> { + final String value; + @SuppressWarnings("unused") + final Type typeOfId; + + private Id(String value, Type typeOfId) { + this.value = value; + this.typeOfId = typeOfId; + } + public String getValue() { + return value; + } + } + + private static final class IdTreeTypeAdapter implements JsonSerializer<Id<?>>, + JsonDeserializer<Id<?>> { + + @SuppressWarnings("rawtypes") + public Id<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + if (!(typeOfT instanceof ParameterizedType)) { + throw new JsonParseException("Id of unknown type: " + typeOfT); + } + ParameterizedType parameterizedType = (ParameterizedType) typeOfT; + // Since Id takes only one TypeVariable, the actual type corresponding to the first + // TypeVariable is the Type we are looking for + Type typeOfId = parameterizedType.getActualTypeArguments()[0]; + return new Id(json.getAsString(), typeOfId); + } + + public JsonElement serialize(Id<?> src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getValue()); + } + } + + @SuppressWarnings("unused") + private static class Student { + Id<Student> id; + String name; + + private Student() { + this(null, null); + } + public Student(Id<Student> id, String name) { + this.id = id; + this.name = name; + } + } + + @SuppressWarnings("unused") + private static class Course<T> { + final List<Student> students; + private final Id<Course<T>> courseId; + private final int numAssignments; + private final Assignment<T> assignment; + + private Course() { + this(null, 0, null, new ArrayList<Student>()); + } + + public Course(Id<Course<T>> courseId, int numAssignments, + Assignment<T> assignment, List<Student> players) { + this.courseId = courseId; + this.numAssignments = numAssignments; + this.assignment = assignment; + this.students = players; + } + public Id<Course<T>> getId() { + return courseId; + } + List<Student> getStudents() { + return students; + } + } + + @SuppressWarnings("unused") + private static class Assignment<T> { + private final Id<Assignment<T>> id; + private final T data; + + private Assignment() { + this(null, null); + } + public Assignment(Id<Assignment<T>> id, T data) { + this.id = id; + this.data = data; + } + } + + @SuppressWarnings("unused") + private static class HistoryCourse { + int numClasses; + } + + private static <T> List<T> createList(T ...items) { + return Arrays.asList(items); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java new file mode 100644 index 00000000..2f13f664 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java @@ -0,0 +1,149 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import junit.framework.TestCase; + +public final class TypeAdapterPrecedenceTest extends TestCase { + public void testNonstreamingFollowedByNonstreaming() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Foo.class, newSerializer("serializer 1")) + .registerTypeAdapter(Foo.class, newSerializer("serializer 2")) + .registerTypeAdapter(Foo.class, newDeserializer("deserializer 1")) + .registerTypeAdapter(Foo.class, newDeserializer("deserializer 2")) + .create(); + assertEquals("\"foo via serializer 2\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via deserializer 2", gson.fromJson("foo", Foo.class).name); + } + + public void testStreamingFollowedByStreaming() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter 1")) + .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter 2")) + .create(); + assertEquals("\"foo via type adapter 2\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via type adapter 2", gson.fromJson("foo", Foo.class).name); + } + + public void testSerializeNonstreamingTypeAdapterFollowedByStreamingTypeAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Foo.class, newSerializer("serializer")) + .registerTypeAdapter(Foo.class, newDeserializer("deserializer")) + .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter")) + .create(); + assertEquals("\"foo via type adapter\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via type adapter", gson.fromJson("foo", Foo.class).name); + } + + public void testStreamingFollowedByNonstreaming() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter")) + .registerTypeAdapter(Foo.class, newSerializer("serializer")) + .registerTypeAdapter(Foo.class, newDeserializer("deserializer")) + .create(); + assertEquals("\"foo via serializer\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via deserializer", gson.fromJson("foo", Foo.class).name); + } + + public void testStreamingHierarchicalFollowedByNonstreaming() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Foo.class, newTypeAdapter("type adapter")) + .registerTypeAdapter(Foo.class, newSerializer("serializer")) + .registerTypeAdapter(Foo.class, newDeserializer("deserializer")) + .create(); + assertEquals("\"foo via serializer\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via deserializer", gson.fromJson("foo", Foo.class).name); + } + + public void testStreamingFollowedByNonstreamingHierarchical() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter")) + .registerTypeHierarchyAdapter(Foo.class, newSerializer("serializer")) + .registerTypeHierarchyAdapter(Foo.class, newDeserializer("deserializer")) + .create(); + assertEquals("\"foo via type adapter\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via type adapter", gson.fromJson("foo", Foo.class).name); + } + + public void testStreamingHierarchicalFollowedByNonstreamingHierarchical() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Foo.class, newSerializer("serializer")) + .registerTypeHierarchyAdapter(Foo.class, newDeserializer("deserializer")) + .registerTypeHierarchyAdapter(Foo.class, newTypeAdapter("type adapter")) + .create(); + assertEquals("\"foo via type adapter\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via type adapter", gson.fromJson("foo", Foo.class).name); + } + + public void testNonstreamingHierarchicalFollowedByNonstreaming() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Foo.class, newSerializer("hierarchical")) + .registerTypeHierarchyAdapter(Foo.class, newDeserializer("hierarchical")) + .registerTypeAdapter(Foo.class, newSerializer("non hierarchical")) + .registerTypeAdapter(Foo.class, newDeserializer("non hierarchical")) + .create(); + assertEquals("\"foo via non hierarchical\"", gson.toJson(new Foo("foo"))); + assertEquals("foo via non hierarchical", gson.fromJson("foo", Foo.class).name); + } + + private static class Foo { + final String name; + private Foo(String name) { + this.name = name; + } + } + + private JsonSerializer<Foo> newSerializer(final String name) { + return new JsonSerializer<Foo>() { + public JsonElement serialize(Foo src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.name + " via " + name); + } + }; + } + + private JsonDeserializer<Foo> newDeserializer(final String name) { + return new JsonDeserializer<Foo>() { + public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return new Foo(json.getAsString() + " via " + name); + } + }; + } + + private TypeAdapter<Foo> newTypeAdapter(final String name) { + return new TypeAdapter<Foo>() { + @Override public Foo read(JsonReader in) throws IOException { + return new Foo(in.nextString() + " via " + name); + } + @Override public void write(JsonWriter out, Foo value) throws IOException { + out.value(value.name + " via " + name); + } + }; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java b/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java new file mode 100644 index 00000000..aa2f8f83 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java @@ -0,0 +1,221 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import junit.framework.TestCase; + +/** + * Test that the hierarchy adapter works when subtypes are used. + */ +public final class TypeHierarchyAdapterTest extends TestCase { + + public void testTypeHierarchy() { + Manager andy = new Manager(); + andy.userid = "andy"; + andy.startDate = 2005; + andy.minions = new Employee[] { + new Employee("inder", 2007), + new Employee("joel", 2006), + new Employee("jesse", 2006), + }; + + CEO eric = new CEO(); + eric.userid = "eric"; + eric.startDate = 2001; + eric.assistant = new Employee("jerome", 2006); + + eric.minions = new Employee[] { + new Employee("larry", 1998), + new Employee("sergey", 1998), + andy, + }; + + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter()) + .setPrettyPrinting() + .create(); + + Company company = new Company(); + company.ceo = eric; + + String json = gson.toJson(company, Company.class); + assertEquals("{\n" + + " \"ceo\": {\n" + + " \"userid\": \"eric\",\n" + + " \"startDate\": 2001,\n" + + " \"minions\": [\n" + + " {\n" + + " \"userid\": \"larry\",\n" + + " \"startDate\": 1998\n" + + " },\n" + + " {\n" + + " \"userid\": \"sergey\",\n" + + " \"startDate\": 1998\n" + + " },\n" + + " {\n" + + " \"userid\": \"andy\",\n" + + " \"startDate\": 2005,\n" + + " \"minions\": [\n" + + " {\n" + + " \"userid\": \"inder\",\n" + + " \"startDate\": 2007\n" + + " },\n" + + " {\n" + + " \"userid\": \"joel\",\n" + + " \"startDate\": 2006\n" + + " },\n" + + " {\n" + + " \"userid\": \"jesse\",\n" + + " \"startDate\": 2006\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"assistant\": {\n" + + " \"userid\": \"jerome\",\n" + + " \"startDate\": 2006\n" + + " }\n" + + " }\n" + + "}", json); + + Company copied = gson.fromJson(json, Company.class); + assertEquals(json, gson.toJson(copied, Company.class)); + assertEquals(copied.ceo.userid, company.ceo.userid); + assertEquals(copied.ceo.assistant.userid, company.ceo.assistant.userid); + assertEquals(copied.ceo.minions[0].userid, company.ceo.minions[0].userid); + assertEquals(copied.ceo.minions[1].userid, company.ceo.minions[1].userid); + assertEquals(copied.ceo.minions[2].userid, company.ceo.minions[2].userid); + assertEquals(((Manager) copied.ceo.minions[2]).minions[0].userid, + ((Manager) company.ceo.minions[2]).minions[0].userid); + assertEquals(((Manager) copied.ceo.minions[2]).minions[1].userid, + ((Manager) company.ceo.minions[2]).minions[1].userid); + } + + public void testRegisterSuperTypeFirst() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter()) + .registerTypeHierarchyAdapter(Manager.class, new ManagerAdapter()) + .create(); + + Manager manager = new Manager(); + manager.userid = "inder"; + + String json = gson.toJson(manager, Manager.class); + assertEquals("\"inder\"", json); + Manager copied = gson.fromJson(json, Manager.class); + assertEquals(manager.userid, copied.userid); + } + + /** This behaviour changed in Gson 2.1; it used to throw. */ + public void testRegisterSubTypeFirstAllowed() { + new GsonBuilder() + .registerTypeHierarchyAdapter(Manager.class, new ManagerAdapter()) + .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter()) + .create(); + } + + static class ManagerAdapter implements JsonSerializer<Manager>, JsonDeserializer<Manager> { + public Manager deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + Manager result = new Manager(); + result.userid = json.getAsString(); + return result; + } + public JsonElement serialize(Manager src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.userid); + } + } + + static class EmployeeAdapter implements JsonSerializer<Employee>, JsonDeserializer<Employee> { + public JsonElement serialize(Employee employee, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject result = new JsonObject(); + result.add("userid", context.serialize(employee.userid, String.class)); + result.add("startDate", context.serialize(employee.startDate, long.class)); + if (employee instanceof Manager) { + result.add("minions", context.serialize(((Manager) employee).minions, Employee[].class)); + if (employee instanceof CEO) { + result.add("assistant", context.serialize(((CEO) employee).assistant, Employee.class)); + } + } + return result; + } + + public Employee deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + JsonObject object = json.getAsJsonObject(); + Employee result = null; + + // if the employee has an assistant, she must be the CEO + JsonElement assistant = object.get("assistant"); + if (assistant != null) { + result = new CEO(); + ((CEO) result).assistant = context.deserialize(assistant, Employee.class); + } + + // only managers have minions + JsonElement minons = object.get("minions"); + if (minons != null) { + if (result == null) { + result = new Manager(); + } + ((Manager) result).minions = context.deserialize(minons, Employee[].class); + } + + if (result == null) { + result = new Employee(); + } + result.userid = context.deserialize(object.get("userid"), String.class); + result.startDate = context.<Long>deserialize(object.get("startDate"), long.class); + return result; + } + } + + static class Employee { + String userid; + long startDate; + + Employee(String userid, long startDate) { + this.userid = userid; + this.startDate = startDate; + } + + Employee() {} + } + + static class Manager extends Employee { + Employee[] minions; + } + + static class CEO extends Manager { + Employee assistant; + } + + static class Company { + CEO ceo; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java new file mode 100644 index 00000000..2d7503eb --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java @@ -0,0 +1,140 @@ +/* + * 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.functional; + +import com.google.gson.Gson; + +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.Arrays; +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Functional test for Gson serialization and deserialization of + * classes with type variables. + * + * @author Joel Leitch + */ +public class TypeVariableTest extends TestCase { + + public void testAdvancedTypeVariables() throws Exception { + Gson gson = new Gson(); + Bar bar1 = new Bar("someString", 1, true); + ArrayList<Integer> arrayList = new ArrayList<Integer>(); + arrayList.add(1); + arrayList.add(2); + arrayList.add(3); + bar1.map.put("key1", arrayList); + bar1.map.put("key2", new ArrayList<Integer>()); + String json = gson.toJson(bar1); + + Bar bar2 = gson.fromJson(json, Bar.class); + assertEquals(bar1, bar2); + } + + public void testTypeVariablesViaTypeParameter() throws Exception { + Gson gson = new Gson(); + Foo<String, Integer> original = new Foo<String, Integer>("e", 5, false); + original.map.put("f", Arrays.asList(6, 7)); + Type type = new TypeToken<Foo<String, Integer>>() {}.getType(); + String json = gson.toJson(original, type); + assertEquals("{\"someSField\":\"e\",\"someTField\":5,\"map\":{\"f\":[6,7]},\"redField\":false}", + json); + assertEquals(original, gson.<Foo<String, Integer>>fromJson(json, type)); + } + + public void testBasicTypeVariables() throws Exception { + Gson gson = new Gson(); + Blue blue1 = new Blue(true); + String json = gson.toJson(blue1); + + Blue blue2 = gson.fromJson(json, Blue.class); + assertEquals(blue1, blue2); + } + + public static class Blue extends Red<Boolean> { + public Blue() { + super(false); + } + + public Blue(boolean value) { + super(value); + } + + // Technically, we should implement hashcode too + @Override + public boolean equals(Object o) { + if (!(o instanceof Blue)) { + return false; + } + Blue blue = (Blue) o; + return redField.equals(blue.redField); + } + } + + public static class Red<S> { + protected S redField; + + public Red() {} + + public Red(S redField) { + this.redField = redField; + } + } + + public static class Foo<S, T> extends Red<Boolean> { + private S someSField; + private T someTField; + public final Map<S, List<T>> map = new HashMap<S, List<T>>(); + + public Foo() {} + + public Foo(S sValue, T tValue, Boolean redField) { + super(redField); + this.someSField = sValue; + this.someTField = tValue; + } + + // Technically, we should implement hashcode too + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object o) { + if (!(o instanceof Foo<?, ?>)) { + return false; + } + Foo<S, T> realFoo = (Foo<S, T>) o; + return redField.equals(realFoo.redField) + && someTField.equals(realFoo.someTField) + && someSField.equals(realFoo.someSField) + && map.equals(realFoo.map); + } + } + + public static class Bar extends Foo<String, Integer> { + public Bar() { + this("", 0, false); + } + + public Bar(String s, Integer i, boolean b) { + super(s, i, b); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java b/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java new file mode 100644 index 00000000..62c7fa09 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java @@ -0,0 +1,134 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.common.TestTypes.BagOfPrimitives; +import com.google.gson.common.TestTypes.ClassOverridingEquals; + +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; + +import java.lang.reflect.Type; + +/** + * Functional tests that do not fall neatly into any of the existing classification. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class UncategorizedTest extends TestCase { + + private Gson gson = null; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testInvalidJsonDeserializationFails() throws Exception { + try { + gson.fromJson("adfasdf1112,,,\":", BagOfPrimitives.class); + fail("Bad JSON should throw a ParseException"); + } catch (JsonParseException expected) { } + + try { + gson.fromJson("{adfasdf1112,,,\":}", BagOfPrimitives.class); + fail("Bad JSON should throw a ParseException"); + } catch (JsonParseException expected) { } + } + + public void testObjectEqualButNotSameSerialization() throws Exception { + ClassOverridingEquals objA = new ClassOverridingEquals(); + ClassOverridingEquals objB = new ClassOverridingEquals(); + objB.ref = objA; + String json = gson.toJson(objB); + assertEquals(objB.getExpectedJson(), json); + } + + public void testStaticFieldsAreNotSerialized() { + BagOfPrimitives target = new BagOfPrimitives(); + assertFalse(gson.toJson(target).contains("DEFAULT_VALUE")); + } + + public void testGsonInstanceReusableForSerializationAndDeserialization() { + BagOfPrimitives bag = new BagOfPrimitives(); + String json = gson.toJson(bag); + BagOfPrimitives deserialized = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(bag, deserialized); + } + + /** + * This test ensures that a custom deserializer is able to return a derived class instance for a + * base class object. For a motivation for this test, see Issue 37 and + * http://groups.google.com/group/google-gson/browse_thread/thread/677d56e9976d7761 + */ + public void testReturningDerivedClassesDuringDeserialization() { + Gson gson = new GsonBuilder().registerTypeAdapter(Base.class, new BaseTypeAdapter()).create(); + String json = "{\"opType\":\"OP1\"}"; + Base base = gson.fromJson(json, Base.class); + assertTrue(base instanceof Derived1); + assertEquals(OperationType.OP1, base.opType); + + json = "{\"opType\":\"OP2\"}"; + base = gson.fromJson(json, Base.class); + assertTrue(base instanceof Derived2); + assertEquals(OperationType.OP2, base.opType); + } + + /** + * Test that trailing whitespace is ignored. + * http://code.google.com/p/google-gson/issues/detail?id=302 + */ + public void testTrailingWhitespace() throws Exception { + List<Integer> integers = gson.fromJson("[1,2,3] \n\n ", + new TypeToken<List<Integer>>() {}.getType()); + assertEquals(Arrays.asList(1, 2, 3), integers); + } + + private enum OperationType { OP1, OP2 } + private static class Base { + OperationType opType; + } + private static class Derived1 extends Base { + Derived1() { opType = OperationType.OP1; } + } + private static class Derived2 extends Base { + Derived2() { opType = OperationType.OP2; } + } + private static class BaseTypeAdapter implements JsonDeserializer<Base> { + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String opTypeStr = json.getAsJsonObject().get("opType").getAsString(); + OperationType opType = OperationType.valueOf(opTypeStr); + switch (opType) { + case OP1: + return new Derived1(); + case OP2: + return new Derived2(); + } + throw new JsonParseException("unknown type: " + json); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/VersioningTest.java b/gson/src/test/java/com/google/gson/functional/VersioningTest.java new file mode 100644 index 00000000..bc526de0 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/VersioningTest.java @@ -0,0 +1,170 @@ +/* + * 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.functional; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Since; +import com.google.gson.annotations.Until; +import com.google.gson.common.TestTypes.BagOfPrimitives; + +import junit.framework.TestCase; + +/** + * Functional tests for versioning support in Gson. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class VersioningTest extends TestCase { + private static final int A = 0; + private static final int B = 1; + private static final int C = 2; + private static final int D = 3; + + private GsonBuilder builder; + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new GsonBuilder(); + } + + public void testVersionedUntilSerialization() { + Version1 target = new Version1(); + Gson gson = builder.setVersion(1.29).create(); + String json = gson.toJson(target); + assertTrue(json.contains("\"a\":" + A)); + + gson = builder.setVersion(1.3).create(); + json = gson.toJson(target); + assertFalse(json.contains("\"a\":" + A)); + } + + public void testVersionedUntilDeserialization() { + Gson gson = builder.setVersion(1.3).create(); + String json = "{\"a\":3,\"b\":4,\"c\":5}"; + Version1 version1 = gson.fromJson(json, Version1.class); + assertEquals(A, version1.a); + } + + public void testVersionedClassesSerialization() { + Gson gson = builder.setVersion(1.0).create(); + String json1 = gson.toJson(new Version1()); + String json2 = gson.toJson(new Version1_1()); + assertEquals(json1, json2); + } + + public void testVersionedClassesDeserialization() { + Gson gson = builder.setVersion(1.0).create(); + String json = "{\"a\":3,\"b\":4,\"c\":5}"; + Version1 version1 = gson.fromJson(json, Version1.class); + assertEquals(3, version1.a); + assertEquals(4, version1.b); + Version1_1 version1_1 = gson.fromJson(json, Version1_1.class); + assertEquals(3, version1_1.a); + assertEquals(4, version1_1.b); + assertEquals(C, version1_1.c); + } + + public void testIgnoreLaterVersionClassSerialization() { + Gson gson = builder.setVersion(1.0).create(); + assertEquals("null", gson.toJson(new Version1_2())); + } + + public void testIgnoreLaterVersionClassDeserialization() { + Gson gson = builder.setVersion(1.0).create(); + String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}"; + Version1_2 version1_2 = gson.fromJson(json, Version1_2.class); + // Since the class is versioned to be after 1.0, we expect null + // This is the new behavior in Gson 2.0 + assertNull(version1_2); + } + + public void testVersionedGsonWithUnversionedClassesSerialization() { + Gson gson = builder.setVersion(1.0).create(); + BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue"); + assertEquals(target.getExpectedJson(), gson.toJson(target)); + } + + public void testVersionedGsonWithUnversionedClassesDeserialization() { + Gson gson = builder.setVersion(1.0).create(); + String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}"; + + BagOfPrimitives expected = new BagOfPrimitives(); + expected.longValue = 10; + expected.intValue = 20; + expected.booleanValue = false; + BagOfPrimitives actual = gson.fromJson(json, BagOfPrimitives.class); + assertEquals(expected, actual); + } + + public void testVersionedGsonMixingSinceAndUntilSerialization() { + Gson gson = builder.setVersion(1.0).create(); + SinceUntilMixing target = new SinceUntilMixing(); + String json = gson.toJson(target); + assertFalse(json.contains("\"b\":" + B)); + + gson = builder.setVersion(1.2).create(); + json = gson.toJson(target); + assertTrue(json.contains("\"b\":" + B)); + + gson = builder.setVersion(1.3).create(); + json = gson.toJson(target); + assertFalse(json.contains("\"b\":" + B)); + } + + public void testVersionedGsonMixingSinceAndUntilDeserialization() { + String json = "{\"a\":5,\"b\":6}"; + Gson gson = builder.setVersion(1.0).create(); + SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class); + assertEquals(5, result.a); + assertEquals(B, result.b); + + gson = builder.setVersion(1.2).create(); + result = gson.fromJson(json, SinceUntilMixing.class); + assertEquals(5, result.a); + assertEquals(6, result.b); + + gson = builder.setVersion(1.3).create(); + result = gson.fromJson(json, SinceUntilMixing.class); + assertEquals(5, result.a); + assertEquals(B, result.b); + } + + private static class Version1 { + @Until(1.3) int a = A; + @Since(1.0) int b = B; + } + + private static class Version1_1 extends Version1 { + @Since(1.1) int c = C; + } + + @Since(1.2) + private static class Version1_2 extends Version1_1 { + @SuppressWarnings("unused") + int d = D; + } + + private static class SinceUntilMixing { + int a = A; + + @Since(1.1) + @Until(1.3) + int b = B; + } +} diff --git a/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java b/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java new file mode 100644 index 00000000..c80700bd --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java @@ -0,0 +1,74 @@ +/* + * 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; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +import junit.framework.TestCase; + +public final class GsonTypesTest extends TestCase { + + public void testNewParameterizedTypeWithoutOwner() throws Exception { + // List<A>. List is a top-level class + Type type = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, A.class); + assertEquals(A.class, getFirstTypeArgument(type)); + + // A<B>. A is a static inner class. + type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, B.class); + assertEquals(B.class, getFirstTypeArgument(type)); + + final class D { + } + try { + // D<A> is not allowed since D is not a static inner class + $Gson$Types.newParameterizedTypeWithOwner(null, D.class, A.class); + fail(); + } catch (IllegalArgumentException expected) {} + + // A<D> is allowed. + type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, D.class); + assertEquals(D.class, getFirstTypeArgument(type)); + } + + public void testGetFirstTypeArgument() throws Exception { + assertNull(getFirstTypeArgument(A.class)); + + Type type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, B.class, C.class); + assertEquals(B.class, getFirstTypeArgument(type)); + } + + private static final class A { + } + private static final class B { + } + private static final class C { + } + + /** + * Given a parameterized type A<B,C>, returns B. If the specified type is not + * a generic type, returns null. + */ + public static Type getFirstTypeArgument(Type type) throws Exception { + if (!(type instanceof ParameterizedType)) return null; + ParameterizedType ptype = (ParameterizedType) type; + Type[] actualTypeArguments = ptype.getActualTypeArguments(); + if (actualTypeArguments.length == 0) return null; + return $Gson$Types.canonicalize(actualTypeArguments[0]); + } +} diff --git a/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java b/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java new file mode 100644 index 00000000..f108fa0d --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java @@ -0,0 +1,32 @@ +/*
+ * Copyright (C) 2015 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 junit.framework.TestCase;
+
+public class LazilyParsedNumberTest extends TestCase {
+ public void testHashCode() {
+ LazilyParsedNumber n1 = new LazilyParsedNumber("1");
+ LazilyParsedNumber n1Another = new LazilyParsedNumber("1");
+ assertEquals(n1.hashCode(), n1Another.hashCode());
+ }
+
+ public void testEquals() {
+ LazilyParsedNumber n1 = new LazilyParsedNumber("1");
+ LazilyParsedNumber n1Another = new LazilyParsedNumber("1");
+ assertTrue(n1.equals(n1Another));
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/internal/LinkedHashTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedHashTreeMapTest.java new file mode 100644 index 00000000..2aeeeb76 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/LinkedHashTreeMapTest.java @@ -0,0 +1,290 @@ +/* + * 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 com.google.gson.common.MoreAsserts; +import com.google.gson.internal.LinkedHashTreeMap.AvlBuilder; +import com.google.gson.internal.LinkedHashTreeMap.AvlIterator; +import com.google.gson.internal.LinkedHashTreeMap.Node; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; +import junit.framework.TestCase; + +public final class LinkedHashTreeMapTest extends TestCase { + public void testIterationOrder() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + assertIterationOrder(map.keySet(), "a", "c", "b"); + assertIterationOrder(map.values(), "android", "cola", "bbq"); + } + + public void testRemoveRootDoesNotDoubleUnlink() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + Iterator<Map.Entry<String,String>> it = map.entrySet().iterator(); + it.next(); + it.next(); + it.next(); + it.remove(); + assertIterationOrder(map.keySet(), "a", "c"); + } + + public void testPutNullKeyFails() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + try { + map.put(null, "android"); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testPutNonComparableKeyFails() { + LinkedHashTreeMap<Object, String> map = new LinkedHashTreeMap<Object, String>(); + try { + map.put(new Object(), "android"); + fail(); + } catch (ClassCastException expected) {} + } + + public void testContainsNonComparableKeyReturnsFalse() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", "android"); + assertFalse(map.containsKey(new Object())); + } + + public void testContainsNullKeyIsAlwaysFalse() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", "android"); + assertFalse(map.containsKey(null)); + } + + public void testPutOverrides() throws Exception { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + assertNull(map.put("d", "donut")); + assertNull(map.put("e", "eclair")); + assertNull(map.put("f", "froyo")); + assertEquals(3, map.size()); + + assertEquals("donut", map.get("d")); + assertEquals("donut", map.put("d", "done")); + assertEquals(3, map.size()); + } + + public void testEmptyStringValues() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", ""); + assertTrue(map.containsKey("a")); + assertEquals("", map.get("a")); + } + + // NOTE that this does not happen every time, but given the below predictable random, + // this test will consistently fail (assuming the initial size is 16 and rehashing + // size remains at 3/4) + public void testForceDoublingAndRehash() throws Exception { + Random random = new Random(1367593214724L); + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + String[] keys = new String[1000]; + for (int i = 0; i < keys.length; i++) { + keys[i] = Integer.toString(Math.abs(random.nextInt()), 36) + "-" + i; + map.put(keys[i], "" + i); + } + + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + assertTrue(map.containsKey(key)); + assertEquals("" + i, map.get(key)); + } + } + + public void testClear() { + LinkedHashTreeMap<String, String> map = new LinkedHashTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + map.clear(); + assertIterationOrder(map.keySet()); + assertEquals(0, map.size()); + } + + public void testEqualsAndHashCode() throws Exception { + LinkedHashTreeMap<String, Integer> map1 = new LinkedHashTreeMap<String, Integer>(); + map1.put("A", 1); + map1.put("B", 2); + map1.put("C", 3); + map1.put("D", 4); + + LinkedHashTreeMap<String, Integer> map2 = new LinkedHashTreeMap<String, Integer>(); + map2.put("C", 3); + map2.put("B", 2); + map2.put("D", 4); + map2.put("A", 1); + + MoreAsserts.assertEqualsAndHashCode(map1, map2); + } + + public void testAvlWalker() { + assertAvlWalker(node(node("a"), "b", node("c")), + "a", "b", "c"); + assertAvlWalker(node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))), + "a", "b", "c", "d", "e", "f", "g"); + assertAvlWalker(node(node(null, "a", node("b")), "c", node(node("d"), "e", null)), + "a", "b", "c", "d", "e"); + assertAvlWalker(node(null, "a", node(null, "b", node(null, "c", node("d")))), + "a", "b", "c", "d"); + assertAvlWalker(node(node(node(node("a"), "b", null), "c", null), "d", null), + "a", "b", "c", "d"); + } + + private void assertAvlWalker(Node<String, String> root, String... values) { + AvlIterator<String, String> iterator = new AvlIterator<String, String>(); + iterator.reset(root); + for (String value : values) { + assertEquals(value, iterator.next().getKey()); + } + assertNull(iterator.next()); + } + + public void testAvlBuilder() { + assertAvlBuilder(1, "a"); + assertAvlBuilder(2, "(. a b)"); + assertAvlBuilder(3, "(a b c)"); + assertAvlBuilder(4, "(a b (. c d))"); + assertAvlBuilder(5, "(a b (c d e))"); + assertAvlBuilder(6, "((. a b) c (d e f))"); + assertAvlBuilder(7, "((a b c) d (e f g))"); + assertAvlBuilder(8, "((a b c) d (e f (. g h)))"); + assertAvlBuilder(9, "((a b c) d (e f (g h i)))"); + assertAvlBuilder(10, "((a b c) d ((. e f) g (h i j)))"); + assertAvlBuilder(11, "((a b c) d ((e f g) h (i j k)))"); + assertAvlBuilder(12, "((a b (. c d)) e ((f g h) i (j k l)))"); + assertAvlBuilder(13, "((a b (c d e)) f ((g h i) j (k l m)))"); + assertAvlBuilder(14, "(((. a b) c (d e f)) g ((h i j) k (l m n)))"); + assertAvlBuilder(15, "(((a b c) d (e f g)) h ((i j k) l (m n o)))"); + assertAvlBuilder(16, "(((a b c) d (e f g)) h ((i j k) l (m n (. o p))))"); + assertAvlBuilder(30, "((((. a b) c (d e f)) g ((h i j) k (l m n))) o " + + "(((p q r) s (t u v)) w ((x y z) A (B C D))))"); + assertAvlBuilder(31, "((((a b c) d (e f g)) h ((i j k) l (m n o))) p " + + "(((q r s) t (u v w)) x ((y z A) B (C D E))))"); + } + + private void assertAvlBuilder(int size, String expected) { + char[] values = "abcdefghijklmnopqrstuvwxyzABCDE".toCharArray(); + AvlBuilder<String, String> avlBuilder = new AvlBuilder<String, String>(); + avlBuilder.reset(size); + for (int i = 0; i < size; i++) { + avlBuilder.add(node(Character.toString(values[i]))); + } + assertTree(expected, avlBuilder.root()); + } + + public void testDoubleCapacity() { + @SuppressWarnings("unchecked") // Arrays and generics don't get along. + Node<String, String>[] oldTable = new Node[1]; + oldTable[0] = node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))); + + Node<String, String>[] newTable = LinkedHashTreeMap.doubleCapacity(oldTable); + assertTree("(b d f)", newTable[0]); // Even hash codes! + assertTree("(a c (. e g))", newTable[1]); // Odd hash codes! + } + + public void testDoubleCapacityAllNodesOnLeft() { + @SuppressWarnings("unchecked") // Arrays and generics don't get along. + Node<String, String>[] oldTable = new Node[1]; + oldTable[0] = node(node("b"), "d", node("f")); + + Node<String, String>[] newTable = LinkedHashTreeMap.doubleCapacity(oldTable); + assertTree("(b d f)", newTable[0]); // Even hash codes! + assertNull(newTable[1]); // Odd hash codes! + + for (Node<?, ?> node : newTable) { + if (node != null) { + assertConsistent(node); + } + } + } + + private static final Node<String, String> head = new Node<String, String>(); + + private Node<String, String> node(String value) { + return new Node<String, String>(null, value, value.hashCode(), head, head); + } + + private Node<String, String> node(Node<String, String> left, String value, + Node<String, String> right) { + Node<String, String> result = node(value); + if (left != null) { + result.left = left; + left.parent = result; + } + if (right != null) { + result.right = right; + right.parent = result; + } + return result; + } + + private void assertTree(String expected, Node<?, ?> root) { + assertEquals(expected, toString(root)); + assertConsistent(root); + } + + private void assertConsistent(Node<?, ?> node) { + int leftHeight = 0; + if (node.left != null) { + assertConsistent(node.left); + assertSame(node, node.left.parent); + leftHeight = node.left.height; + } + int rightHeight = 0; + if (node.right != null) { + assertConsistent(node.right); + assertSame(node, node.right.parent); + rightHeight = node.right.height; + } + if (node.parent != null) { + assertTrue(node.parent.left == node || node.parent.right == node); + } + if (Math.max(leftHeight, rightHeight) + 1 != node.height) { + fail(); + } + } + + private String toString(Node<?, ?> root) { + if (root == null) { + return "."; + } else if (root.left == null && root.right == null) { + return String.valueOf(root.key); + } else { + return String.format("(%s %s %s)", toString(root.left), root.key, toString(root.right)); + } + } + + private <T> void assertIterationOrder(Iterable<T> actual, T... expected) { + ArrayList<T> actualList = new ArrayList<T>(); + for (T t : actual) { + actualList.add(t); + } + assertEquals(Arrays.asList(expected), actualList); + } +} diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java new file mode 100644 index 00000000..580d25a5 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java @@ -0,0 +1,150 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; + +import junit.framework.TestCase; + +import com.google.gson.common.MoreAsserts; + +public final class LinkedTreeMapTest extends TestCase { + + public void testIterationOrder() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + assertIterationOrder(map.keySet(), "a", "c", "b"); + assertIterationOrder(map.values(), "android", "cola", "bbq"); + } + + public void testRemoveRootDoesNotDoubleUnlink() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + Iterator<Map.Entry<String,String>> it = map.entrySet().iterator(); + it.next(); + it.next(); + it.next(); + it.remove(); + assertIterationOrder(map.keySet(), "a", "c"); + } + + public void testPutNullKeyFails() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + try { + map.put(null, "android"); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testPutNonComparableKeyFails() { + LinkedTreeMap<Object, String> map = new LinkedTreeMap<Object, String>(); + try { + map.put(new Object(), "android"); + fail(); + } catch (ClassCastException expected) {} + } + + public void testContainsNonComparableKeyReturnsFalse() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", "android"); + assertFalse(map.containsKey(new Object())); + } + + public void testContainsNullKeyIsAlwaysFalse() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", "android"); + assertFalse(map.containsKey(null)); + } + + public void testPutOverrides() throws Exception { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + assertNull(map.put("d", "donut")); + assertNull(map.put("e", "eclair")); + assertNull(map.put("f", "froyo")); + assertEquals(3, map.size()); + + assertEquals("donut", map.get("d")); + assertEquals("donut", map.put("d", "done")); + assertEquals(3, map.size()); + } + + public void testEmptyStringValues() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", ""); + assertTrue(map.containsKey("a")); + assertEquals("", map.get("a")); + } + + public void testLargeSetOfRandomKeys() throws Exception { + Random random = new Random(1367593214724L); + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + String[] keys = new String[1000]; + for (int i = 0; i < keys.length; i++) { + keys[i] = Integer.toString(Math.abs(random.nextInt()), 36) + "-" + i; + map.put(keys[i], "" + i); + } + + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + assertTrue(map.containsKey(key)); + assertEquals("" + i, map.get(key)); + } + } + + public void testClear() { + LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>(); + map.put("a", "android"); + map.put("c", "cola"); + map.put("b", "bbq"); + map.clear(); + assertIterationOrder(map.keySet()); + assertEquals(0, map.size()); + } + + public void testEqualsAndHashCode() throws Exception { + LinkedTreeMap<String, Integer> map1 = new LinkedTreeMap<String, Integer>(); + map1.put("A", 1); + map1.put("B", 2); + map1.put("C", 3); + map1.put("D", 4); + + LinkedTreeMap<String, Integer> map2 = new LinkedTreeMap<String, Integer>(); + map2.put("C", 3); + map2.put("B", 2); + map2.put("D", 4); + map2.put("A", 1); + + MoreAsserts.assertEqualsAndHashCode(map1, map2); + } + + private <T> void assertIterationOrder(Iterable<T> actual, T... expected) { + ArrayList<T> actualList = new ArrayList<T>(); + for (T t : actual) { + actualList.add(t); + } + assertEquals(Arrays.asList(expected), actualList); + } +} diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java new file mode 100644 index 00000000..10624711 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java @@ -0,0 +1,312 @@ +/* + * 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.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonToken; +import java.io.IOException; +import junit.framework.TestCase; + +@SuppressWarnings("resource") +public final class JsonElementReaderTest extends TestCase { + + public void testNumbers() throws IOException { + JsonElement element = new JsonParser().parse("[1, 2, 3]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals(1, reader.nextInt()); + assertEquals(2L, reader.nextLong()); + assertEquals(3.0, reader.nextDouble()); + reader.endArray(); + } + + public void testLenientNansAndInfinities() throws IOException { + JsonElement element = new JsonParser().parse("[NaN, -Infinity, Infinity]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.setLenient(true); + reader.beginArray(); + assertTrue(Double.isNaN(reader.nextDouble())); + assertEquals(Double.NEGATIVE_INFINITY, reader.nextDouble()); + assertEquals(Double.POSITIVE_INFINITY, reader.nextDouble()); + reader.endArray(); + } + + public void testStrictNansAndInfinities() throws IOException { + JsonElement element = new JsonParser().parse("[NaN, -Infinity, Infinity]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.setLenient(false); + reader.beginArray(); + try { + reader.nextDouble(); + fail(); + } catch (NumberFormatException e) { + } + assertEquals("NaN", reader.nextString()); + try { + reader.nextDouble(); + fail(); + } catch (NumberFormatException e) { + } + assertEquals("-Infinity", reader.nextString()); + try { + reader.nextDouble(); + fail(); + } catch (NumberFormatException e) { + } + assertEquals("Infinity", reader.nextString()); + reader.endArray(); + } + + public void testNumbersFromStrings() throws IOException { + JsonElement element = new JsonParser().parse("[\"1\", \"2\", \"3\"]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals(1, reader.nextInt()); + assertEquals(2L, reader.nextLong()); + assertEquals(3.0, reader.nextDouble()); + reader.endArray(); + } + + public void testStringsFromNumbers() throws IOException { + JsonElement element = new JsonParser().parse("[1]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals("1", reader.nextString()); + reader.endArray(); + } + + public void testBooleans() throws IOException { + JsonElement element = new JsonParser().parse("[true, false]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + assertEquals(false, reader.nextBoolean()); + reader.endArray(); + } + + public void testNulls() throws IOException { + JsonElement element = new JsonParser().parse("[null,null]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + reader.nextNull(); + reader.nextNull(); + reader.endArray(); + } + + public void testStrings() throws IOException { + JsonElement element = new JsonParser().parse("[\"A\",\"B\"]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals("A", reader.nextString()); + assertEquals("B", reader.nextString()); + reader.endArray(); + } + + public void testArray() throws IOException { + JsonElement element = new JsonParser().parse("[1, 2, 3]"); + JsonTreeReader reader = new JsonTreeReader(element); + assertEquals(JsonToken.BEGIN_ARRAY, reader.peek()); + reader.beginArray(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals(1, reader.nextInt()); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals(2, reader.nextInt()); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals(3, reader.nextInt()); + assertEquals(JsonToken.END_ARRAY, reader.peek()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testObject() throws IOException { + JsonElement element = new JsonParser().parse("{\"A\": 1, \"B\": 2}"); + JsonTreeReader reader = new JsonTreeReader(element); + assertEquals(JsonToken.BEGIN_OBJECT, reader.peek()); + reader.beginObject(); + assertEquals(JsonToken.NAME, reader.peek()); + assertEquals("A", reader.nextName()); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals(1, reader.nextInt()); + assertEquals(JsonToken.NAME, reader.peek()); + assertEquals("B", reader.nextName()); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals(2, reader.nextInt()); + assertEquals(JsonToken.END_OBJECT, reader.peek()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testEmptyArray() throws IOException { + JsonElement element = new JsonParser().parse("[]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + reader.endArray(); + } + + public void testNestedArrays() throws IOException { + JsonElement element = new JsonParser().parse("[[],[[]]]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + reader.beginArray(); + reader.endArray(); + reader.beginArray(); + reader.beginArray(); + reader.endArray(); + reader.endArray(); + reader.endArray(); + } + + public void testNestedObjects() throws IOException { + JsonElement element = new JsonParser().parse("{\"A\":{},\"B\":{\"C\":{}}}"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginObject(); + assertEquals("A", reader.nextName()); + reader.beginObject(); + reader.endObject(); + assertEquals("B", reader.nextName()); + reader.beginObject(); + assertEquals("C", reader.nextName()); + reader.beginObject(); + reader.endObject(); + reader.endObject(); + reader.endObject(); + } + + public void testEmptyObject() throws IOException { + JsonElement element = new JsonParser().parse("{}"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginObject(); + reader.endObject(); + } + + public void testSkipValue() throws IOException { + JsonElement element = new JsonParser().parse("[\"A\",{\"B\":[[]]},\"C\",[[]],\"D\",null]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + assertEquals("A", reader.nextString()); + reader.skipValue(); + assertEquals("C", reader.nextString()); + reader.skipValue(); + assertEquals("D", reader.nextString()); + reader.skipValue(); + reader.endArray(); + } + + public void testWrongType() throws IOException { + JsonElement element = new JsonParser().parse("[[],\"A\"]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + try { + reader.nextBoolean(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextNull(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextString(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextInt(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextLong(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextDouble(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + reader.beginArray(); + reader.endArray(); + + try { + reader.nextBoolean(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextNull(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextInt(); + fail(); + } catch (NumberFormatException expected) { + } + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException expected) { + } + try { + reader.nextDouble(); + fail(); + } catch (NumberFormatException expected) { + } + try { + reader.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals("A", reader.nextString()); + reader.endArray(); + } + + public void testEarlyClose() throws IOException { + JsonElement element = new JsonParser().parse("[1, 2, 3]"); + JsonTreeReader reader = new JsonTreeReader(element); + reader.beginArray(); + reader.close(); + try { + reader.peek(); + fail(); + } catch (IllegalStateException expected) { + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java new file mode 100644 index 00000000..e07014d3 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -0,0 +1,175 @@ +/* + * 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.JsonNull; +import java.io.IOException; +import junit.framework.TestCase; + +@SuppressWarnings("resource") +public final class JsonTreeWriterTest extends TestCase { + public void testArray() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.beginArray(); + writer.value(1); + writer.value(2); + writer.value(3); + writer.endArray(); + assertEquals("[1,2,3]", writer.get().toString()); + } + + public void testNestedArray() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.beginArray(); + writer.beginArray(); + writer.endArray(); + writer.beginArray(); + writer.beginArray(); + writer.endArray(); + writer.endArray(); + writer.endArray(); + assertEquals("[[],[[]]]", writer.get().toString()); + } + + public void testObject() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.beginObject(); + writer.name("A").value(1); + writer.name("B").value(2); + writer.endObject(); + assertEquals("{\"A\":1,\"B\":2}", writer.get().toString()); + } + + public void testNestedObject() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.beginObject(); + writer.name("A"); + writer.beginObject(); + writer.name("B"); + writer.beginObject(); + writer.endObject(); + writer.endObject(); + writer.name("C"); + writer.beginObject(); + writer.endObject(); + writer.endObject(); + assertEquals("{\"A\":{\"B\":{}},\"C\":{}}", writer.get().toString()); + } + + public void testWriteAfterClose() throws Exception { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setLenient(true); + writer.beginArray(); + writer.value("A"); + writer.endArray(); + writer.close(); + try { + writer.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testPrematureClose() throws Exception { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setLenient(true); + writer.beginArray(); + try { + writer.close(); + fail(); + } catch (IOException expected) { + } + } + + public void testSerializeNullsFalse() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setSerializeNulls(false); + writer.beginObject(); + writer.name("A"); + writer.nullValue(); + writer.endObject(); + assertEquals("{}", writer.get().toString()); + } + + public void testSerializeNullsTrue() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setSerializeNulls(true); + writer.beginObject(); + writer.name("A"); + writer.nullValue(); + writer.endObject(); + assertEquals("{\"A\":null}", writer.get().toString()); + } + + public void testEmptyWriter() { + JsonTreeWriter writer = new JsonTreeWriter(); + assertEquals(JsonNull.INSTANCE, writer.get()); + } + + public void testLenientNansAndInfinities() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setLenient(true); + writer.beginArray(); + writer.value(Double.NaN); + writer.value(Double.NEGATIVE_INFINITY); + writer.value(Double.POSITIVE_INFINITY); + writer.endArray(); + assertEquals("[NaN,-Infinity,Infinity]", writer.get().toString()); + } + + public void testStrictNansAndInfinities() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setLenient(false); + writer.beginArray(); + try { + writer.value(Double.NaN); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + writer.value(Double.NEGATIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + writer.value(Double.POSITIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testStrictBoxedNansAndInfinities() throws IOException { + JsonTreeWriter writer = new JsonTreeWriter(); + writer.setLenient(false); + writer.beginArray(); + try { + writer.value(new Double(Double.NaN)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + writer.value(new Double(Double.NEGATIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + writer.value(new Double(Double.POSITIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + } +} diff --git a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java new file mode 100644 index 00000000..cf444eee --- /dev/null +++ b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java @@ -0,0 +1,346 @@ +/* + * 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.metrics; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.Expose; +import com.google.gson.reflect.TypeToken; + +import junit.framework.TestCase; + +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests to measure performance for Gson. All tests in this file will be disabled in code. To run + * them remove disabled_ prefix from the tests and run them. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class PerformanceTest extends TestCase { + private static final int COLLECTION_SIZE = 5000; + + private static final int NUM_ITERATIONS = 100; + + private Gson gson; + + @Override + protected void setUp() throws Exception { + super.setUp(); + gson = new Gson(); + } + + public void testDummy() { + // This is here to prevent Junit for complaining when we disable all tests. + } + + public void disabled_testStringDeserialization() { + StringBuilder sb = new StringBuilder(8096); + sb.append("Error Yippie"); + + while (true) { + try { + String stackTrace = sb.toString(); + sb.append(stackTrace); + String json = "{\"message\":\"Error message.\"," + "\"stackTrace\":\"" + stackTrace + "\"}"; + parseLongJson(json); + System.out.println("Gson could handle a string of size: " + stackTrace.length()); + } catch (JsonParseException expected) { + break; + } + } + } + + private void parseLongJson(String json) throws JsonParseException { + ExceptionHolder target = gson.fromJson(json, ExceptionHolder.class); + assertTrue(target.message.contains("Error")); + assertTrue(target.stackTrace.contains("Yippie")); + } + + private static class ExceptionHolder { + public final String message; + public final String stackTrace; + + // For use by Gson + @SuppressWarnings("unused") + private ExceptionHolder() { + this("", ""); + } + public ExceptionHolder(String message, String stackTrace) { + this.message = message; + this.stackTrace = stackTrace; + } + } + + @SuppressWarnings("unused") + private static class CollectionEntry { + final String name; + final String value; + + // For use by Gson + private CollectionEntry() { + this(null, null); + } + + CollectionEntry(String name, String value) { + this.name = name; + this.value = value; + } + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 + */ + public void disabled_testLargeCollectionSerialization() { + int count = 1400000; + List<CollectionEntry> list = new ArrayList<CollectionEntry>(count); + for (int i = 0; i < count; ++i) { + list.add(new CollectionEntry("name"+i,"value"+i)); + } + gson.toJson(list); + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 + */ + public void disabled_testLargeCollectionDeserialization() { + StringBuilder sb = new StringBuilder(); + int count = 87000; + boolean first = true; + sb.append('['); + for (int i = 0; i < count; ++i) { + if (first) { + first = false; + } else { + sb.append(','); + } + sb.append("{name:'name").append(i).append("',value:'value").append(i).append("'}"); + } + sb.append(']'); + String json = sb.toString(); + Type collectionType = new TypeToken<ArrayList<CollectionEntry>>(){}.getType(); + List<CollectionEntry> list = gson.fromJson(json, collectionType); + assertEquals(count, list.size()); + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 + */ + // Last I tested, Gson was able to serialize upto 14MB byte array + public void disabled_testByteArraySerialization() { + for (int size = 4145152; true; size += 1036288) { + byte[] ba = new byte[size]; + for (int i = 0; i < size; ++i) { + ba[i] = 0x05; + } + gson.toJson(ba); + System.out.printf("Gson could serialize a byte array of size: %d\n", size); + } + } + + /** + * Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 + */ + // Last I tested, Gson was able to deserialize a byte array of 11MB + public void disable_testByteArrayDeserialization() { + for (int numElements = 10639296; true; numElements += 16384) { + StringBuilder sb = new StringBuilder(numElements*2); + sb.append("["); + boolean first = true; + for (int i = 0; i < numElements; ++i) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append("5"); + } + sb.append("]"); + String json = sb.toString(); + byte[] ba = gson.fromJson(json, byte[].class); + System.out.printf("Gson could deserialize a byte array of size: %d\n", ba.length); + } + } + +// The tests to measure serialization and deserialization performance of Gson +// Based on the discussion at +// http://groups.google.com/group/google-gson/browse_thread/thread/7a50b17a390dfaeb +// Test results: 10/19/2009 +// Serialize classes avg time: 60 ms +// Deserialized classes avg time: 70 ms +// Serialize exposed classes avg time: 159 ms +// Deserialized exposed classes avg time: 173 ms + + public void disabled_testSerializeClasses() { + ClassWithList c = new ClassWithList("str"); + for (int i = 0; i < COLLECTION_SIZE; ++i) { + c.list.add(new ClassWithField("element-" + i)); + } + StringWriter w = new StringWriter(); + long t1 = System.currentTimeMillis(); + for (int i = 0; i < NUM_ITERATIONS; ++i) { + gson.toJson(c, w); + } + long t2 = System.currentTimeMillis(); + long avg = (t2 - t1) / NUM_ITERATIONS; + System.out.printf("Serialize classes avg time: %d ms\n", avg); + } + + public void disabled_testDeserializeClasses() { + String json = buildJsonForClassWithList(); + ClassWithList[] target = new ClassWithList[NUM_ITERATIONS]; + long t1 = System.currentTimeMillis(); + for (int i = 0; i < NUM_ITERATIONS; ++i) { + target[i] = gson.fromJson(json, ClassWithList.class); + } + long t2 = System.currentTimeMillis(); + long avg = (t2 - t1) / NUM_ITERATIONS; + System.out.printf("Deserialize classes avg time: %d ms\n", avg); + } + + public void disable_testLargeObjectSerializationAndDeserialization() { + Map<String, Long> largeObject = new HashMap<String, Long>(); + for (long l = 0; l < 100000; l++) { + largeObject.put("field" + l, l); + } + + long t1 = System.currentTimeMillis(); + String json = gson.toJson(largeObject); + long t2 = System.currentTimeMillis(); + System.out.printf("Large object serialized in: %d ms\n", (t2 - t1)); + + t1 = System.currentTimeMillis(); + gson.fromJson(json, new TypeToken<Map<String, Long>>() {}.getType()); + t2 = System.currentTimeMillis(); + System.out.printf("Large object deserialized in: %d ms\n", (t2 - t1)); + + } + + public void disabled_testSerializeExposedClasses() { + ClassWithListOfObjects c1 = new ClassWithListOfObjects("str"); + for (int i1 = 0; i1 < COLLECTION_SIZE; ++i1) { + c1.list.add(new ClassWithExposedField("element-" + i1)); + } + ClassWithListOfObjects c = c1; + StringWriter w = new StringWriter(); + long t1 = System.currentTimeMillis(); + for (int i = 0; i < NUM_ITERATIONS; ++i) { + gson.toJson(c, w); + } + long t2 = System.currentTimeMillis(); + long avg = (t2 - t1) / NUM_ITERATIONS; + System.out.printf("Serialize exposed classes avg time: %d ms\n", avg); + } + + public void disabled_testDeserializeExposedClasses() { + String json = buildJsonForClassWithList(); + ClassWithListOfObjects[] target = new ClassWithListOfObjects[NUM_ITERATIONS]; + long t1 = System.currentTimeMillis(); + for (int i = 0; i < NUM_ITERATIONS; ++i) { + target[i] = gson.fromJson(json, ClassWithListOfObjects.class); + } + long t2 = System.currentTimeMillis(); + long avg = (t2 - t1) / NUM_ITERATIONS; + System.out.printf("Deserialize exposed classes avg time: %d ms\n", avg); + } + + public void disabled_testLargeGsonMapRoundTrip() throws Exception { + Map<Long, Long> original = new HashMap<Long, Long>(); + for (long i = 0; i < 1000000; i++) { + original.put(i, i + 1); + } + + Gson gson = new Gson(); + String json = gson.toJson(original); + Type longToLong = new TypeToken<Map<Long, Long>>(){}.getType(); + gson.fromJson(json, longToLong); + } + + private String buildJsonForClassWithList() { + StringBuilder sb = new StringBuilder("{"); + sb.append("field:").append("'str',"); + sb.append("list:["); + boolean first = true; + for (int i = 0; i < COLLECTION_SIZE; ++i) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append("{field:'element-" + i + "'}"); + } + sb.append("]"); + sb.append("}"); + String json = sb.toString(); + return json; + } + + @SuppressWarnings("unused") + private static final class ClassWithList { + final String field; + final List<ClassWithField> list = new ArrayList<ClassWithField>(COLLECTION_SIZE); + ClassWithList() { + this(null); + } + ClassWithList(String field) { + this.field = field; + } + } + + @SuppressWarnings("unused") + private static final class ClassWithField { + final String field; + ClassWithField() { + this(""); + } + public ClassWithField(String field) { + this.field = field; + } + } + + @SuppressWarnings("unused") + private static final class ClassWithListOfObjects { + @Expose + final String field; + @Expose + final List<ClassWithExposedField> list = new ArrayList<ClassWithExposedField>(COLLECTION_SIZE); + ClassWithListOfObjects() { + this(null); + } + ClassWithListOfObjects(String field) { + this.field = field; + } + } + + @SuppressWarnings("unused") + private static final class ClassWithExposedField { + @Expose + final String field; + ClassWithExposedField() { + this(""); + } + ClassWithExposedField(String field) { + this.field = field; + } + } +} diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java new file mode 100644 index 00000000..7dda9d47 --- /dev/null +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -0,0 +1,82 @@ +/* + * 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.reflect; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.RandomAccess; +import java.util.Set; +import junit.framework.TestCase; + +/** + * @author Jesse Wilson + */ +@SuppressWarnings({"deprecation"}) +public final class TypeTokenTest extends TestCase { + + List<Integer> listOfInteger = null; + List<Number> listOfNumber = null; + List<String> listOfString = null; + List<?> listOfUnknown = null; + List<Set<String>> listOfSetOfString = null; + List<Set<?>> listOfSetOfUnknown = null; + + public void testIsAssignableFromRawTypes() { + assertTrue(TypeToken.get(Object.class).isAssignableFrom(String.class)); + assertFalse(TypeToken.get(String.class).isAssignableFrom(Object.class)); + assertTrue(TypeToken.get(RandomAccess.class).isAssignableFrom(ArrayList.class)); + assertFalse(TypeToken.get(ArrayList.class).isAssignableFrom(RandomAccess.class)); + } + + public void testIsAssignableFromWithTypeParameters() throws Exception { + Type a = getClass().getDeclaredField("listOfInteger").getGenericType(); + Type b = getClass().getDeclaredField("listOfNumber").getGenericType(); + assertTrue(TypeToken.get(a).isAssignableFrom(a)); + assertTrue(TypeToken.get(b).isAssignableFrom(b)); + + // listOfInteger = listOfNumber; // doesn't compile; must be false + assertFalse(TypeToken.get(a).isAssignableFrom(b)); + // listOfNumber = listOfInteger; // doesn't compile; must be false + assertFalse(TypeToken.get(b).isAssignableFrom(a)); + } + + public void testIsAssignableFromWithBasicWildcards() throws Exception { + Type a = getClass().getDeclaredField("listOfString").getGenericType(); + Type b = getClass().getDeclaredField("listOfUnknown").getGenericType(); + assertTrue(TypeToken.get(a).isAssignableFrom(a)); + assertTrue(TypeToken.get(b).isAssignableFrom(b)); + + // listOfString = listOfUnknown // doesn't compile; must be false + assertFalse(TypeToken.get(a).isAssignableFrom(b)); + listOfUnknown = listOfString; // compiles; must be true + // The following assertion is too difficult to support reliably, so disabling + // assertTrue(TypeToken.get(b).isAssignableFrom(a)); + } + + public void testIsAssignableFromWithNestedWildcards() throws Exception { + Type a = getClass().getDeclaredField("listOfSetOfString").getGenericType(); + Type b = getClass().getDeclaredField("listOfSetOfUnknown").getGenericType(); + assertTrue(TypeToken.get(a).isAssignableFrom(a)); + assertTrue(TypeToken.get(b).isAssignableFrom(b)); + + // listOfSetOfString = listOfSetOfUnknown; // doesn't compile; must be false + assertFalse(TypeToken.get(a).isAssignableFrom(b)); + // listOfSetOfUnknown = listOfSetOfString; // doesn't compile; must be false + assertFalse(TypeToken.get(b).isAssignableFrom(a)); + } +} diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java new file mode 100644 index 00000000..50661664 --- /dev/null +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -0,0 +1,217 @@ +/* + * 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.stream; + +import java.io.IOException; +import java.io.StringReader; +import junit.framework.TestCase; + +@SuppressWarnings("resource") +public class JsonReaderPathTest extends TestCase { + public void testPath() throws IOException { + JsonReader reader = new JsonReader( + new StringReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}")); + assertEquals("$", reader.getPath()); + reader.beginObject(); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPath()); + reader.beginArray(); + assertEquals("$.a[0]", reader.getPath()); + reader.nextInt(); + assertEquals("$.a[1]", reader.getPath()); + reader.nextBoolean(); + assertEquals("$.a[2]", reader.getPath()); + reader.nextBoolean(); + assertEquals("$.a[3]", reader.getPath()); + reader.nextNull(); + assertEquals("$.a[4]", reader.getPath()); + reader.nextString(); + assertEquals("$.a[5]", reader.getPath()); + reader.beginObject(); + assertEquals("$.a[5].", reader.getPath()); + reader.nextName(); + assertEquals("$.a[5].c", reader.getPath()); + reader.nextString(); + assertEquals("$.a[5].c", reader.getPath()); + reader.endObject(); + assertEquals("$.a[6]", reader.getPath()); + reader.beginArray(); + assertEquals("$.a[6][0]", reader.getPath()); + reader.nextInt(); + assertEquals("$.a[6][1]", reader.getPath()); + reader.endArray(); + assertEquals("$.a[7]", reader.getPath()); + reader.endArray(); + assertEquals("$.a", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPath()); + } + + public void testObjectPath() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}")); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.beginObject(); + assertEquals("$.", reader.getPath()); + + reader.peek(); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPath()); + + reader.peek(); + assertEquals("$.a", reader.getPath()); + reader.nextInt(); + assertEquals("$.a", reader.getPath()); + + reader.peek(); + assertEquals("$.a", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPath()); + + reader.peek(); + assertEquals("$.b", reader.getPath()); + reader.nextInt(); + assertEquals("$.b", reader.getPath()); + + reader.peek(); + assertEquals("$.b", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.close(); + assertEquals("$", reader.getPath()); + } + + public void testArrayPath() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[1,2]")); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.beginArray(); + assertEquals("$[0]", reader.getPath()); + + reader.peek(); + assertEquals("$[0]", reader.getPath()); + reader.nextInt(); + assertEquals("$[1]", reader.getPath()); + + reader.peek(); + assertEquals("$[1]", reader.getPath()); + reader.nextInt(); + assertEquals("$[2]", reader.getPath()); + + reader.peek(); + assertEquals("$[2]", reader.getPath()); + reader.endArray(); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.close(); + assertEquals("$", reader.getPath()); + } + + public void testMultipleTopLevelValuesInOneDocument() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[][]")); + reader.setLenient(true); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPath()); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPath()); + } + + public void testSkipArrayElements() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[1,2,3]")); + reader.beginArray(); + reader.skipValue(); + reader.skipValue(); + assertEquals("$[2]", reader.getPath()); + } + + public void testSkipObjectNames() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1}")); + reader.beginObject(); + reader.skipValue(); + assertEquals("$.null", reader.getPath()); + } + + public void testSkipObjectValues() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}")); + reader.beginObject(); + reader.nextName(); + reader.skipValue(); + assertEquals("$.null", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPath()); + } + + public void testSkipNestedStructures() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[[1,2,3],4]")); + reader.beginArray(); + reader.skipValue(); + assertEquals("$[1]", reader.getPath()); + } + + public void testArrayOfObjects() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[{},{},{}]")); + reader.beginArray(); + assertEquals("$[0]", reader.getPath()); + reader.beginObject(); + assertEquals("$[0].", reader.getPath()); + reader.endObject(); + assertEquals("$[1]", reader.getPath()); + reader.beginObject(); + assertEquals("$[1].", reader.getPath()); + reader.endObject(); + assertEquals("$[2]", reader.getPath()); + reader.beginObject(); + assertEquals("$[2].", reader.getPath()); + reader.endObject(); + assertEquals("$[3]", reader.getPath()); + reader.endArray(); + assertEquals("$", reader.getPath()); + } + + public void testArrayOfArrays() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[[],[],[]]")); + reader.beginArray(); + assertEquals("$[0]", reader.getPath()); + reader.beginArray(); + assertEquals("$[0][0]", reader.getPath()); + reader.endArray(); + assertEquals("$[1]", reader.getPath()); + reader.beginArray(); + assertEquals("$[1][0]", reader.getPath()); + reader.endArray(); + assertEquals("$[2]", reader.getPath()); + reader.beginArray(); + assertEquals("$[2][0]", reader.getPath()); + reader.endArray(); + assertEquals("$[3]", reader.getPath()); + reader.endArray(); + assertEquals("$", reader.getPath()); + } +} diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java new file mode 100644 index 00000000..72c9aa4c --- /dev/null +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -0,0 +1,1775 @@ +/* + * 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.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Arrays; +import junit.framework.TestCase; + +import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; +import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; +import static com.google.gson.stream.JsonToken.BOOLEAN; +import static com.google.gson.stream.JsonToken.END_ARRAY; +import static com.google.gson.stream.JsonToken.END_OBJECT; +import static com.google.gson.stream.JsonToken.NAME; +import static com.google.gson.stream.JsonToken.NULL; +import static com.google.gson.stream.JsonToken.NUMBER; +import static com.google.gson.stream.JsonToken.STRING; + +@SuppressWarnings("resource") +public final class JsonReaderTest extends TestCase { + public void testReadArray() throws IOException { + JsonReader reader = new JsonReader(reader("[true, true]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + assertEquals(true, reader.nextBoolean()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testReadEmptyArray() throws IOException { + JsonReader reader = new JsonReader(reader("[]")); + reader.beginArray(); + assertFalse(reader.hasNext()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testReadObject() throws IOException { + JsonReader reader = new JsonReader(reader( + "{\"a\": \"android\", \"b\": \"banana\"}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals("android", reader.nextString()); + assertEquals("b", reader.nextName()); + assertEquals("banana", reader.nextString()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testReadEmptyObject() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + assertFalse(reader.hasNext()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipArray() throws IOException { + JsonReader reader = new JsonReader(reader( + "{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + reader.skipValue(); + assertEquals("b", reader.nextName()); + assertEquals(123, reader.nextInt()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipArrayAfterPeek() throws Exception { + JsonReader reader = new JsonReader(reader( + "{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals(BEGIN_ARRAY, reader.peek()); + reader.skipValue(); + assertEquals("b", reader.nextName()); + assertEquals(123, reader.nextInt()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipTopLevelObject() throws Exception { + JsonReader reader = new JsonReader(reader( + "{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}")); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipObject() throws IOException { + JsonReader reader = new JsonReader(reader( + "{\"a\": { \"c\": [], \"d\": [true, true, {}] }, \"b\": \"banana\"}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + reader.skipValue(); + assertEquals("b", reader.nextName()); + reader.skipValue(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipObjectAfterPeek() throws Exception { + String json = "{" + " \"one\": { \"num\": 1 }" + + ", \"two\": { \"num\": 2 }" + ", \"three\": { \"num\": 3 }" + "}"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginObject(); + assertEquals("one", reader.nextName()); + assertEquals(BEGIN_OBJECT, reader.peek()); + reader.skipValue(); + assertEquals("two", reader.nextName()); + assertEquals(BEGIN_OBJECT, reader.peek()); + reader.skipValue(); + assertEquals("three", reader.nextName()); + reader.skipValue(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipInteger() throws IOException { + JsonReader reader = new JsonReader(reader( + "{\"a\":123456789,\"b\":-123456789}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + reader.skipValue(); + assertEquals("b", reader.nextName()); + reader.skipValue(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipDouble() throws IOException { + JsonReader reader = new JsonReader(reader( + "{\"a\":-123.456e-789,\"b\":123456789.0}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + reader.skipValue(); + assertEquals("b", reader.nextName()); + reader.skipValue(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testHelloWorld() throws IOException { + String json = "{\n" + + " \"hello\": true,\n" + + " \"foo\": [\"world\"]\n" + + "}"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginObject(); + assertEquals("hello", reader.nextName()); + assertEquals(true, reader.nextBoolean()); + assertEquals("foo", reader.nextName()); + reader.beginArray(); + assertEquals("world", reader.nextString()); + reader.endArray(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testNulls() { + try { + new JsonReader(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testEmptyString() { + try { + new JsonReader(reader("")).beginArray(); + fail(); + } catch (IOException expected) { + } + try { + new JsonReader(reader("")).beginObject(); + fail(); + } catch (IOException expected) { + } + } + + public void testNoTopLevelObject() { + try { + new JsonReader(reader("true")).nextBoolean(); + fail(); + } catch (IOException expected) { + } + } + + public void testCharacterUnescaping() throws IOException { + String json = "[\"a\"," + + "\"a\\\"\"," + + "\"\\\"\"," + + "\":\"," + + "\",\"," + + "\"\\b\"," + + "\"\\f\"," + + "\"\\n\"," + + "\"\\r\"," + + "\"\\t\"," + + "\" \"," + + "\"\\\\\"," + + "\"{\"," + + "\"}\"," + + "\"[\"," + + "\"]\"," + + "\"\\u0000\"," + + "\"\\u0019\"," + + "\"\\u20AC\"" + + "]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + assertEquals("a", reader.nextString()); + assertEquals("a\"", reader.nextString()); + assertEquals("\"", reader.nextString()); + assertEquals(":", reader.nextString()); + assertEquals(",", reader.nextString()); + assertEquals("\b", reader.nextString()); + assertEquals("\f", reader.nextString()); + assertEquals("\n", reader.nextString()); + assertEquals("\r", reader.nextString()); + assertEquals("\t", reader.nextString()); + assertEquals(" ", reader.nextString()); + assertEquals("\\", reader.nextString()); + assertEquals("{", reader.nextString()); + assertEquals("}", reader.nextString()); + assertEquals("[", reader.nextString()); + assertEquals("]", reader.nextString()); + assertEquals("\0", reader.nextString()); + assertEquals("\u0019", reader.nextString()); + assertEquals("\u20AC", reader.nextString()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testUnescapingInvalidCharacters() throws IOException { + String json = "[\"\\u000g\"]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (NumberFormatException expected) { + } + } + + public void testUnescapingTruncatedCharacters() throws IOException { + String json = "[\"\\u000"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testUnescapingTruncatedSequence() throws IOException { + String json = "[\"\\"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testIntegersWithFractionalPartSpecified() throws IOException { + JsonReader reader = new JsonReader(reader("[1.0,1.0,1.0]")); + reader.beginArray(); + assertEquals(1.0, reader.nextDouble()); + assertEquals(1, reader.nextInt()); + assertEquals(1L, reader.nextLong()); + } + + public void testDoubles() throws IOException { + String json = "[-0.0," + + "1.0," + + "1.7976931348623157E308," + + "4.9E-324," + + "0.0," + + "-0.5," + + "2.2250738585072014E-308," + + "3.141592653589793," + + "2.718281828459045]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + assertEquals(-0.0, reader.nextDouble()); + assertEquals(1.0, reader.nextDouble()); + assertEquals(1.7976931348623157E308, reader.nextDouble()); + assertEquals(4.9E-324, reader.nextDouble()); + assertEquals(0.0, reader.nextDouble()); + assertEquals(-0.5, reader.nextDouble()); + assertEquals(2.2250738585072014E-308, reader.nextDouble()); + assertEquals(3.141592653589793, reader.nextDouble()); + assertEquals(2.718281828459045, reader.nextDouble()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testStrictNonFiniteDoubles() throws IOException { + String json = "[NaN]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.nextDouble(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testStrictQuotedNonFiniteDoubles() throws IOException { + String json = "[\"NaN\"]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.nextDouble(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testLenientNonFiniteDoubles() throws IOException { + String json = "[NaN, -Infinity, Infinity]"; + JsonReader reader = new JsonReader(reader(json)); + reader.setLenient(true); + reader.beginArray(); + assertTrue(Double.isNaN(reader.nextDouble())); + assertEquals(Double.NEGATIVE_INFINITY, reader.nextDouble()); + assertEquals(Double.POSITIVE_INFINITY, reader.nextDouble()); + reader.endArray(); + } + + public void testLenientQuotedNonFiniteDoubles() throws IOException { + String json = "[\"NaN\", \"-Infinity\", \"Infinity\"]"; + JsonReader reader = new JsonReader(reader(json)); + reader.setLenient(true); + reader.beginArray(); + assertTrue(Double.isNaN(reader.nextDouble())); + assertEquals(Double.NEGATIVE_INFINITY, reader.nextDouble()); + assertEquals(Double.POSITIVE_INFINITY, reader.nextDouble()); + reader.endArray(); + } + + public void testStrictNonFiniteDoublesWithSkipValue() throws IOException { + String json = "[NaN]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testLongs() throws IOException { + String json = "[0,0,0," + + "1,1,1," + + "-1,-1,-1," + + "-9223372036854775808," + + "9223372036854775807]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + assertEquals(0L, reader.nextLong()); + assertEquals(0, reader.nextInt()); + assertEquals(0.0, reader.nextDouble()); + assertEquals(1L, reader.nextLong()); + assertEquals(1, reader.nextInt()); + assertEquals(1.0, reader.nextDouble()); + assertEquals(-1L, reader.nextLong()); + assertEquals(-1, reader.nextInt()); + assertEquals(-1.0, reader.nextDouble()); + try { + reader.nextInt(); + fail(); + } catch (NumberFormatException expected) { + } + assertEquals(Long.MIN_VALUE, reader.nextLong()); + try { + reader.nextInt(); + fail(); + } catch (NumberFormatException expected) { + } + assertEquals(Long.MAX_VALUE, reader.nextLong()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void disabled_testNumberWithOctalPrefix() throws IOException { + String json = "[01]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + try { + reader.peek(); + fail(); + } catch (MalformedJsonException expected) { + } + try { + reader.nextInt(); + fail(); + } catch (MalformedJsonException expected) { + } + try { + reader.nextLong(); + fail(); + } catch (MalformedJsonException expected) { + } + try { + reader.nextDouble(); + fail(); + } catch (MalformedJsonException expected) { + } + assertEquals("01", reader.nextString()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testBooleans() throws IOException { + JsonReader reader = new JsonReader(reader("[true,false]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + assertEquals(false, reader.nextBoolean()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testPeekingUnquotedStringsPrefixedWithBooleans() throws IOException { + JsonReader reader = new JsonReader(reader("[truey]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + try { + reader.nextBoolean(); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals("truey", reader.nextString()); + reader.endArray(); + } + + public void testMalformedNumbers() throws IOException { + assertNotANumber("-"); + assertNotANumber("."); + + // exponent lacks digit + assertNotANumber("e"); + assertNotANumber("0e"); + assertNotANumber(".e"); + assertNotANumber("0.e"); + assertNotANumber("-.0e"); + + // no integer + assertNotANumber("e1"); + assertNotANumber(".e1"); + assertNotANumber("-e1"); + + // trailing characters + assertNotANumber("1x"); + assertNotANumber("1.1x"); + assertNotANumber("1e1x"); + assertNotANumber("1ex"); + assertNotANumber("1.1ex"); + assertNotANumber("1.1e1x"); + + // fraction has no digit + assertNotANumber("0."); + assertNotANumber("-0."); + assertNotANumber("0.e1"); + assertNotANumber("-0.e1"); + + // no leading digit + assertNotANumber(".0"); + assertNotANumber("-.0"); + assertNotANumber(".0e1"); + assertNotANumber("-.0e1"); + } + + private void assertNotANumber(String s) throws IOException { + JsonReader reader = new JsonReader(reader("[" + s + "]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(JsonToken.STRING, reader.peek()); + assertEquals(s, reader.nextString()); + reader.endArray(); + } + + public void testPeekingUnquotedStringsPrefixedWithIntegers() throws IOException { + JsonReader reader = new JsonReader(reader("[12.34e5x]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + try { + reader.nextInt(); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals("12.34e5x", reader.nextString()); + } + + public void testPeekLongMinValue() throws IOException { + JsonReader reader = new JsonReader(reader("[-9223372036854775808]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + assertEquals(-9223372036854775808L, reader.nextLong()); + } + + public void testPeekLongMaxValue() throws IOException { + JsonReader reader = new JsonReader(reader("[9223372036854775807]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + assertEquals(9223372036854775807L, reader.nextLong()); + } + + public void testLongLargerThanMaxLongThatWrapsAround() throws IOException { + JsonReader reader = new JsonReader(reader("[22233720368547758070]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException expected) { + } + } + + public void testLongLargerThanMinLongThatWrapsAround() throws IOException { + JsonReader reader = new JsonReader(reader("[-22233720368547758070]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException expected) { + } + } + + /** + * This test fails because there's no double for 9223372036854775808, and our + * long parsing uses Double.parseDouble() for fractional values. + */ + public void disabled_testPeekLargerThanLongMaxValue() throws IOException { + JsonReader reader = new JsonReader(reader("[9223372036854775808]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException e) { + } + } + + /** + * This test fails because there's no double for -9223372036854775809, and our + * long parsing uses Double.parseDouble() for fractional values. + */ + public void disabled_testPeekLargerThanLongMinValue() throws IOException { + JsonReader reader = new JsonReader(reader("[-9223372036854775809]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException expected) { + } + assertEquals(-9223372036854775809d, reader.nextDouble()); + } + + /** + * This test fails because there's no double for 9223372036854775806, and + * our long parsing uses Double.parseDouble() for fractional values. + */ + public void disabled_testHighPrecisionLong() throws IOException { + String json = "[9223372036854775806.000]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + assertEquals(9223372036854775806L, reader.nextLong()); + reader.endArray(); + } + + public void testPeekMuchLargerThanLongMinValue() throws IOException { + JsonReader reader = new JsonReader(reader("[-92233720368547758080]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(NUMBER, reader.peek()); + try { + reader.nextLong(); + fail(); + } catch (NumberFormatException expected) { + } + assertEquals(-92233720368547758080d, reader.nextDouble()); + } + + public void testQuotedNumberWithEscape() throws IOException { + JsonReader reader = new JsonReader(reader("[\"12\u00334\"]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + assertEquals(1234, reader.nextInt()); + } + + public void testMixedCaseLiterals() throws IOException { + JsonReader reader = new JsonReader(reader("[True,TruE,False,FALSE,NULL,nulL]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + assertEquals(true, reader.nextBoolean()); + assertEquals(false, reader.nextBoolean()); + assertEquals(false, reader.nextBoolean()); + reader.nextNull(); + reader.nextNull(); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testMissingValue() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testPrematureEndOfInput() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":true,")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals(true, reader.nextBoolean()); + try { + reader.nextName(); + fail(); + } catch (IOException expected) { + } + } + + public void testPrematurelyClosed() throws IOException { + try { + JsonReader reader = new JsonReader(reader("{\"a\":[]}")); + reader.beginObject(); + reader.close(); + reader.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + + try { + JsonReader reader = new JsonReader(reader("{\"a\":[]}")); + reader.close(); + reader.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + + try { + JsonReader reader = new JsonReader(reader("{\"a\":true}")); + reader.beginObject(); + reader.nextName(); + reader.peek(); + reader.close(); + reader.nextBoolean(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNextFailuresDoNotAdvance() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":true}")); + reader.beginObject(); + try { + reader.nextString(); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals("a", reader.nextName()); + try { + reader.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals(true, reader.nextBoolean()); + try { + reader.nextString(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.nextName(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + reader.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + reader.close(); + } + + public void testIntegerMismatchFailuresDoNotAdvance() throws IOException { + JsonReader reader = new JsonReader(reader("[1.5]")); + reader.beginArray(); + try { + reader.nextInt(); + fail(); + } catch (NumberFormatException expected) { + } + assertEquals(1.5d, reader.nextDouble()); + reader.endArray(); + } + + public void testStringNullIsNotNull() throws IOException { + JsonReader reader = new JsonReader(reader("[\"null\"]")); + reader.beginArray(); + try { + reader.nextNull(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNullLiteralIsNotAString() throws IOException { + JsonReader reader = new JsonReader(reader("[null]")); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testStrictNameValueSeparator() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\"=true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("{\"a\"=>true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientNameValueSeparator() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\"=true}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals(true, reader.nextBoolean()); + + reader = new JsonReader(reader("{\"a\"=>true}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals(true, reader.nextBoolean()); + } + + public void testStrictNameValueSeparatorWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\"=true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("{\"a\"=>true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testCommentsInStringValue() throws Exception { + JsonReader reader = new JsonReader(reader("[\"// comment\"]")); + reader.beginArray(); + assertEquals("// comment", reader.nextString()); + reader.endArray(); + + reader = new JsonReader(reader("{\"a\":\"#someComment\"}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals("#someComment", reader.nextString()); + reader.endObject(); + + reader = new JsonReader(reader("{\"#//a\":\"#some //Comment\"}")); + reader.beginObject(); + assertEquals("#//a", reader.nextName()); + assertEquals("#some //Comment", reader.nextString()); + reader.endObject(); + } + + public void testStrictComments() throws IOException { + JsonReader reader = new JsonReader(reader("[// comment \n true]")); + reader.beginArray(); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[# comment \n true]")); + reader.beginArray(); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[/* comment */ true]")); + reader.beginArray(); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientComments() throws IOException { + JsonReader reader = new JsonReader(reader("[// comment \n true]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + + reader = new JsonReader(reader("[# comment \n true]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + + reader = new JsonReader(reader("[/* comment */ true]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + } + + public void testStrictCommentsWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("[// comment \n true]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[# comment \n true]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[/* comment */ true]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictUnquotedNames() throws IOException { + JsonReader reader = new JsonReader(reader("{a:true}")); + reader.beginObject(); + try { + reader.nextName(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientUnquotedNames() throws IOException { + JsonReader reader = new JsonReader(reader("{a:true}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + } + + public void testStrictUnquotedNamesWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("{a:true}")); + reader.beginObject(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictSingleQuotedNames() throws IOException { + JsonReader reader = new JsonReader(reader("{'a':true}")); + reader.beginObject(); + try { + reader.nextName(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientSingleQuotedNames() throws IOException { + JsonReader reader = new JsonReader(reader("{'a':true}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + } + + public void testStrictSingleQuotedNamesWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("{'a':true}")); + reader.beginObject(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictUnquotedStrings() throws IOException { + JsonReader reader = new JsonReader(reader("[a]")); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testStrictUnquotedStringsWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("[a]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testLenientUnquotedStrings() throws IOException { + JsonReader reader = new JsonReader(reader("[a]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals("a", reader.nextString()); + } + + public void testStrictSingleQuotedStrings() throws IOException { + JsonReader reader = new JsonReader(reader("['a']")); + reader.beginArray(); + try { + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientSingleQuotedStrings() throws IOException { + JsonReader reader = new JsonReader(reader("['a']")); + reader.setLenient(true); + reader.beginArray(); + assertEquals("a", reader.nextString()); + } + + public void testStrictSingleQuotedStringsWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("['a']")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictSemicolonDelimitedArray() throws IOException { + JsonReader reader = new JsonReader(reader("[true;true]")); + reader.beginArray(); + try { + reader.nextBoolean(); + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientSemicolonDelimitedArray() throws IOException { + JsonReader reader = new JsonReader(reader("[true;true]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + assertEquals(true, reader.nextBoolean()); + } + + public void testStrictSemicolonDelimitedArrayWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("[true;true]")); + reader.beginArray(); + try { + reader.skipValue(); + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictSemicolonDelimitedNameValuePair() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.nextBoolean(); + reader.nextName(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientSemicolonDelimitedNameValuePair() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals(true, reader.nextBoolean()); + assertEquals("b", reader.nextName()); + } + + public void testStrictSemicolonDelimitedNameValuePairWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + try { + reader.skipValue(); + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictUnnecessaryArraySeparators() throws IOException { + JsonReader reader = new JsonReader(reader("[true,,true]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + try { + reader.nextNull(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[,true]")); + reader.beginArray(); + try { + reader.nextNull(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[true,]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + try { + reader.nextNull(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[,]")); + reader.beginArray(); + try { + reader.nextNull(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientUnnecessaryArraySeparators() throws IOException { + JsonReader reader = new JsonReader(reader("[true,,true]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + reader.nextNull(); + assertEquals(true, reader.nextBoolean()); + reader.endArray(); + + reader = new JsonReader(reader("[,true]")); + reader.setLenient(true); + reader.beginArray(); + reader.nextNull(); + assertEquals(true, reader.nextBoolean()); + reader.endArray(); + + reader = new JsonReader(reader("[true,]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + reader.nextNull(); + reader.endArray(); + + reader = new JsonReader(reader("[,]")); + reader.setLenient(true); + reader.beginArray(); + reader.nextNull(); + reader.nextNull(); + reader.endArray(); + } + + public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("[true,,true]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[,true]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[true,]")); + reader.beginArray(); + assertEquals(true, reader.nextBoolean()); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + + reader = new JsonReader(reader("[,]")); + reader.beginArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictMultipleTopLevelValues() throws IOException { + JsonReader reader = new JsonReader(reader("[] []")); + reader.beginArray(); + reader.endArray(); + try { + reader.peek(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientMultipleTopLevelValues() throws IOException { + JsonReader reader = new JsonReader(reader("[] true {}")); + reader.setLenient(true); + reader.beginArray(); + reader.endArray(); + assertEquals(true, reader.nextBoolean()); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException { + JsonReader reader = new JsonReader(reader("[] []")); + reader.beginArray(); + reader.endArray(); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictTopLevelString() { + JsonReader reader = new JsonReader(reader("\"a\"")); + try { + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientTopLevelString() throws IOException { + JsonReader reader = new JsonReader(reader("\"a\"")); + reader.setLenient(true); + assertEquals("a", reader.nextString()); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testStrictTopLevelValueType() { + JsonReader reader = new JsonReader(reader("true")); + try { + reader.nextBoolean(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientTopLevelValueType() throws IOException { + JsonReader reader = new JsonReader(reader("true")); + reader.setLenient(true); + assertEquals(true, reader.nextBoolean()); + } + + public void testStrictTopLevelValueTypeWithSkipValue() { + JsonReader reader = new JsonReader(reader("true")); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictNonExecutePrefix() { + JsonReader reader = new JsonReader(reader(")]}'\n []")); + try { + reader.beginArray(); + fail(); + } catch (IOException expected) { + } + } + + public void testStrictNonExecutePrefixWithSkipValue() { + JsonReader reader = new JsonReader(reader(")]}'\n []")); + try { + reader.skipValue(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientNonExecutePrefix() throws IOException { + JsonReader reader = new JsonReader(reader(")]}'\n []")); + reader.setLenient(true); + reader.beginArray(); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testLenientNonExecutePrefixWithLeadingWhitespace() throws IOException { + JsonReader reader = new JsonReader(reader("\r\n \t)]}'\n []")); + reader.setLenient(true); + reader.beginArray(); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testLenientPartialNonExecutePrefix() { + JsonReader reader = new JsonReader(reader(")]}' []")); + reader.setLenient(true); + try { + assertEquals(")", reader.nextString()); + reader.nextString(); + fail(); + } catch (IOException expected) { + } + } + + public void testBomIgnoredAsFirstCharacterOfDocument() throws IOException { + JsonReader reader = new JsonReader(reader("\ufeff[]")); + reader.beginArray(); + reader.endArray(); + } + + public void testBomForbiddenAsOtherCharacterInDocument() throws IOException { + JsonReader reader = new JsonReader(reader("[\ufeff]")); + reader.beginArray(); + try { + reader.endArray(); + fail(); + } catch (IOException expected) { + } + } + + public void testFailWithPosition() throws IOException { + testFailWithPosition("Expected value at line 6 column 5 path $[1]", + "[\n\n\n\n\n\"a\",}]"); + } + + public void testFailWithPositionGreaterThanBufferSize() throws IOException { + String spaces = repeat(' ', 8192); + testFailWithPosition("Expected value at line 6 column 5 path $[1]", + "[\n\n" + spaces + "\n\n\n\"a\",}]"); + } + + public void testFailWithPositionOverSlashSlashEndOfLineComment() throws IOException { + testFailWithPosition("Expected value at line 5 column 6 path $[1]", + "\n// foo\n\n//bar\r\n[\"a\",}"); + } + + public void testFailWithPositionOverHashEndOfLineComment() throws IOException { + testFailWithPosition("Expected value at line 5 column 6 path $[1]", + "\n# foo\n\n#bar\r\n[\"a\",}"); + } + + public void testFailWithPositionOverCStyleComment() throws IOException { + testFailWithPosition("Expected value at line 6 column 12 path $[1]", + "\n\n/* foo\n*\n*\r\nbar */[\"a\",}"); + } + + public void testFailWithPositionOverQuotedString() throws IOException { + testFailWithPosition("Expected value at line 5 column 3 path $[1]", + "[\"foo\nbar\r\nbaz\n\",\n }"); + } + + public void testFailWithPositionOverUnquotedString() throws IOException { + testFailWithPosition("Expected value at line 5 column 2 path $[1]", "[\n\nabcd\n\n,}"); + } + + public void testFailWithEscapedNewlineCharacter() throws IOException { + testFailWithPosition("Expected value at line 5 column 3 path $[1]", "[\n\n\"\\\n\n\",}"); + } + + public void testFailWithPositionIsOffsetByBom() throws IOException { + testFailWithPosition("Expected value at line 1 column 6 path $[1]", + "\ufeff[\"a\",}]"); + } + + private void testFailWithPosition(String message, String json) throws IOException { + // Validate that it works reading the string normally. + JsonReader reader1 = new JsonReader(reader(json)); + reader1.setLenient(true); + reader1.beginArray(); + reader1.nextString(); + try { + reader1.peek(); + fail(); + } catch (IOException expected) { + assertEquals(message, expected.getMessage()); + } + + // Also validate that it works when skipping. + JsonReader reader2 = new JsonReader(reader(json)); + reader2.setLenient(true); + reader2.beginArray(); + reader2.skipValue(); + try { + reader2.peek(); + fail(); + } catch (IOException expected) { + assertEquals(message, expected.getMessage()); + } + } + + public void testFailWithPositionDeepPath() throws IOException { + JsonReader reader = new JsonReader(reader("[1,{\"a\":[2,3,}")); + reader.beginArray(); + reader.nextInt(); + reader.beginObject(); + reader.nextName(); + reader.beginArray(); + reader.nextInt(); + reader.nextInt(); + try { + reader.peek(); + fail(); + } catch (IOException expected) { + assertEquals("Expected value at line 1 column 14 path $[1].a[2]", expected.getMessage()); + } + } + + public void testStrictVeryLongNumber() throws IOException { + JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]")); + reader.beginArray(); + try { + assertEquals(1d, reader.nextDouble()); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testLenientVeryLongNumber() throws IOException { + JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(JsonToken.STRING, reader.peek()); + assertEquals(1d, reader.nextDouble()); + reader.endArray(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testVeryLongUnquotedLiteral() throws IOException { + String literal = "a" + repeat('b', 8192) + "c"; + JsonReader reader = new JsonReader(reader("[" + literal + "]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(literal, reader.nextString()); + reader.endArray(); + } + + public void testDeeplyNestedArrays() throws IOException { + // this is nested 40 levels deep; Gson is tuned for nesting is 30 levels deep or fewer + JsonReader reader = new JsonReader(reader( + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]")); + for (int i = 0; i < 40; i++) { + reader.beginArray(); + } + assertEquals("$[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]" + + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0]", reader.getPath()); + for (int i = 0; i < 40; i++) { + reader.endArray(); + } + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testDeeplyNestedObjects() throws IOException { + // Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 40 levels deep + String array = "{\"a\":%s}"; + String json = "true"; + for (int i = 0; i < 40; i++) { + json = String.format(array, json); + } + + JsonReader reader = new JsonReader(reader(json)); + for (int i = 0; i < 40; i++) { + reader.beginObject(); + assertEquals("a", reader.nextName()); + } + assertEquals("$.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" + + ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a", reader.getPath()); + assertEquals(true, reader.nextBoolean()); + for (int i = 0; i < 40; i++) { + reader.endObject(); + } + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + // http://code.google.com/p/google-gson/issues/detail?id=409 + public void testStringEndingInSlash() throws IOException { + JsonReader reader = new JsonReader(reader("/")); + reader.setLenient(true); + try { + reader.peek(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testDocumentWithCommentEndingInSlash() throws IOException { + JsonReader reader = new JsonReader(reader("/* foo *//")); + reader.setLenient(true); + try { + reader.peek(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testStringWithLeadingSlash() throws IOException { + JsonReader reader = new JsonReader(reader("/x")); + reader.setLenient(true); + try { + reader.peek(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testUnterminatedObject() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":\"android\"x")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals("android", reader.nextString()); + try { + reader.peek(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + public void testVeryLongQuotedString() throws IOException { + char[] stringChars = new char[1024 * 16]; + Arrays.fill(stringChars, 'x'); + String string = new String(stringChars); + String json = "[\"" + string + "\"]"; + JsonReader reader = new JsonReader(reader(json)); + reader.beginArray(); + assertEquals(string, reader.nextString()); + reader.endArray(); + } + + public void testVeryLongUnquotedString() throws IOException { + char[] stringChars = new char[1024 * 16]; + Arrays.fill(stringChars, 'x'); + String string = new String(stringChars); + String json = "[" + string + "]"; + JsonReader reader = new JsonReader(reader(json)); + reader.setLenient(true); + reader.beginArray(); + assertEquals(string, reader.nextString()); + reader.endArray(); + } + + public void testVeryLongUnterminatedString() throws IOException { + char[] stringChars = new char[1024 * 16]; + Arrays.fill(stringChars, 'x'); + String string = new String(stringChars); + String json = "[" + string; + JsonReader reader = new JsonReader(reader(json)); + reader.setLenient(true); + reader.beginArray(); + assertEquals(string, reader.nextString()); + try { + reader.peek(); + fail(); + } catch (EOFException expected) { + } + } + + public void testSkipVeryLongUnquotedString() throws IOException { + JsonReader reader = new JsonReader(reader("[" + repeat('x', 8192) + "]")); + reader.setLenient(true); + reader.beginArray(); + reader.skipValue(); + reader.endArray(); + } + + public void testSkipTopLevelUnquotedString() throws IOException { + JsonReader reader = new JsonReader(reader(repeat('x', 8192))); + reader.setLenient(true); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipVeryLongQuotedString() throws IOException { + JsonReader reader = new JsonReader(reader("[\"" + repeat('x', 8192) + "\"]")); + reader.beginArray(); + reader.skipValue(); + reader.endArray(); + } + + public void testSkipTopLevelQuotedString() throws IOException { + JsonReader reader = new JsonReader(reader("\"" + repeat('x', 8192) + "\"")); + reader.setLenient(true); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testStringAsNumberWithTruncatedExponent() throws IOException { + JsonReader reader = new JsonReader(reader("[123e]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + } + + public void testStringAsNumberWithDigitAndNonDigitExponent() throws IOException { + JsonReader reader = new JsonReader(reader("[123e4b]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + } + + public void testStringAsNumberWithNonDigitExponent() throws IOException { + JsonReader reader = new JsonReader(reader("[123eb]")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(STRING, reader.peek()); + } + + public void testEmptyStringName() throws IOException { + JsonReader reader = new JsonReader(reader("{\"\":true}")); + reader.setLenient(true); + assertEquals(BEGIN_OBJECT, reader.peek()); + reader.beginObject(); + assertEquals(NAME, reader.peek()); + assertEquals("", reader.nextName()); + assertEquals(JsonToken.BOOLEAN, reader.peek()); + assertEquals(true, reader.nextBoolean()); + assertEquals(JsonToken.END_OBJECT, reader.peek()); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testStrictExtraCommasInMaps() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":\"b\",}")); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals("b", reader.nextString()); + try { + reader.peek(); + fail(); + } catch (IOException expected) { + } + } + + public void testLenientExtraCommasInMaps() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":\"b\",}")); + reader.setLenient(true); + reader.beginObject(); + assertEquals("a", reader.nextName()); + assertEquals("b", reader.nextString()); + try { + reader.peek(); + fail(); + } catch (IOException expected) { + } + } + + private String repeat(char c, int count) { + char[] array = new char[count]; + Arrays.fill(array, c); + return new String(array); + } + + public void testMalformedDocuments() throws IOException { + assertDocument("{]", BEGIN_OBJECT, IOException.class); + assertDocument("{,", BEGIN_OBJECT, IOException.class); + assertDocument("{{", BEGIN_OBJECT, IOException.class); + assertDocument("{[", BEGIN_OBJECT, IOException.class); + assertDocument("{:", BEGIN_OBJECT, IOException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\"=>\"string\",\"name\"", BEGIN_OBJECT, NAME, STRING, NAME); + assertDocument("[}", BEGIN_ARRAY, IOException.class); + assertDocument("[,]", BEGIN_ARRAY, NULL, NULL, END_ARRAY); + assertDocument("{", BEGIN_OBJECT, IOException.class); + assertDocument("{\"name\"", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{'name'", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{'name',", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("{name", BEGIN_OBJECT, NAME, IOException.class); + assertDocument("[", BEGIN_ARRAY, IOException.class); + assertDocument("[string", BEGIN_ARRAY, STRING, IOException.class); + assertDocument("[\"string\"", BEGIN_ARRAY, STRING, IOException.class); + assertDocument("['string'", BEGIN_ARRAY, STRING, IOException.class); + assertDocument("[123", BEGIN_ARRAY, NUMBER, IOException.class); + assertDocument("[123,", BEGIN_ARRAY, NUMBER, IOException.class); + assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, IOException.class); + assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, IOException.class); + assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); + assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); + } + + /** + * This test behave slightly differently in Gson 2.2 and earlier. It fails + * during peek rather than during nextString(). + */ + public void testUnterminatedStringFailure() throws IOException { + JsonReader reader = new JsonReader(reader("[\"string")); + reader.setLenient(true); + reader.beginArray(); + assertEquals(JsonToken.STRING, reader.peek()); + try { + reader.nextString(); + fail(); + } catch (MalformedJsonException expected) { + } + } + + private void assertDocument(String document, Object... expectations) throws IOException { + JsonReader reader = new JsonReader(reader(document)); + reader.setLenient(true); + for (Object expectation : expectations) { + if (expectation == BEGIN_OBJECT) { + reader.beginObject(); + } else if (expectation == BEGIN_ARRAY) { + reader.beginArray(); + } else if (expectation == END_OBJECT) { + reader.endObject(); + } else if (expectation == END_ARRAY) { + reader.endArray(); + } else if (expectation == NAME) { + assertEquals("name", reader.nextName()); + } else if (expectation == BOOLEAN) { + assertEquals(false, reader.nextBoolean()); + } else if (expectation == STRING) { + assertEquals("string", reader.nextString()); + } else if (expectation == NUMBER) { + assertEquals(123, reader.nextInt()); + } else if (expectation == NULL) { + reader.nextNull(); + } else if (expectation == IOException.class) { + try { + reader.peek(); + fail(); + } catch (IOException expected) { + } + } else { + throw new AssertionError(); + } + } + } + + /** + * Returns a reader that returns one character at a time. + */ + private Reader reader(final String s) { + /* if (true) */ return new StringReader(s); + /* return new Reader() { + int position = 0; + @Override public int read(char[] buffer, int offset, int count) throws IOException { + if (position == s.length()) { + return -1; + } else if (count > 0) { + buffer[offset] = s.charAt(position++); + return 1; + } else { + throw new IllegalArgumentException(); + } + } + @Override public void close() throws IOException { + } + }; */ + } +} diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java new file mode 100644 index 00000000..4cfd55a7 --- /dev/null +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -0,0 +1,579 @@ +/* + * 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; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import junit.framework.TestCase; + +@SuppressWarnings("resource") +public final class JsonWriterTest extends TestCase { + + public void testWrongTopLevelType() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + try { + jsonWriter.value("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testTwoNames() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + try { + jsonWriter.name("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNameWithoutValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + try { + jsonWriter.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testValueWithoutName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + try { + jsonWriter.value(true); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray().endArray(); + try { + jsonWriter.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testBadNestingObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginObject(); + try { + jsonWriter.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testBadNestingArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginArray(); + try { + jsonWriter.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testNullName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + try { + jsonWriter.name(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testNullStringValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.value((String) null); + jsonWriter.endObject(); + assertEquals("{\"a\":null}", stringWriter.toString()); + } + + public void testJsonValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.jsonValue("{\"b\":true}"); + jsonWriter.name("c"); + jsonWriter.value(1); + jsonWriter.endObject(); + assertEquals("{\"a\":{\"b\":true},\"c\":1}", stringWriter.toString()); + } + + public void testNonFiniteDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + try { + jsonWriter.value(Double.NaN); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.NEGATIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(Double.POSITIVE_INFINITY); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testNonFiniteBoxedDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + try { + jsonWriter.value(new Double(Double.NaN)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(new Double(Double.NEGATIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + jsonWriter.value(new Double(Double.POSITIVE_INFINITY)); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testDoubles() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(-0.0); + jsonWriter.value(1.0); + jsonWriter.value(Double.MAX_VALUE); + jsonWriter.value(Double.MIN_VALUE); + jsonWriter.value(0.0); + jsonWriter.value(-0.5); + jsonWriter.value(2.2250738585072014E-308); + jsonWriter.value(Math.PI); + jsonWriter.value(Math.E); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[-0.0," + + "1.0," + + "1.7976931348623157E308," + + "4.9E-324," + + "0.0," + + "-0.5," + + "2.2250738585072014E-308," + + "3.141592653589793," + + "2.718281828459045]", stringWriter.toString()); + } + + public void testLongs() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(0); + jsonWriter.value(1); + jsonWriter.value(-1); + jsonWriter.value(Long.MIN_VALUE); + jsonWriter.value(Long.MAX_VALUE); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[0," + + "1," + + "-1," + + "-9223372036854775808," + + "9223372036854775807]", stringWriter.toString()); + } + + public void testNumbers() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(new BigInteger("0")); + jsonWriter.value(new BigInteger("9223372036854775808")); + jsonWriter.value(new BigInteger("-9223372036854775809")); + jsonWriter.value(new BigDecimal("3.141592653589793238462643383")); + jsonWriter.endArray(); + jsonWriter.close(); + assertEquals("[0," + + "9223372036854775808," + + "-9223372036854775809," + + "3.141592653589793238462643383]", stringWriter.toString()); + } + + public void testBooleans() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value(true); + jsonWriter.value(false); + jsonWriter.endArray(); + assertEquals("[true,false]", stringWriter.toString()); + } + + public void testNulls() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.nullValue(); + jsonWriter.endArray(); + assertEquals("[null]", stringWriter.toString()); + } + + public void testStrings() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value("a"); + jsonWriter.value("a\""); + jsonWriter.value("\""); + jsonWriter.value(":"); + jsonWriter.value(","); + jsonWriter.value("\b"); + jsonWriter.value("\f"); + jsonWriter.value("\n"); + jsonWriter.value("\r"); + jsonWriter.value("\t"); + jsonWriter.value(" "); + jsonWriter.value("\\"); + jsonWriter.value("{"); + jsonWriter.value("}"); + jsonWriter.value("["); + jsonWriter.value("]"); + jsonWriter.value("\0"); + jsonWriter.value("\u0019"); + jsonWriter.endArray(); + assertEquals("[\"a\"," + + "\"a\\\"\"," + + "\"\\\"\"," + + "\":\"," + + "\",\"," + + "\"\\b\"," + + "\"\\f\"," + + "\"\\n\"," + + "\"\\r\"," + + "\"\\t\"," + + "\" \"," + + "\"\\\\\"," + + "\"{\"," + + "\"}\"," + + "\"[\"," + + "\"]\"," + + "\"\\u0000\"," + + "\"\\u0019\"]", stringWriter.toString()); + } + + public void testUnicodeLineBreaksEscaped() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.value("\u2028 \u2029"); + jsonWriter.endArray(); + assertEquals("[\"\\u2028 \\u2029\"]", stringWriter.toString()); + } + + public void testEmptyArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.endArray(); + assertEquals("[]", stringWriter.toString()); + } + + public void testEmptyObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.endObject(); + assertEquals("{}", stringWriter.toString()); + } + + public void testObjectsInArrays() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginArray(); + jsonWriter.beginObject(); + jsonWriter.name("a").value(5); + jsonWriter.name("b").value(false); + jsonWriter.endObject(); + jsonWriter.beginObject(); + jsonWriter.name("c").value(6); + jsonWriter.name("d").value(true); + jsonWriter.endObject(); + jsonWriter.endArray(); + assertEquals("[{\"a\":5,\"b\":false}," + + "{\"c\":6,\"d\":true}]", stringWriter.toString()); + } + + public void testArraysInObjects() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.beginArray(); + jsonWriter.value(5); + jsonWriter.value(false); + jsonWriter.endArray(); + jsonWriter.name("b"); + jsonWriter.beginArray(); + jsonWriter.value(6); + jsonWriter.value(true); + jsonWriter.endArray(); + jsonWriter.endObject(); + assertEquals("{\"a\":[5,false]," + + "\"b\":[6,true]}", stringWriter.toString()); + } + + public void testDeepNestingArrays() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + for (int i = 0; i < 20; i++) { + jsonWriter.beginArray(); + } + for (int i = 0; i < 20; i++) { + jsonWriter.endArray(); + } + assertEquals("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]", stringWriter.toString()); + } + + public void testDeepNestingObjects() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + for (int i = 0; i < 20; i++) { + jsonWriter.name("a"); + jsonWriter.beginObject(); + } + for (int i = 0; i < 20; i++) { + jsonWriter.endObject(); + } + jsonWriter.endObject(); + assertEquals("{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":" + + "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{" + + "}}}}}}}}}}}}}}}}}}}}}", stringWriter.toString()); + } + + public void testRepeatedName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.beginObject(); + jsonWriter.name("a").value(true); + jsonWriter.name("a").value(false); + jsonWriter.endObject(); + // JsonWriter doesn't attempt to detect duplicate names + assertEquals("{\"a\":true,\"a\":false}", stringWriter.toString()); + } + + public void testPrettyPrintObject() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + + jsonWriter.beginObject(); + jsonWriter.name("a").value(true); + jsonWriter.name("b").value(false); + jsonWriter.name("c").value(5.0); + jsonWriter.name("e").nullValue(); + jsonWriter.name("f").beginArray(); + jsonWriter.value(6.0); + jsonWriter.value(7.0); + jsonWriter.endArray(); + jsonWriter.name("g").beginObject(); + jsonWriter.name("h").value(8.0); + jsonWriter.name("i").value(9.0); + jsonWriter.endObject(); + jsonWriter.endObject(); + + String expected = "{\n" + + " \"a\": true,\n" + + " \"b\": false,\n" + + " \"c\": 5.0,\n" + + " \"e\": null,\n" + + " \"f\": [\n" + + " 6.0,\n" + + " 7.0\n" + + " ],\n" + + " \"g\": {\n" + + " \"h\": 8.0,\n" + + " \"i\": 9.0\n" + + " }\n" + + "}"; + assertEquals(expected, stringWriter.toString()); + } + + public void testPrettyPrintArray() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + + jsonWriter.beginArray(); + jsonWriter.value(true); + jsonWriter.value(false); + jsonWriter.value(5.0); + jsonWriter.nullValue(); + jsonWriter.beginObject(); + jsonWriter.name("a").value(6.0); + jsonWriter.name("b").value(7.0); + jsonWriter.endObject(); + jsonWriter.beginArray(); + jsonWriter.value(8.0); + jsonWriter.value(9.0); + jsonWriter.endArray(); + jsonWriter.endArray(); + + String expected = "[\n" + + " true,\n" + + " false,\n" + + " 5.0,\n" + + " null,\n" + + " {\n" + + " \"a\": 6.0,\n" + + " \"b\": 7.0\n" + + " },\n" + + " [\n" + + " 8.0,\n" + + " 9.0\n" + + " ]\n" + + "]"; + assertEquals(expected, stringWriter.toString()); + } + + public void testLenientWriterPermitsMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.setLenient(true); + writer.beginArray(); + writer.endArray(); + writer.beginArray(); + writer.endArray(); + writer.close(); + assertEquals("[][]", stringWriter.toString()); + } + + public void testStrictWriterDoesNotPermitMultipleTopLevelValues() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + try { + writer.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnStructure() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.beginArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.endArray(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.beginObject(); + fail(); + } catch (IllegalStateException expected) { + } + try { + writer.endObject(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnName() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.name("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnValue() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.value("a"); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testClosedWriterThrowsOnFlush() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + try { + writer.flush(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testWriterCloseIsIdempotent() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginArray(); + writer.endArray(); + writer.close(); + writer.close(); + } +} |