Provide a setting to allow a client to skip the escaping of special HTML characters.

As well, remove the "/" from the list of special HTML characters since it is causing some incompatibilities.
This commit is contained in:
Joel Leitch 2008-12-28 23:05:22 +00:00
parent 1c87bd5993
commit 73d93e3322
11 changed files with 211 additions and 105 deletions

View File

@ -36,32 +36,35 @@ import java.util.Set;
*/
class Escaper {
static final char[] HEX_CHARS = {
private static final char[] HEX_CHARS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
private static final Set<Character> JS_ESCAPE_CHARS;
private static final Set<Character> HTML_ESCAPE_CHARS;
static {
Set<Character> tmpSet = new HashSet<Character>();
tmpSet.add('\u0000');
tmpSet.add('\r');
tmpSet.add('\n');
tmpSet.add('\u2028');
tmpSet.add('\u2029');
tmpSet.add('\u0085');
tmpSet.add('\'');
tmpSet.add('"');
tmpSet.add('<');
tmpSet.add('>');
tmpSet.add('&');
tmpSet.add('=');
tmpSet.add('/');
tmpSet.add('\\');
JS_ESCAPE_CHARS = Collections.unmodifiableSet(tmpSet);
Set<Character> mandatoryEscapeSet = new HashSet<Character>();
mandatoryEscapeSet.add('"');
mandatoryEscapeSet.add('\\');
JS_ESCAPE_CHARS = Collections.unmodifiableSet(mandatoryEscapeSet);
Set<Character> htmlEscapeSet = new HashSet<Character>();
htmlEscapeSet.add('<');
htmlEscapeSet.add('>');
htmlEscapeSet.add('&');
htmlEscapeSet.add('=');
// htmlEscapeSet.add('/'); -- Removing slash for now since it causes some incompatibilities
HTML_ESCAPE_CHARS = Collections.unmodifiableSet(htmlEscapeSet);
}
public static String escapeJsonString(CharSequence plainText) {
private final boolean escapeHtmlCharacters;
Escaper(boolean escapeHtmlCharacters) {
this.escapeHtmlCharacters = escapeHtmlCharacters;
}
public String escapeJsonString(CharSequence plainText) {
StringBuffer escapedString = new StringBuffer(plainText.length() + 20);
try {
escapeJsonString(plainText, escapedString);
@ -71,54 +74,62 @@ class Escaper {
return escapedString.toString();
}
private static void escapeJsonString(CharSequence plainText, StringBuffer out) throws IOException {
private void escapeJsonString(CharSequence plainText, StringBuffer out) throws IOException {
int pos = 0; // Index just past the last char in plainText written to out.
int len = plainText.length();
for (int charCount, i = 0; i < len; i += charCount) {
int codePoint = Character.codePointAt(plainText, i);
charCount = Character.charCount(codePoint);
for (int charCount, i = 0; i < len; i += charCount) {
int codePoint = Character.codePointAt(plainText, i);
charCount = Character.charCount(codePoint);
if (!isControlCharacter(codePoint) && !mustEscapeCharInJsString(codePoint)) {
continue;
}
if (!isControlCharacter(codePoint) && !mustEscapeCharInJsString(codePoint)) {
continue;
}
out.append(plainText, pos, i);
pos = i + charCount;
switch (codePoint) {
case '\b':
out.append("\\b");
break;
case '\t':
out.append("\\t");
break;
case '\n':
out.append("\\n");
break;
case '\f':
out.append("\\f");
break;
case '\r':
out.append("\\r");
break;
case '\\':
out.append("\\\\");
break;
case '/':
out.append("\\/");
break;
case '"':
out.append('\\').append((char) codePoint);
break;
case '\'':
out.append((char) codePoint);
break;
default:
appendHexJavaScriptRepresentation(codePoint, out);
break;
}
out.append(plainText, pos, i);
pos = i + charCount;
switch (codePoint) {
case '\b':
out.append("\\b");
break;
case '\t':
out.append("\\t");
break;
case '\n':
out.append("\\n");
break;
case '\f':
out.append("\\f");
break;
case '\r':
out.append("\\r");
break;
case '\\':
out.append("\\\\");
break;
case '/':
out.append("\\/");
break;
case '"':
out.append('\\').append((char) codePoint);
break;
default:
appendHexJavaScriptRepresentation(codePoint, out);
break;
}
}
out.append(plainText, pos, len);
}
private boolean mustEscapeCharInJsString(int codepoint) {
if (!Character.isSupplementaryCodePoint(codepoint)) {
char c = (char) codepoint;
return JS_ESCAPE_CHARS.contains(c)
|| (escapeHtmlCharacters && HTML_ESCAPE_CHARS.contains(c));
} else {
return false;
}
}
private static boolean isControlCharacter(int codePoint) {
// JSON spec defines these code points as control characters, so they must be escaped
@ -146,12 +157,4 @@ class Escaper {
.append(HEX_CHARS[(codePoint >>> 4) & 0xf])
.append(HEX_CHARS[codePoint & 0xf]);
}
private static boolean mustEscapeCharInJsString(int codepoint) {
if (!Character.isSupplementaryCodePoint(codepoint)) {
return JS_ESCAPE_CHARS.contains((char)codepoint);
} else {
return false;
}
}
}

View File

@ -206,6 +206,31 @@ public final class GsonBuilder {
setFormatter(new JsonPrintFormatter());
return this;
}
/**
* Configures Gson to output Json that fits in a page for pretty printing. This option only
* affects Json serialization.
*
* @param escapeHtmlChars true if specific HTML characters should be escaped
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.3
*/
public GsonBuilder setPrettyPrinting(boolean escapeHtmlChars) {
setFormatter(new JsonPrintFormatter(escapeHtmlChars));
return this;
}
/**
* Configures Gson to output Json in a compact format.
*
* @param escapeHtmlChars true if specific HTML characters should be escaped
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.3
*/
public GsonBuilder setCompactPrinting(boolean escapeHtmlChars) {
setFormatter(new JsonCompactFormatter(escapeHtmlChars));
return this;
}
/**
* Configures Gson with a new formatting strategy other than the default strategy. The default

View File

@ -124,14 +124,24 @@ final class JsonCompactFormatter implements JsonFormatter {
writer.append('}');
}
}
private final boolean escapeHtmlChars;
JsonCompactFormatter() {
this(true);
}
JsonCompactFormatter(boolean escapeHtmlChars) {
this.escapeHtmlChars = escapeHtmlChars;
}
public void format(JsonElement root, Appendable writer,
boolean serializeNulls) throws IOException {
if (root == null) {
return;
}
JsonElementVisitor visitor =
new JsonEscapingVisitor(new FormattingVisitor(writer, serializeNulls));
JsonElementVisitor visitor = new JsonEscapingVisitor(
new FormattingVisitor(writer, serializeNulls), escapeHtmlChars);
JsonTreeNavigator navigator = new JsonTreeNavigator(visitor, serializeNulls);
navigator.navigate(root);
}

View File

@ -25,14 +25,20 @@ import java.io.IOException;
* @author Joel Leitch
*/
class JsonEscapingVisitor extends DelegatingJsonElementVisitor {
private final Escaper escaper;
/**
* Constructs a Visitor that will properly escape any JSON primitive values.
*
* @param delegate the JsonElementVisitor that this instance will use for delegation
*/
protected JsonEscapingVisitor(JsonElementVisitor delegate) {
protected JsonEscapingVisitor(JsonElementVisitor delegate, boolean escapeHtmlChars) {
this(delegate, new Escaper(escapeHtmlChars));
}
protected JsonEscapingVisitor(JsonElementVisitor delegate, Escaper escaper) {
super(delegate);
this.escaper = escaper;
}
@Override
@ -55,7 +61,7 @@ class JsonEscapingVisitor extends DelegatingJsonElementVisitor {
private JsonPrimitive escapeJsonPrimitive(JsonPrimitive member) {
if (member.isString()) {
String memberValue = member.getAsString();
String escapedValue = Escaper.escapeJsonString(memberValue);
String escapedValue = escaper.escapeJsonString(memberValue);
if (!escapedValue.equals(memberValue)) {
member.setValue(escapedValue);
}

View File

@ -32,19 +32,26 @@ final class JsonPrintFormatter implements JsonFormatter {
private final int printMargin;
private final int indentationSize;
private final int rightMargin;
private final boolean escapeHtmlChars;
public static final int DEFAULT_PRINT_MARGIN = 80;
public static final int DEFAULT_INDENTATION_SIZE = 2;
public static final int DEFAULT_RIGHT_MARGIN = 4;
public JsonPrintFormatter() {
this(DEFAULT_PRINT_MARGIN, DEFAULT_INDENTATION_SIZE, DEFAULT_RIGHT_MARGIN);
JsonPrintFormatter() {
this(true);
}
JsonPrintFormatter(boolean escapeHtmlChars) {
this(DEFAULT_PRINT_MARGIN, DEFAULT_INDENTATION_SIZE, DEFAULT_RIGHT_MARGIN, escapeHtmlChars);
}
public JsonPrintFormatter(int printMargin, int indentationSize, int rightMargin) {
JsonPrintFormatter(int printMargin, int indentationSize, int rightMargin,
boolean escapeHtmlChars) {
this.printMargin = printMargin;
this.indentationSize = indentationSize;
this.rightMargin = rightMargin;
this.escapeHtmlChars = escapeHtmlChars;
}
private class JsonWriter {
@ -234,8 +241,8 @@ final class JsonPrintFormatter implements JsonFormatter {
return;
}
JsonWriter jsonWriter = new JsonWriter(writer);
JsonElementVisitor visitor =
new JsonEscapingVisitor(new PrintFormattingVisitor(jsonWriter, serializeNulls));
JsonElementVisitor visitor = new JsonEscapingVisitor(
new PrintFormattingVisitor(jsonWriter, serializeNulls), escapeHtmlChars);
JsonTreeNavigator navigator = new JsonTreeNavigator(visitor, serializeNulls);
navigator.navigate(root);
jsonWriter.finishLine();

View File

@ -0,0 +1,12 @@
package com.google.gson;
public class TestCharacters {
public static void main(String[] args) {
System.out.println("\\b: " + Character.codePointAt("\b", 0));
System.out.println("\\r: " + Character.codePointAt("\r", 0));
System.out.println("\\n: " + Character.codePointAt("\n", 0));
System.out.println("\\t: " + Character.codePointAt("\t", 0));
System.out.println("': " + Character.codePointAt("'", 0));
}
}

View File

@ -25,52 +25,62 @@ import junit.framework.TestCase;
*/
public class EscaperTest extends TestCase {
private Escaper escapeHtmlChar;
private Escaper noEscapeHtmlChar;
@Override
protected void setUp() throws Exception {
super.setUp();
escapeHtmlChar = new Escaper(true);
noEscapeHtmlChar = new Escaper(false);
}
public void testNoSpecialCharacters() {
String value = "Testing123";
String escapedString = Escaper.escapeJsonString(value);
String escapedString = escapeHtmlChar.escapeJsonString(value);
assertEquals(value, escapedString);
}
public void testNewlineEscaping() throws Exception {
String containsNewline = "123\n456";
String escapedString = Escaper.escapeJsonString(containsNewline);
String escapedString = escapeHtmlChar.escapeJsonString(containsNewline);
assertEquals("123\\n456", escapedString);
}
public void testCarrageReturnEscaping() throws Exception {
String containsCarrageReturn = "123\r456";
String escapedString = Escaper.escapeJsonString(containsCarrageReturn);
String escapedString = escapeHtmlChar.escapeJsonString(containsCarrageReturn);
assertEquals("123\\r456", escapedString);
}
public void testTabEscaping() throws Exception {
String containsTab = "123\t456";
String escapedString = Escaper.escapeJsonString(containsTab);
String escapedString = escapeHtmlChar.escapeJsonString(containsTab);
assertEquals("123\\t456", escapedString);
}
public void testQuoteEscaping() throws Exception {
String containsQuote = "123\"456";
String escapedString = Escaper.escapeJsonString(containsQuote);
String escapedString = escapeHtmlChar.escapeJsonString(containsQuote);
assertEquals("123\\\"456", escapedString);
}
public void testLineSeparatorEscaping() throws Exception {
String src = "123\u2028 456";
String escapedString = Escaper.escapeJsonString(src);
String escapedString = escapeHtmlChar.escapeJsonString(src);
assertEquals("123\\u2028 456", escapedString);
}
public void testParagraphSeparatorEscaping() throws Exception {
String src = "123\u2029 456";
String escapedString = Escaper.escapeJsonString(src);
String escapedString = escapeHtmlChar.escapeJsonString(src);
assertEquals("123\\u2029 456", escapedString);
}
public void testControlCharBlockEscaping() throws Exception {
for (char c = '\u007f'; c <= '\u009f'; ++c) {
String src = "123 " + c + " 456";
String escapedString = Escaper.escapeJsonString(src);
String escapedString = escapeHtmlChar.escapeJsonString(src);
assertFalse(src.equals(escapedString));
}
}
@ -79,8 +89,11 @@ public class EscaperTest extends TestCase {
String containsEquals = "123=456";
int index = containsEquals.indexOf('=');
String unicodeValue = convertToUnicodeString(Character.codePointAt(containsEquals, index));
String escapedString = Escaper.escapeJsonString(containsEquals);
String escapedString = escapeHtmlChar.escapeJsonString(containsEquals);
assertEquals("123" + unicodeValue + "456", escapedString);
escapedString = noEscapeHtmlChar.escapeJsonString(containsEquals);
assertEquals(containsEquals, escapedString);
}
public void testGreaterThanAndLessThanEscaping() throws Exception {
@ -90,8 +103,11 @@ public class EscaperTest extends TestCase {
String gtAsUnicode = convertToUnicodeString(Character.codePointAt(containsLtGt, gtIndex));
String ltAsUnicode = convertToUnicodeString(Character.codePointAt(containsLtGt, ltIndex));
String escapedString = Escaper.escapeJsonString(containsLtGt);
String escapedString = escapeHtmlChar.escapeJsonString(containsLtGt);
assertEquals("123" + gtAsUnicode + "456" + ltAsUnicode, escapedString);
escapedString = noEscapeHtmlChar.escapeJsonString(containsLtGt);
assertEquals(containsLtGt, escapedString);
}
public void testAmpersandEscaping() throws Exception {
@ -99,24 +115,30 @@ public class EscaperTest extends TestCase {
int ampIndex = containsAmp.indexOf('&');
String ampAsUnicode = convertToUnicodeString(Character.codePointAt(containsAmp, ampIndex));
String escapedString = Escaper.escapeJsonString(containsAmp);
String escapedString = escapeHtmlChar.escapeJsonString(containsAmp);
assertEquals("123" + ampAsUnicode + "456", escapedString);
escapedString = noEscapeHtmlChar.escapeJsonString(containsAmp);
assertEquals(containsAmp, escapedString);
char ampCharAsUnicode = '\u0026';
String containsAmpUnicode = "123" + ampCharAsUnicode + "456";
escapedString = Escaper.escapeJsonString(containsAmpUnicode);
escapedString = escapeHtmlChar.escapeJsonString(containsAmpUnicode);
assertEquals("123" + ampAsUnicode + "456", escapedString);
escapedString = noEscapeHtmlChar.escapeJsonString(containsAmpUnicode);
assertEquals(containsAmp, escapedString);
}
public void testSlashEscaping() throws Exception {
String containsSlash = "123\\456";
String escapedString = Escaper.escapeJsonString(containsSlash);
String escapedString = escapeHtmlChar.escapeJsonString(containsSlash);
assertEquals("123\\\\456", escapedString);
}
public void testSingleQuoteNotEscaped() throws Exception {
String containsSingleQuote = "123'456";
String escapedString = Escaper.escapeJsonString(containsSingleQuote);
String escapedString = escapeHtmlChar.escapeJsonString(containsSingleQuote);
assertEquals(containsSingleQuote, escapedString);
}
@ -124,7 +146,7 @@ public class EscaperTest extends TestCase {
char unicodeChar = '\u2028';
String unicodeString = "Testing" + unicodeChar;
String escapedString = Escaper.escapeJsonString(unicodeString);
String escapedString = escapeHtmlChar.escapeJsonString(unicodeString);
assertFalse(unicodeString.equals(escapedString));
assertEquals("Testing\\u2028", escapedString);
}
@ -132,7 +154,7 @@ public class EscaperTest extends TestCase {
public void testUnicodeCharacterStringNoEscaping() throws Exception {
String unicodeString = "\u0065\u0066";
String escapedString = Escaper.escapeJsonString(unicodeString);
String escapedString = escapeHtmlChar.escapeJsonString(unicodeString);
assertEquals(unicodeString, escapedString);
}

View File

@ -60,7 +60,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testPrettyPrintList() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
BagOfPrimitives b = new BagOfPrimitives();
List<BagOfPrimitives> listOfB = new LinkedList<BagOfPrimitives>();
@ -74,7 +75,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testPrettyPrintArrayOfObjects() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
ArrayOfObjects target = new ArrayOfObjects();
String json = gson.toJson(target);
@ -83,7 +85,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testPrettyPrintArrayOfPrimitives() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
int[] ints = new int[] { 1, 2, 3, 4, 5 };
String json = gson.toJson(ints);
@ -91,7 +94,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testPrettyPrintArrayOfPrimitiveArrays() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
int[][] ints = new int[][] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 }, { 9, 0 }, { 10 } };
String json = gson.toJson(ints);
@ -99,7 +103,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testPrettyPrintListOfPrimitiveArrays() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
List<Integer[]> list = Arrays.asList(new Integer[][] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 },
{ 9, 0 }, { 10 } });
@ -108,7 +113,8 @@ public class FunctionalWithInternalDependenciesTest extends TestCase {
}
public void testMultipleArrays() {
JsonFormatter formatter = new JsonPrintFormatter(PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN);
JsonFormatter formatter = new JsonPrintFormatter(
PRINT_MARGIN, INDENTATION_SIZE, RIGHT_MARGIN, true);
Gson gson = builder.setFormatter(formatter).create();
int[][][] ints = new int[][][] { { { 1 }, { 2 } } };
String json = gson.toJson(ints);

View File

@ -26,12 +26,14 @@ import junit.framework.TestCase;
public class JsonEscapingVisitorTest extends TestCase {
private StubbedJsonElementVisitor stubVisitor;
private JsonEscapingVisitor escapingVisitor;
private Escaper escaper;
@Override
protected void setUp() throws Exception {
super.setUp();
stubVisitor = new StubbedJsonElementVisitor();
escapingVisitor = new JsonEscapingVisitor(stubVisitor);
escaper = new Escaper(true);
escapingVisitor = new JsonEscapingVisitor(stubVisitor, escaper);
}
public void testNonStringPrimitiveVisitation() throws Exception {
@ -52,7 +54,7 @@ public class JsonEscapingVisitorTest extends TestCase {
String value = "Testing\"123";
JsonPrimitive primitive = new JsonPrimitive(value);
escapingVisitor.visitPrimitive(primitive);
assertEquals(Escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
assertEquals(escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
}
public void testNonStringArrayVisitation() throws Exception {
@ -79,7 +81,7 @@ public class JsonEscapingVisitorTest extends TestCase {
JsonArray array = new JsonArray();
array.add(primitive);
escapingVisitor.visitArrayMember(array, primitive, true);
assertEquals(Escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
assertEquals(escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
}
public void testNonStringFieldVisitation() throws Exception {
@ -112,7 +114,7 @@ public class JsonEscapingVisitorTest extends TestCase {
object.addProperty(fieldName, value);
escapingVisitor.visitObjectMember(object, fieldName, primitive, true);
assertEquals(Escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
assertEquals(escaper.escapeJsonString(value), stubVisitor.primitiveReceived.getAsString());
}
private static class StubbedJsonElementVisitor implements JsonElementVisitor {

View File

@ -52,7 +52,7 @@ public class DefaultTypeAdaptersTest extends TestCase {
public void testUrlSerialization() throws Exception {
String urlValue = "http://google.com/";
URL url = new URL(urlValue);
assertEquals("\"http:\\/\\/google.com\\/\"", gson.toJson(url));
assertEquals("\"http://google.com/\"", gson.toJson(url));
}
public void testUrlDeserialization() {
@ -60,6 +60,9 @@ public class DefaultTypeAdaptersTest extends TestCase {
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 {
@ -80,7 +83,7 @@ public class DefaultTypeAdaptersTest extends TestCase {
public void testUriSerialization() throws Exception {
String uriValue = "http://google.com/";
URI uri = new URI(uriValue);
assertEquals("\"http:\\/\\/google.com\\/\"", gson.toJson(uri));
assertEquals("\"http://google.com/\"", gson.toJson(uri));
}
public void testUriDeserialization() {

View File

@ -547,4 +547,14 @@ public class PrimitiveTest extends TestCase {
value = gson.fromJson("\"25\"", long.class);
assertEquals(25, value);
}
public void testHtmlCharacterSerialization() throws Exception {
String target = "<script>var a = 12;</script>";
String result = gson.toJson(target);
assertFalse(result.equals('"' + target + '"'));
gson = new GsonBuilder().setCompactPrinting(false).create();
result = gson.toJson(target);
assertTrue(result.equals('"' + target + '"'));
}
}