diff --git a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java index 3ebeb72c6d..da003e8962 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java @@ -162,6 +162,17 @@ public enum Feature { * @since 2.3 */ STRICT_DUPLICATE_DETECTION(false), + + /** + * Feature that specifies that Unicode newline characters U+2028 + * and U+2029 must be explicitly escaped in JSON output. This + * ensures that the JSON output can be used as JSONP. + *
+ * Feature is disabled by default. + * + * @since 2.4 + */ + JSONP_COMPLIANT(false), ; private final boolean _defaultState; @@ -430,6 +441,10 @@ public PrettyPrinter getPrettyPrinter() { */ public int getHighestEscapedChar() { return 0; } + public JsonGenerator setJsonpCompliantOutput(boolean escape) { return this; } + + public boolean getJsonpCompliantOutput() { return false; } + /** * Method for accessing custom escapes factory uses for {@link JsonGenerator}s * it creates. diff --git a/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java b/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java index f249133903..4c7d8e0e6e 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/GeneratorBase.java @@ -91,6 +91,8 @@ public JsonGenerator enable(Feature f) { _cfgNumbersAsStrings = true; } else if (f == Feature.ESCAPE_NON_ASCII) { setHighestNonEscapedChar(127); + } else if (f == Feature.JSONP_COMPLIANT) { + setJsonpCompliantOutput(true); } return this; } @@ -102,6 +104,8 @@ public JsonGenerator disable(Feature f) { _cfgNumbersAsStrings = false; } else if (f == Feature.ESCAPE_NON_ASCII) { setHighestNonEscapedChar(0); + } else if (f == Feature.JSONP_COMPLIANT) { + setJsonpCompliantOutput(false); } return this; } diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java index dbe7c9d698..067fdbebda 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java @@ -97,6 +97,9 @@ public JsonGeneratorImpl(IOContext ctxt, int features, ObjectCodec codec) if (isEnabled(Feature.ESCAPE_NON_ASCII)) { setHighestNonEscapedChar(127); } + if (isEnabled(Feature.JSONP_COMPLIANT)) { + setJsonpCompliantOutput(true); + } } /* @@ -116,6 +119,22 @@ public int getHighestEscapedChar() { return _maximumNonEscapedChar; } + @Override + public JsonGenerator setJsonpCompliantOutput(boolean escape) { + if (escape) { + _characterEscapes = new JsonpCharacterEscapes(); + } else if (getJsonpCompliantOutput()) { + _characterEscapes = null; + } + + return this; + } + + @Override + public boolean getJsonpCompliantOutput() { + return _characterEscapes instanceof JsonpCharacterEscapes; + } + @Override public JsonGenerator setCharacterEscapes(CharacterEscapes esc) { diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonpCharacterEscapes.java b/src/main/java/com/fasterxml/jackson/core/json/JsonpCharacterEscapes.java new file mode 100644 index 0000000000..e60ccb08fc --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonpCharacterEscapes.java @@ -0,0 +1,36 @@ +package com.fasterxml.jackson.core.json; + +import com.fasterxml.jackson.core.SerializableString; +import com.fasterxml.jackson.core.io.CharacterEscapes; +import com.fasterxml.jackson.core.io.SerializedString; + +/** + * Character escapes for producing json that can be safely used for JSONP. + * Used when {@link com.fasterxml.jackson.core.JsonGenerator.Feature#JSONP_COMPLIANT} + * is enabled. + */ +public class JsonpCharacterEscapes extends CharacterEscapes +{ + private static final int[] asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON(); + private static final SerializedString escapeFor2028 = new SerializedString("\\u2028"); + private static final SerializedString escapeFor2029 = new SerializedString("\\u2029"); + + @Override + public SerializableString getEscapeSequence(int ch) + { + switch (ch) { + case 0x2028: + return escapeFor2028; + case 0x2029: + return escapeFor2029; + default: + return null; + } + } + + @Override + public int[] getEscapeCodesForAscii() + { + return asciiEscapes; + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java b/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java index e88b41cd6c..93b1ee158d 100644 --- a/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java +++ b/src/test/java/com/fasterxml/jackson/core/json/TestJsonFactory.java @@ -69,18 +69,22 @@ public void testCopy() throws Exception assertTrue(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES)); assertFalse(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS)); assertFalse(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII)); + assertFalse(jf.isEnabled(JsonGenerator.Feature.JSONP_COMPLIANT)); jf.disable(JsonFactory.Feature.INTERN_FIELD_NAMES); jf.enable(JsonParser.Feature.ALLOW_COMMENTS); jf.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII); + jf.enable(JsonGenerator.Feature.JSONP_COMPLIANT); // then change, verify that changes "stick" assertFalse(jf.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES)); assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS)); assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII)); + assertTrue(jf.isEnabled(JsonGenerator.Feature.JSONP_COMPLIANT)); JsonFactory jf2 = jf.copy(); assertFalse(jf2.isEnabled(JsonFactory.Feature.INTERN_FIELD_NAMES)); assertTrue(jf.isEnabled(JsonParser.Feature.ALLOW_COMMENTS)); assertTrue(jf.isEnabled(JsonGenerator.Feature.ESCAPE_NON_ASCII)); + assertTrue(jf.isEnabled(JsonGenerator.Feature.JSONP_COMPLIANT)); } } diff --git a/src/test/java/com/fasterxml/jackson/core/json/TestJsonGeneratorFeatures.java b/src/test/java/com/fasterxml/jackson/core/json/TestJsonGeneratorFeatures.java index 8c0d08ebe1..19602be7ee 100644 --- a/src/test/java/com/fasterxml/jackson/core/json/TestJsonGeneratorFeatures.java +++ b/src/test/java/com/fasterxml/jackson/core/json/TestJsonGeneratorFeatures.java @@ -84,6 +84,23 @@ public void testBigDecimalAsPlain() throws IOException jg.close(); assertEquals("100", sw.toString()); } + + public void testJsonpCompliantOutput() throws IOException + { + JsonFactory jf = new JsonFactory(); + // by default, escaping should be disabled + _testJsonpCompliantEscaping(jf, false); + // can enable it + jf.enable(JsonGenerator.Feature.JSONP_COMPLIANT); + _testJsonpCompliantEscaping(jf, true); + // and (re)disable: + jf.disable(JsonGenerator.Feature.JSONP_COMPLIANT); + _testJsonpCompliantEscaping(jf, false); + } + + /** + * Testing for generating JSONP compliant output. + */ private String _writeNumbers(JsonFactory jf) throws IOException { @@ -150,4 +167,22 @@ private void _testNonNumericQuoting(JsonFactory jf, boolean quoted) assertEquals("{\"double\":NaN} {\"float\":NaN}", result); } } + + private void _testJsonpCompliantEscaping(JsonFactory jf, boolean escaped) + throws IOException + { + StringWriter sw = new StringWriter(); + JsonGenerator jg = jf.createGenerator(sw); + jg.writeStartObject(); + jg.writeStringField("str", "foo\u2028bar\u2029"); + jg.writeEndObject(); + jg.close(); + + String result = sw.toString(); + if (escaped) { + assertEquals("{\"str\":\"foo\\u2028bar\\u2029\"}", result); + } else { + assertEquals("{\"str\":\"foo\u2028bar\u2029\"}", result); + } + } }