This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.jcr.contentparser-1.2.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-contentparser.git
commit 23501ea0f0247b7ec11f9db029990a5c0b7c8b51 Author: Stefan Seifert <[email protected]> AuthorDate: Tue May 23 21:03:37 2017 +0000 SLING-6872 JCR Content Parser: Support tick as well as double quote when parsing JSON git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/jcr/contentparser@1795961 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 6 ++ .../{package-info.java => JsonParserFeature.java} | 19 +++- .../sling/jcr/contentparser/ParserOptions.java | 25 +++++ .../jcr/contentparser/impl/JsonContentParser.java | 47 ++++++++- .../jcr/contentparser/impl/JsonTicksConverter.java | 85 +++++++++++++++++ .../sling/jcr/contentparser/package-info.java | 2 +- .../contentparser/impl/JsonContentParserTest.java | 9 ++ .../impl/JsonContentParserTicksTest.java | 106 +++++++++++++++++++++ .../contentparser/impl/JsonTicksConverterTest.java | 61 ++++++++++++ .../sling/jcr/contentparser/impl/TestUtils.java | 10 ++ 10 files changed, 362 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index e889134..5df4e2f 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,12 @@ <scope>compile</scope> </dependency> <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.4</version> + <scope>compile</scope> + </dependency> + <dependency> <groupId>org.apache.johnzon</groupId> <artifactId>johnzon-core</artifactId> <version>1.0.0</version> diff --git a/src/main/java/org/apache/sling/jcr/contentparser/package-info.java b/src/main/java/org/apache/sling/jcr/contentparser/JsonParserFeature.java similarity index 73% copy from src/main/java/org/apache/sling/jcr/contentparser/package-info.java copy to src/main/java/org/apache/sling/jcr/contentparser/JsonParserFeature.java index 771f212..59c5a23 100644 --- a/src/main/java/org/apache/sling/jcr/contentparser/package-info.java +++ b/src/main/java/org/apache/sling/jcr/contentparser/JsonParserFeature.java @@ -16,8 +16,21 @@ * specific language governing permissions and limitations * under the License. */ +package org.apache.sling.jcr.contentparser; + /** - * Parser for repository content serialized e.g. as JSON or JCR XML. + * Feature flags for parsing JSON files. */ [email protected]("1.1.0") -package org.apache.sling.jcr.contentparser; +public enum JsonParserFeature { + + /** + * Support comments (/* ... */) in JSON files. + */ + COMMENTS, + + /** + * Support ticks (') additional to double quotes (") as quoting symbol for JSON names and strings. + */ + QUOTE_TICK + +} diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java b/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java index e8c3939..f878745 100644 --- a/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java +++ b/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java @@ -20,6 +20,7 @@ package org.apache.sling.jcr.contentparser; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Set; @@ -47,11 +48,18 @@ public final class ParserOptions { "jcr:uri:" ))); + /** + * List of JSON parser features activated by default. + */ + public static final EnumSet<JsonParserFeature> DEFAULT_JSON_PARSER_FEATURES + = EnumSet.of(JsonParserFeature.COMMENTS, JsonParserFeature.QUOTE_TICK); + private String defaultPrimaryType = DEFAULT_PRIMARY_TYPE; private boolean detectCalendarValues; private Set<String> ignorePropertyNames; private Set<String> ignoreResourceNames; private Set<String> removePropertyNamePrefixes = DEFAULT_REMOVE_PROPERTY_NAME_PREFIXES; + private EnumSet<JsonParserFeature> jsonParserFeatures = DEFAULT_JSON_PARSER_FEATURES; /** * Default "jcr:primaryType" property for resources that have no explicit value for this value. @@ -121,4 +129,21 @@ public final class ParserOptions { return removePropertyNamePrefixes; } + /** + * Set set of features the JSON parser should apply when parsing files. + * @param value JSON parser features + * @return this + */ + public ParserOptions jsonParserFeatures(EnumSet<JsonParserFeature> value) { + this.jsonParserFeatures = value; + return this; + } + public ParserOptions jsonParserFeatures(JsonParserFeature... value) { + this.jsonParserFeatures = EnumSet.copyOf(Arrays.asList(value)); + return this; + } + public EnumSet<JsonParserFeature> getJsonParserFeatures() { + return jsonParserFeatures; + } + } diff --git a/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonContentParser.java b/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonContentParser.java index 093fbec..054be85 100644 --- a/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonContentParser.java +++ b/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonContentParser.java @@ -20,6 +20,7 @@ package org.apache.sling.jcr.contentparser.impl; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.util.Calendar; import java.util.HashMap; import java.util.LinkedHashMap; @@ -35,8 +36,11 @@ import javax.json.JsonString; import javax.json.JsonValue; import javax.json.stream.JsonParsingException; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.CharEncoding; import org.apache.sling.jcr.contentparser.ContentHandler; import org.apache.sling.jcr.contentparser.ContentParser; +import org.apache.sling.jcr.contentparser.JsonParserFeature; import org.apache.sling.jcr.contentparser.ParseException; import org.apache.sling.jcr.contentparser.ParserOptions; @@ -54,21 +58,56 @@ public final class JsonContentParser implements ContentParser { */ private final JsonReaderFactory jsonReaderFactory; + private final boolean jsonQuoteTickets; + public JsonContentParser(ParserOptions options) { this.helper = new ParserHelper(options); - // allow comments in JSON files + Map<String,Object> jsonReaderFactoryConfig = new HashMap<>(); - jsonReaderFactoryConfig.put("org.apache.johnzon.supports-comments", true); + + // allow comments in JSON files? + if (options.getJsonParserFeatures().contains(JsonParserFeature.COMMENTS)) { + jsonReaderFactoryConfig.put("org.apache.johnzon.supports-comments", true); + } + jsonQuoteTickets = options.getJsonParserFeatures().contains(JsonParserFeature.QUOTE_TICK); + jsonReaderFactory = Json.createReaderFactory(jsonReaderFactoryConfig); } @Override public void parse(ContentHandler handler, InputStream is) throws IOException, ParseException { + parse(handler, toJsonObject(is), "/"); + } + + private JsonObject toJsonObject(InputStream is) { + if (jsonQuoteTickets) { + return toJsonObjectWithJsonTicks(is); + } try (JsonReader reader = jsonReaderFactory.createReader(is)) { - parse(handler, reader.readObject(), "/"); + return reader.readObject(); + } + catch (JsonParsingException ex) { + throw new ParseException("Error parsing JSON content: " + ex.getMessage(), ex); + } + } + + private JsonObject toJsonObjectWithJsonTicks(InputStream is) { + String jsonString; + try { + jsonString = IOUtils.toString(is, CharEncoding.UTF_8); + } + catch (IOException ex) { + throw new ParseException("Error getting JSON string.", ex); + } + + // convert ticks to double quotes + jsonString = JsonTicksConverter.tickToDoubleQuote(jsonString); + + try (JsonReader reader = jsonReaderFactory.createReader(new StringReader(jsonString))) { + return reader.readObject(); } catch (JsonParsingException ex) { - throw new ParseException("Error parsing JSON content.", ex); + throw new ParseException("Error parsing JSON content: " + ex.getMessage(), ex); } } diff --git a/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverter.java b/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverter.java new file mode 100644 index 0000000..6125599 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverter.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.jcr.contentparser.impl; + +/** + * Converts JSON with ticks to JSON with quotes. + * <p>Conversions:</p> + * <ul> + * <li>Converts ticks ' to " when used as quotation marks for names or string values</li> + * <li>Within names or string values quoted with ticks, ticks have to be escaped with <code>\'</code>. + * This escaping sign is removed on the conversion, because in JSON ticks must not be escaped.</li> + * <li>Within names or string values quoted with ticks, double quotes may or may not be escaped. + * After the conversion they are always escaped.</li> + * </ul> + */ +class JsonTicksConverter { + + static String tickToDoubleQuote(final String input) { + final StringBuilder output = new StringBuilder(); + boolean quoted = false; + boolean tickQuoted = false; + boolean escaped = false; + for (int i = 0, len = input.length(); i < len; i++) { + char in = input.charAt(i); + if (quoted || tickQuoted) { + if (escaped) { + if (in != '\'') { + output.append("\\"); + } + escaped = false; + } + else { + if (in == '"') { + if (quoted) { + quoted = false; + } + else if (tickQuoted) { + output.append("\\"); + } + } + else if (in == '\'') { + if (tickQuoted) { + in = '"'; + tickQuoted = false; + } + } + else if (in == '\\') { + escaped = true; + } + } + } + else { + if (in == '\'') { + in = '"'; + tickQuoted = true; + } + else if (in == '"') { + quoted = true; + } + } + if (in == '\\') { + continue; + } + output.append(in); + } + return output.toString(); + } + +} diff --git a/src/main/java/org/apache/sling/jcr/contentparser/package-info.java b/src/main/java/org/apache/sling/jcr/contentparser/package-info.java index 771f212..5cf33d1 100644 --- a/src/main/java/org/apache/sling/jcr/contentparser/package-info.java +++ b/src/main/java/org/apache/sling/jcr/contentparser/package-info.java @@ -19,5 +19,5 @@ /** * Parser for repository content serialized e.g. as JSON or JCR XML. */ [email protected]("1.1.0") [email protected]("1.2.0") package org.apache.sling.jcr.contentparser; diff --git a/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTest.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTest.java index 706cb6e..b8f3598 100644 --- a/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTest.java +++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTest.java @@ -27,12 +27,14 @@ import static org.junit.Assert.assertNull; import java.io.File; import java.math.BigDecimal; import java.util.Calendar; +import java.util.EnumSet; import java.util.Map; import java.util.TimeZone; import org.apache.sling.jcr.contentparser.ContentParser; import org.apache.sling.jcr.contentparser.ContentParserFactory; import org.apache.sling.jcr.contentparser.ContentType; +import org.apache.sling.jcr.contentparser.JsonParserFeature; import org.apache.sling.jcr.contentparser.ParseException; import org.apache.sling.jcr.contentparser.ParserOptions; import org.apache.sling.jcr.contentparser.impl.mapsupport.ContentElement; @@ -168,4 +170,11 @@ public class JsonContentParserTest { assertNull(invalidChild); } + @Test(expected = ParseException.class) + public void testFailsWithoutCommentsEnabled() throws Exception { + ContentParser underTest = ContentParserFactory.create(ContentType.JSON, + new ParserOptions().jsonParserFeatures(EnumSet.noneOf(JsonParserFeature.class))); + parse(underTest, file); + } + } diff --git a/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTicksTest.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTicksTest.java new file mode 100644 index 0000000..1ba5e62 --- /dev/null +++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonContentParserTicksTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.jcr.contentparser.impl; + +import static org.apache.sling.jcr.contentparser.impl.TestUtils.parse; +import static org.junit.Assert.assertEquals; + +import java.util.EnumSet; +import java.util.Map; + +import org.apache.sling.jcr.contentparser.ContentParser; +import org.apache.sling.jcr.contentparser.ContentParserFactory; +import org.apache.sling.jcr.contentparser.ContentType; +import org.apache.sling.jcr.contentparser.JsonParserFeature; +import org.apache.sling.jcr.contentparser.ParseException; +import org.apache.sling.jcr.contentparser.ParserOptions; +import org.apache.sling.jcr.contentparser.impl.mapsupport.ContentElement; +import org.junit.Before; +import org.junit.Test; + +public class JsonContentParserTicksTest { + + private ContentParser underTest; + + @Before + public void setUp() { + underTest = ContentParserFactory.create(ContentType.JSON, + new ParserOptions().jsonParserFeatures(JsonParserFeature.QUOTE_TICK)); + } + + @Test + public void testJsonWithTicks() throws Exception { + ContentElement content = parse(underTest, "{'prop1':'value1','prop2':123,'obj':{'prop3':'value2'}}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("value1", props.get("prop1")); + assertEquals(123L, props.get("prop2")); + assertEquals("value2", content.getChild("obj").getProperties().get("prop3")); + } + + @Test + public void testJsonWithTicksMixed() throws Exception { + ContentElement content = parse(underTest, "{\"prop1\":'value1','prop2':123,'obj':{'prop3':\"value2\"}}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("value1", props.get("prop1")); + assertEquals(123L, props.get("prop2")); + assertEquals("value2", content.getChild("obj").getProperties().get("prop3")); + } + + @Test + public void testTicksDoubleQuotesInDoubleQuotes() throws Exception { + ContentElement content = parse(underTest, "{\"prop1\":\"'\\\"\'\\\"\"}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("'\"'\"", props.get("prop1")); + } + + @Test + public void testTicksDoubleQuotesInTicks() throws Exception { + ContentElement content = parse(underTest, "{'prop1':'\\'\\\"\\\'\\\"'}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("'\"'\"", props.get("prop1")); + } + + @Test + public void testWithUtf8Escaped() throws Exception { + ContentElement content = parse(underTest, "{\"prop1\":\"\\u03A9\\u03A6\\u00A5\"}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("\u03A9\u03A6\u00A5", props.get("prop1")); + } + + @Test + public void testWithTicksUtf8Escaped() throws Exception { + ContentElement content = parse(underTest, "{'prop1':'\\u03A9\\u03A6\\u00A5'}"); + + Map<String, Object> props = content.getProperties(); + assertEquals("\u03A9\u03A6\u00A5", props.get("prop1")); + } + + @Test(expected = ParseException.class) + public void testFailsWihtoutFeatureEnabled() throws Exception { + underTest = ContentParserFactory.create(ContentType.JSON, + new ParserOptions().jsonParserFeatures(EnumSet.noneOf(JsonParserFeature.class))); + parse(underTest, "{'prop1':'value1','prop2':123,'obj':{'prop3':'value2'}}"); + } + +} diff --git a/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverterTest.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverterTest.java new file mode 100644 index 0000000..836d1a8 --- /dev/null +++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/JsonTicksConverterTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sling.jcr.contentparser.impl; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import static org.apache.sling.jcr.contentparser.impl.JsonTicksConverter.*; + +public class JsonTicksConverterTest { + + @Test + public void testNoConvert() { + assertEquals("{\"p\":\"v\"}", tickToDoubleQuote("{\"p\":\"v\"}")); + } + + @Test + public void testTickToQuote() { + assertEquals("{\"p\":\"v\"}", tickToDoubleQuote("{'p':\"v\"}")); + } + + @Test + public void testTickToQuoteMixed() { + assertEquals("{\"p\":\"v\"}", tickToDoubleQuote("{'p':\"v\"}")); + assertEquals("{\"p\":\"v\"}", tickToDoubleQuote("{\"p\":'v'}")); + } + + @Test + public void testTicksDoubleQuotesInDoubleQuotes() { + assertEquals("{\"p\":\"'\\\"'\\\"\"}", tickToDoubleQuote("{\"p\":\"'\\\"'\\\"\"}")); + } + + @Test + public void testTicksDoubleQuotesInTicks() { + assertEquals("{\"p\":\"'\\\"'\\\"\"}", tickToDoubleQuote("{\"p\":'\\'\\\"\\'\\\"'}")); + assertEquals("{\"p\":\"'\\\"'\\\"\"}", tickToDoubleQuote("{\"p\":'\\'\"\\'\"'}")); + } + + @Test + public void testTickToQuoteWithUtf8Escaped() { + assertEquals("{\"p\":\"\\u03A9\\u03A6\\u00A5\"}", tickToDoubleQuote("{'p':\"\\u03A9\\u03A6\\u00A5\"}")); + } + +} diff --git a/src/test/java/org/apache/sling/jcr/contentparser/impl/TestUtils.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/TestUtils.java index be395b9..0908a47 100644 --- a/src/test/java/org/apache/sling/jcr/contentparser/impl/TestUtils.java +++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/TestUtils.java @@ -19,10 +19,12 @@ package org.apache.sling.jcr.contentparser.impl; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import org.apache.commons.lang3.CharEncoding; import org.apache.sling.jcr.contentparser.ContentParser; import org.apache.sling.jcr.contentparser.impl.mapsupport.ContentElement; import org.apache.sling.jcr.contentparser.impl.mapsupport.ContentElementHandler; @@ -42,4 +44,12 @@ public final class TestUtils { } } + public static ContentElement parse(ContentParser contentParser, String jsonContent) throws IOException { + try (ByteArrayInputStream is = new ByteArrayInputStream(jsonContent.getBytes(CharEncoding.UTF_8))) { + ContentElementHandler handler = new ContentElementHandler(); + contentParser.parse(handler, is); + return handler.getRoot(); + } + } + } -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
