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@1795961 13f79535-47bb-0310-9956-ffa450edef68
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/JsonParserFeature.java b/src/main/java/org/apache/sling/jcr/contentparser/JsonParserFeature.java
new file mode 100644
index 0000000..59c5a23
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentparser/JsonParserFeature.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Feature flags for parsing JSON files.
+ */
+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 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
@@ -47,11 +48,18 @@
"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 @@
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 @@
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.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 @@
*/
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);
+ 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.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.
*/
-@org.osgi.annotation.versioning.Version("1.1.0")
+@org.osgi.annotation.versioning.Version("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 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 @@
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 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();
+ }
+ }
+
}