diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 627108ca..cf2779fa 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -17,6 +17,7 @@ package com.google.gson; import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -110,6 +111,7 @@ public final class Gson { private final JsonFormatter formatter; private final boolean serializeNulls; + private final boolean htmlSafe; private final boolean generateNonExecutableJson; @@ -151,7 +153,7 @@ public final class Gson { this(DEFAULT_EXCLUSION_STRATEGY, DEFAULT_EXCLUSION_STRATEGY, DEFAULT_NAMING_POLICY, new MappedObjectConstructor(DefaultTypeAdapters.getDefaultInstanceCreators()), DEFAULT_JSON_FORMATTER, false, DefaultTypeAdapters.getDefaultSerializers(), - DefaultTypeAdapters.getDefaultDeserializers(), DEFAULT_JSON_NON_EXECUTABLE); + DefaultTypeAdapters.getDefaultDeserializers(), DEFAULT_JSON_NON_EXECUTABLE, true); } Gson(ExclusionStrategy serializationStrategy, ExclusionStrategy deserializationStrategy, @@ -159,7 +161,7 @@ public final class Gson { JsonFormatter formatter, boolean serializeNulls, ParameterizedTypeHandlerMap> serializers, ParameterizedTypeHandlerMap> deserializers, - boolean generateNonExecutableGson) { + boolean generateNonExecutableGson, boolean htmlSafe) { this.serializationStrategy = serializationStrategy; this.deserializationStrategy = deserializationStrategy; this.fieldNamingPolicy = fieldNamingPolicy; @@ -169,6 +171,7 @@ public final class Gson { this.serializers = serializers; this.deserializers = deserializers; this.generateNonExecutableJson = generateNonExecutableGson; + this.htmlSafe = htmlSafe; } private ObjectNavigatorFactory createDefaultObjectNavigatorFactory(ExclusionStrategy strategy) { @@ -268,7 +271,7 @@ public final class Gson { */ public String toJson(Object src, Type typeOfSrc) { StringWriter writer = new StringWriter(); - toJson(src, typeOfSrc, writer); + toJson(toJsonTree(src, typeOfSrc), writer); return writer.toString(); } @@ -317,6 +320,14 @@ public final class Gson { toJson(jsonElement, writer); } + /** + * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to + * {@code writer}. + */ + public void toJson(Object src, Type typeOfSrc, JsonWriter writer) { + toJson(toJsonTree(src, typeOfSrc), writer); + } + /** * Converts a tree of {@link JsonElement}s into its equivalent JSON representation. * @@ -342,15 +353,30 @@ public final class Gson { if (generateNonExecutableJson) { writer.append(JSON_NON_EXECUTABLE_PREFIX); } - if (jsonElement == null && serializeNulls) { - writeOutNullString(writer); - } - formatter.format(jsonElement, writer, serializeNulls); + toJson(jsonElement, new JsonWriter(Streams.writerForAppendable(writer))); } catch (IOException e) { throw new RuntimeException(e); } } + /** + * Writes the JSON for {@code jsonElement} to {@code writer}. + */ + public void toJson(JsonElement jsonElement, JsonWriter writer) { + boolean oldLenient = writer.isLenient(); + writer.setLenient(true); + boolean oldHtmlSafe = writer.isHtmlSafe(); + writer.setHtmlSafe(htmlSafe); + try { + Streams.write(jsonElement, serializeNulls, writer); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + writer.setLenient(oldLenient); + writer.setHtmlSafe(oldHtmlSafe); + } + } + /** * 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 @@ -417,7 +443,7 @@ public final class Gson { * @since 1.2 */ public T fromJson(Reader json, Class classOfT) throws JsonParseException { - T target = classOfT.cast(fromJson(json, (Type) classOfT)); + T target = classOfT.cast(fromJson(new JsonReader(json), classOfT)); return target; } @@ -439,12 +465,24 @@ public final class Gson { * @throws JsonParseException if json is not a valid representation for an object of type typeOfT * @since 1.2 */ - @SuppressWarnings("unchecked") public T fromJson(Reader json, Type typeOfT) throws JsonParseException { - JsonReader jsonReader = new JsonReader(json); - jsonReader.setLenient(true); - JsonElement root = Streams.parse(jsonReader); - return (T) fromJson(root, typeOfT); + return this.fromJson(new JsonReader(json), typeOfT); + } + + /** + * Reads the next JSON value from {@code reader} and convert it to an object + * of type {@code typeOfT}. + */ + @SuppressWarnings("unchecked") // this method is unsafe and should be used very carefully + public T fromJson(JsonReader reader, Type typeOfT) throws JsonParseException { + boolean oldLenient = reader.isLenient(); + reader.setLenient(true); + try { + JsonElement root = Streams.parse(reader); + return (T) fromJson(root, typeOfT); + } finally { + reader.setLenient(oldLenient); + } } /** diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 8335891e..ecac6657 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -546,7 +546,7 @@ public final class GsonBuilder { new JsonPrintFormatter(escapeHtmlChars) : new JsonCompactFormatter(escapeHtmlChars); Gson gson = new Gson(serializationExclusionStrategy, deserializationExclusionStrategy, fieldNamingPolicy, objConstructor, formatter, serializeNulls, customSerializers, - customDeserializers, generateNonExecutableJson); + customDeserializers, generateNonExecutableJson, escapeHtmlChars); return gson; } diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java index d44495ef..0aecf4bf 100755 --- a/gson/src/main/java/com/google/gson/JsonParser.java +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -50,10 +50,20 @@ public final class JsonParser { * @since 1.3 */ public JsonElement parse(Reader json) throws JsonParseException { + return parse(new JsonReader(json)); + } + + /** + * 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 + */ + public JsonElement parse(JsonReader json) throws JsonParseException { + boolean lenient = json.isLenient(); + json.setLenient(true); try { - JsonReader jsonReader = new JsonReader(json); - jsonReader.setLenient(true); - return Streams.parse(jsonReader); + return Streams.parse(json); } catch (StackOverflowError e) { throw new JsonParseException("Failed parsing JSON source: " + json + " to Json", e); } catch (OutOfMemoryError e) { @@ -64,6 +74,8 @@ public final class JsonParser { } else { throw e; } + } finally { + json.setLenient(lenient); } - } + } } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index d504b744..3c773119 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -142,6 +142,8 @@ public final class JsonWriter implements Closeable { private boolean lenient; + private boolean htmlSafe; + /** * Creates a new instance that writes a JSON-encoded stream to {@code out}. * For best performance, ensure {@link Writer} is buffered; wrapping in @@ -188,6 +190,32 @@ public final class JsonWriter implements Closeable { 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 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 boolean isHtmlSafe() { + return htmlSafe; + } + /** * Begins encoding a new array. Each call to this method must be paired with * a call to {@link #endArray}. @@ -430,6 +458,18 @@ public final class JsonWriter implements Closeable { out.write("\\f"); break; + case '<': + case '>': + case '&': + case '=': + case '\'': + if (htmlSafe) { + out.write(String.format("\\u%04x", (int) c)); + } else { + out.write(c); + } + break; + default: if (c <= 0x1F) { out.write(String.format("\\u%04x", (int) c)); diff --git a/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java b/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java index 790703be..11a214c4 100644 --- a/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java +++ b/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java @@ -42,7 +42,7 @@ public class FunctionWithInternalDependenciesTest extends TestCase { Gson gson = new Gson(exclusionStrategy, exclusionStrategy, Gson.DEFAULT_NAMING_POLICY, new MappedObjectConstructor(DefaultTypeAdapters.getDefaultInstanceCreators()), Gson.DEFAULT_JSON_FORMATTER, false, DefaultTypeAdapters.getDefaultSerializers(), - DefaultTypeAdapters.getDefaultDeserializers(), Gson.DEFAULT_JSON_NON_EXECUTABLE); + DefaultTypeAdapters.getDefaultDeserializers(), Gson.DEFAULT_JSON_NON_EXECUTABLE, true); assertEquals("{}", gson.toJson(new ClassWithNoFields() { // empty anonymous 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..ed9e07d3 --- /dev/null +++ b/gson/src/test/java/com/google/gson/MixedStreamTest.java @@ -0,0 +1,94 @@ +/* + * 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.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +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(); + } + + static final class Car { + String name; + int color; + + Car(String name, int color) { + this.name = name; + this.color = color; + } + + Car() {} // for Gson + + @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/functional/EscapingTest.java b/gson/src/test/java/com/google/gson/functional/EscapingTest.java index 6ad1b043..fbbcb199 100644 --- a/gson/src/test/java/com/google/gson/functional/EscapingTest.java +++ b/gson/src/test/java/com/google/gson/functional/EscapingTest.java @@ -54,7 +54,7 @@ public class EscapingTest extends TestCase { strings.add("&"); strings.add("'"); strings.add("\""); - assertEquals("[\"\\u003c\",\"\\u003e\",\"\\u003d\",\"\\u0026\",\"'\",\"\\\"\"]", + assertEquals("[\"\\u003c\",\"\\u003e\",\"\\u003d\",\"\\u0026\",\"\\u0027\",\"\\\"\"]", gson.toJson(strings)); } diff --git a/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java b/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java index 2ae49923..d5d59cd2 100755 --- a/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java +++ b/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java @@ -131,7 +131,7 @@ public class NullObjectAndFieldTest extends TestCase { gsonBuilder = new GsonBuilder(); Gson gson = gsonBuilder.setPrettyPrinting().create(); String result = gson.toJson(new ClassWithMembers()); - assertEquals("{}\n", result); + assertEquals("{}", result); gson = gsonBuilder.serializeNulls().create(); result = gson.toJson(new ClassWithMembers()); @@ -142,11 +142,11 @@ public class NullObjectAndFieldTest extends TestCase { gsonBuilder = new GsonBuilder(); Gson gson = gsonBuilder.setPrettyPrinting().create(); String result = gson.toJson(new String[] { "1", null, "3" }); - assertEquals("[\"1\",null,\"3\"]\n", result); + assertEquals("[\"1\",null,\"3\"]", result); gson = gsonBuilder.serializeNulls().create(); result = gson.toJson(new String[] { "1", null, "3" }); - assertEquals("[\"1\",null,\"3\"]\n", result); + assertEquals("[\"1\",null,\"3\"]", result); } private static class ClassWithNullWrappedPrimitive { diff --git a/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java index 5aa6285c..99996c14 100644 --- a/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java +++ b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java @@ -59,34 +59,32 @@ public class PrettyPrintingTest extends TestCase { Type typeOfSrc = new TypeToken>() {}.getType(); String json = gson.toJson(listOfB, typeOfSrc); print(json); - assertPrintMargin(json); } public void testPrettyPrintArrayOfObjects() { ArrayOfObjects target = new ArrayOfObjects(); String json = gson.toJson(target); print(json); - assertPrintMargin(json); } public void testPrettyPrintArrayOfPrimitives() { int[] ints = new int[] { 1, 2, 3, 4, 5 }; String json = gson.toJson(ints); - assertEquals("[1,2,3,4,5]\n", json); + assertEquals("[1,2,3,4,5]", 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("[[1,2],[3,4],[5,6],[7,8],[9,0],[10]]\n", json); + assertEquals("[[1,2],[3,4],[5,6],[7,8],[9,0],[10]]", json); } public void testPrettyPrintListOfPrimitiveArrays() { List list = Arrays.asList(new Integer[][] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 }, { 9, 0 }, { 10 } }); String json = gson.toJson(list); - assertEquals("[[1,2],[3,4],[5,6],[7,8],[9,0],[10]]\n", json); + assertEquals("[[1,2],[3,4],[5,6],[7,8],[9,0],[10]]", json); } public void testMap() { @@ -94,7 +92,7 @@ public class PrettyPrintingTest extends TestCase { map.put("abc", 1); map.put("def", 5); String json = gson.toJson(map); - assertEquals("{\"abc\":1,\"def\":5}\n", json); + assertEquals("{\"abc\":1,\"def\":5}", json); } // In response to bug 153 @@ -114,7 +112,7 @@ public class PrettyPrintingTest extends TestCase { public void testMultipleArrays() { int[][][] ints = new int[][][] { { { 1 }, { 2 } } }; String json = gson.toJson(ints); - assertEquals("[[[1],[2]]]\n", json); + assertEquals("[[[1],[2]]]", json); } private void print(String msg) { @@ -122,16 +120,4 @@ public class PrettyPrintingTest extends TestCase { System.out.println(msg); } } - - private void assertPrintMargin(String str) { - int position = 0; - char[] chars = str.toCharArray(); - for (int i = 0; i < chars.length; ++i, ++position) { - char c = chars[i]; - if (c == '\n') { - position = 0; - } - assertTrue(position <= PRINT_MARGIN - RIGHT_MARGIN + 1); - } - } }