You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2019/07/22 09:43:55 UTC

[sling-org-apache-sling-contentparser-json] 01/08: SLING-8570 - Extract a generic Content Parser API from org.apache.sling.jcr.contentparser with pluggable implementations

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-contentparser-json.git

commit ebbec8446d5a5efec62e5471bd0b91de610c7bbe
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Wed Jul 10 19:19:43 2019 +0200

    SLING-8570 - Extract a generic Content Parser API from org.apache.sling.jcr.contentparser with pluggable implementations
    
    * extracted API bundle
    * first attempt at implementing a separate JSON content parser
---
 pom.xml                                            | 112 +++++++++
 .../json/internal/JsonContentParser.java           | 212 ++++++++++++++++
 .../json/internal/JsonTicksConverter.java          | 106 ++++++++
 .../json/internal/JsonContentParserTest.java       | 186 ++++++++++++++
 .../contentparser/json/internal/TestUtils.java     |  60 +++++
 .../json/internal/mapsupport/ContentElement.java   |  52 ++++
 .../internal/mapsupport/ContentElementHandler.java |  69 ++++++
 .../internal/mapsupport/ContentElementImpl.java    |  68 +++++
 src/test/resources/content-test/content.json       | 274 +++++++++++++++++++++
 .../invalid-test/contentWithObjectList.json        |  14 ++
 src/test/resources/invalid-test/invalid.json       |   1 +
 11 files changed, 1154 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a33598b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling-bundle-parent</artifactId>
+        <version>35</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.contentparser.json</artifactId>
+    <version>0.9.0-SNAPSHOT</version>
+
+    <name>Apache Sling Content Parser API</name>
+    <description>
+        Parser API Apache Sling Resource trees stored in files (e.g. JSON, FileVault XML, etc.).
+    </description>
+
+    <scm>
+        <connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-contentparser-json.git</connection>
+        <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-contentparser-json.git</developerConnection>
+        <url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-contentparser-json.git</url>
+      <tag>HEAD</tag>
+  </scm>
+
+    <build>
+        <plugins>
+            
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.contentparser.api</artifactId>
+            <version>0.9.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-json_1.0_spec</artifactId>
+            <version>1.0-alpha-1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.framework</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>15.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-core</artifactId>
+            <version>1.0.0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/contentparser/json/internal/JsonContentParser.java b/src/main/java/org/apache/sling/contentparser/json/internal/JsonContentParser.java
new file mode 100644
index 0000000..16d47e0
--- /dev/null
+++ b/src/main/java/org/apache/sling/contentparser/json/internal/JsonContentParser.java
@@ -0,0 +1,212 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonReaderFactory;
+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.contentparser.api.ContentHandler;
+import org.apache.sling.contentparser.api.ContentParser;
+import org.apache.sling.contentparser.api.JsonParserFeature;
+import org.apache.sling.contentparser.api.ParseException;
+import org.apache.sling.contentparser.api.ParserHelper;
+import org.apache.sling.contentparser.api.ParserOptions;
+import org.osgi.service.component.annotations.Component;
+
+@Component(
+        property = {
+                ContentParser.SERVICE_PROPERTY_CONTENT_TYPE + "=" + ContentParser.JSON_CONTENT_TYPE
+        }
+)
+public class JsonContentParser implements ContentParser {
+
+    @Override
+    public void parse(ContentHandler handler, InputStream is, ParserOptions parserOptions) throws ParseException {
+        final boolean jsonQuoteTicks = parserOptions.getJsonParserFeatures().contains(JsonParserFeature.QUOTE_TICK);
+
+        /*
+         * Implementation note: This parser uses JsonReader instead of the (more memory-efficient)
+         * JsonParser Stream API because otherwise it would not be possible to report parent resources
+         * including all properties properly before their children.
+         */
+        final JsonReaderFactory jsonReaderFactory =
+                Json.createReaderFactory(
+                        parserOptions.getJsonParserFeatures().contains(JsonParserFeature.COMMENTS) ?
+                                new HashMap<String, Object>() {{
+                                    put("org.apache.johnzon.supports-comments", true);
+                                }} :
+                                Collections.emptyMap()
+                );
+        JsonObject jsonObject = jsonQuoteTicks ? toJsonObjectWithJsonTicks(jsonReaderFactory, is) : toJsonObject(jsonReaderFactory, is);
+        parse(handler, jsonObject, parserOptions, "/");
+    }
+
+    private JsonObject toJsonObject(JsonReaderFactory jsonReaderFactory, InputStream is) {
+        try (JsonReader reader = jsonReaderFactory.createReader(is)) {
+            return reader.readObject();
+        } catch (JsonParsingException ex) {
+            throw new ParseException("Error parsing JSON content: " + ex.getMessage(), ex);
+        }
+    }
+
+    private JsonObject toJsonObjectWithJsonTicks(JsonReaderFactory jsonReaderFactory, 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);
+        }
+    }
+
+    private void parse(ContentHandler handler, JsonObject object, ParserOptions parserOptions, String path) {
+        // parse JSON object
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, JsonObject> children = new LinkedHashMap<>();
+        for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
+            String childName = entry.getKey();
+            Object value = null;
+            boolean ignore = false;
+            try {
+                value = convertValue(parserOptions, entry.getValue());
+            } catch (ParseException ex) {
+                if (parserOptions.getIgnoreResourceNames().contains(childName) || parserOptions.getIgnorePropertyNames()
+                        .contains(removePrefixFromPropertyName(parserOptions.getRemovePropertyNamePrefixes(), childName))) {
+                    ignore = true;
+                } else {
+                    throw ex;
+                }
+            }
+            boolean isResource = (value instanceof JsonObject);
+            if (!ignore) {
+                if (isResource) {
+                    ignore = parserOptions.getIgnoreResourceNames().contains(childName);
+                } else {
+                    for (String prefix : parserOptions.getRemovePropertyNamePrefixes()) {
+                        if (childName.startsWith(prefix)) {
+                            childName = childName.substring(prefix.length());
+                            break;
+                        }
+
+                    }
+                    ignore = parserOptions.getIgnorePropertyNames().contains(childName);
+                }
+            }
+            if (!ignore) {
+                if (isResource) {
+                    children.put(childName, (JsonObject) value);
+                } else {
+                    properties.put(childName, value);
+                }
+            }
+        }
+        String defaultPrimaryType = parserOptions.getDefaultPrimaryType();
+        if (defaultPrimaryType != null) {
+            if (!properties.containsKey("jcr:primaryType")) {
+                properties.put("jcr:primaryType", defaultPrimaryType);
+            }
+        }
+
+        // report current JSON object
+        handler.resource(path, properties);
+
+        // parse and report children
+        for (Map.Entry<String, JsonObject> entry : children.entrySet()) {
+            String childPath = path.endsWith("/") ? path + entry.getKey() : path + "/" + entry.getKey();
+            parse(handler, entry.getValue(), parserOptions, childPath);
+        }
+    }
+
+    private Object convertValue(ParserOptions parserOptions, JsonValue value) {
+        switch (value.getValueType()) {
+            case STRING:
+                String stringValue = ((JsonString) value).getString();
+                if (parserOptions.isDetectCalendarValues()) {
+                    Calendar calendar = ParserHelper.parseDate(stringValue);
+                    if (calendar != null) {
+                        return calendar;
+                    }
+                }
+                return stringValue;
+            case NUMBER:
+                JsonNumber numberValue = (JsonNumber) value;
+                if (numberValue.isIntegral()) {
+                    return numberValue.longValue();
+                } else {
+                    return numberValue.bigDecimalValue();
+                }
+            case TRUE:
+                return true;
+            case FALSE:
+                return false;
+            case NULL:
+                return null;
+            case ARRAY:
+                JsonArray arrayValue = (JsonArray) value;
+                Object[] values = new Object[arrayValue.size()];
+                for (int i = 0; i < values.length; i++) {
+                    values[i] = convertValue(parserOptions, arrayValue.get(i));
+                }
+                return ParserHelper.convertSingleTypeArray(values);
+            case OBJECT:
+                return value;
+            default:
+                throw new ParseException("Unexpected JSON value type: " + value.getValueType());
+        }
+    }
+
+    private String removePrefixFromPropertyName(Set<String> prefixes, String propertyName) {
+        for (String prefix : prefixes) {
+            if (propertyName.startsWith(prefix)) {
+                return propertyName.substring(prefix.length());
+            }
+        }
+        return propertyName;
+    }
+
+
+}
diff --git a/src/main/java/org/apache/sling/contentparser/json/internal/JsonTicksConverter.java b/src/main/java/org/apache/sling/contentparser/json/internal/JsonTicksConverter.java
new file mode 100644
index 0000000..df0be76
--- /dev/null
+++ b/src/main/java/org/apache/sling/contentparser/json/internal/JsonTicksConverter.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.contentparser.json.internal;
+
+/**
+ * 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>
+ */
+final class JsonTicksConverter {
+    
+    private JsonTicksConverter() {
+        // static methods only
+    }
+    
+    static String tickToDoubleQuote(final String input) {
+        final int len = input.length();
+        final StringBuilder output = new StringBuilder(len);
+        boolean quoted = false;
+        boolean tickQuoted = false;
+        boolean escaped = false;
+        boolean comment = false;
+        char lastChar = ' ';
+        for (int i = 0; i < len; i++) {
+            char in = input.charAt(i);
+            if (quoted || tickQuoted) {
+                if (escaped) {
+                    if (in != '\'') {
+                        output.append("\\");
+                    }
+                    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 (comment) {
+                    if (lastChar == '*' && in == '/') {
+                        comment = false;
+                    }
+                }
+                else {
+                    if (lastChar == '/' && in == '*') {
+                        comment = true;
+                    }
+                    else if (in == '\'') {
+                        in = '"';
+                        tickQuoted = true;
+                    }
+                    else if (in == '"') {
+                        quoted = true;
+                    }
+                }
+            }
+            if (in == '\\') {
+                continue;
+            }
+            output.append(in);
+            lastChar = in;
+        }
+        return output.toString();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/contentparser/json/internal/JsonContentParserTest.java b/src/test/java/org/apache/sling/contentparser/json/internal/JsonContentParserTest.java
new file mode 100644
index 0000000..3099ae8
--- /dev/null
+++ b/src/test/java/org/apache/sling/contentparser/json/internal/JsonContentParserTest.java
@@ -0,0 +1,186 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal;
+
+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.contentparser.api.ContentParser;
+import org.apache.sling.contentparser.api.JsonParserFeature;
+import org.apache.sling.contentparser.api.ParseException;
+import org.apache.sling.contentparser.api.ParserOptions;
+import org.apache.sling.contentparser.json.internal.mapsupport.ContentElement;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+import static junit.framework.TestCase.assertNull;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class JsonContentParserTest {
+
+    private File file;
+    private ContentParser contentParser;
+
+    @Before
+    public void setUp() {
+        file = new File("src/test/resources/content-test/content.json");
+        contentParser = new JsonContentParser();
+    }
+
+    @Test
+    public void testPageJcrPrimaryType() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, file);
+
+        assertEquals("app:Page", content.getProperties().get("jcr:primaryType"));
+    }
+
+    @Test
+    public void testDataTypes() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, file);
+
+        Map<String, Object> props = content.getChild("toolbar/profiles/jcr:content").getProperties();
+        assertEquals(true, props.get("hideInNav"));
+
+        assertEquals(1234567890123L, props.get("longProp"));
+        assertEquals(new BigDecimal("1.2345"), props.get("decimalProp"));
+        assertEquals(true, props.get("booleanProp"));
+
+        assertArrayEquals(new Long[]{1234567890123L, 55L}, (Long[]) props.get("longPropMulti"));
+        assertArrayEquals(new BigDecimal[]{new BigDecimal("1.2345"), new BigDecimal("1.1")}, (BigDecimal[]) props.get("decimalPropMulti"));
+        assertArrayEquals(new Boolean[]{true, false}, (Boolean[]) props.get("booleanPropMulti"));
+    }
+
+    @Test
+    public void testContentProperties() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, file);
+
+        Map<String, Object> props = content.getChild("jcr:content/header").getProperties();
+        assertEquals("/content/dam/sample/header.png", props.get("imageReference"));
+    }
+
+    @Test
+    public void testCalendar() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, new ParserOptions().detectCalendarValues(true), file);
+
+        Map<String, Object> props = content.getChild("jcr:content").getProperties();
+
+        Calendar calendar = (Calendar) props.get("app:lastModified");
+        assertNotNull(calendar);
+
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+2"));
+
+        assertEquals(2014, calendar.get(Calendar.YEAR));
+        assertEquals(4, calendar.get(Calendar.MONTH) + 1);
+        assertEquals(22, calendar.get(Calendar.DAY_OF_MONTH));
+
+        assertEquals(15, calendar.get(Calendar.HOUR_OF_DAY));
+        assertEquals(11, calendar.get(Calendar.MINUTE));
+        assertEquals(24, calendar.get(Calendar.SECOND));
+    }
+
+    @Test
+    public void testIso8601Calendar() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, new ParserOptions().detectCalendarValues(true), file);
+
+        Map<String, Object> props = content.getChild("jcr:content").getProperties();
+
+        Calendar calendar = (Calendar) props.get("dateISO8601String");
+        assertNotNull(calendar);
+
+        assertEquals(2014, calendar.get(Calendar.YEAR));
+        assertEquals(4, calendar.get(Calendar.MONTH) + 1);
+        assertEquals(22, calendar.get(Calendar.DAY_OF_MONTH));
+
+        assertEquals(15, calendar.get(Calendar.HOUR_OF_DAY));
+        assertEquals(11, calendar.get(Calendar.MINUTE));
+        assertEquals(24, calendar.get(Calendar.SECOND));
+    }
+
+    @Test
+    public void testUTF8Chars() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, file);
+
+        Map<String, Object> props = content.getChild("jcr:content").getProperties();
+
+        assertEquals("äöü߀", props.get("utf8Property"));
+    }
+
+    @Test(expected = ParseException.class)
+    public void testParseInvalidJson() throws Exception {
+        file = new File("src/test/resources/invalid-test/invalid.json");
+        ContentElement content = TestUtils.parse(contentParser, file);
+        assertNull(content);
+    }
+
+    @Test(expected = ParseException.class)
+    public void testParseInvalidJsonWithObjectList() throws Exception {
+        file = new File("src/test/resources/invalid-test/contentWithObjectList.json");
+        ContentElement content = TestUtils.parse(contentParser, file);
+        assertNull(content);
+    }
+
+    @Test
+    public void testIgnoreResourcesProperties() throws Exception {
+        ContentElement content = TestUtils.parse(
+                contentParser,
+                new ParserOptions().ignoreResourceNames(ImmutableSet.of("header", "newslist", "security:acl", "security:principals"))
+                        .ignorePropertyNames(ImmutableSet.of("jcr:title")), file);
+        ContentElement child = content.getChild("jcr:content");
+
+        assertEquals("Sample Homepage", child.getProperties().get("pageTitle"));
+        assertNull(child.getProperties().get("jcr:title"));
+
+        assertNull(child.getChildren().get("header"));
+        assertNull(child.getChildren().get("newslist"));
+        assertNotNull(child.getChildren().get("lead"));
+
+        assertEquals("abc", child.getProperties().get("refpro1"));
+        assertEquals("def", child.getProperties().get("pathprop1"));
+    }
+
+    @Test
+    public void testGetChild() throws Exception {
+        ContentElement content = TestUtils.parse(contentParser, file);
+        assertNull(content.getName());
+
+        ContentElement deepChild = content.getChild("jcr:content/par/image/file/jcr:content");
+        assertEquals("jcr:content", deepChild.getName());
+        assertEquals("nt:resource", deepChild.getProperties().get("jcr:primaryType"));
+
+        ContentElement invalidChild = content.getChild("non/existing/path");
+        assertNull(invalidChild);
+
+        invalidChild = content.getChild("/jcr:content");
+        assertNull(invalidChild);
+    }
+
+    @Test(expected = ParseException.class)
+    public void testFailsWithoutCommentsEnabled() throws Exception {
+        TestUtils.parse(contentParser, new ParserOptions().jsonParserFeatures(EnumSet.noneOf(JsonParserFeature.class)), file);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/contentparser/json/internal/TestUtils.java b/src/test/java/org/apache/sling/contentparser/json/internal/TestUtils.java
new file mode 100644
index 0000000..0d81e36
--- /dev/null
+++ b/src/test/java/org/apache/sling/contentparser/json/internal/TestUtils.java
@@ -0,0 +1,60 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sling.contentparser.api.ContentParser;
+import org.apache.sling.contentparser.api.ParserOptions;
+import org.apache.sling.contentparser.json.internal.mapsupport.ContentElement;
+import org.apache.sling.contentparser.json.internal.mapsupport.ContentElementHandler;
+
+public final class TestUtils {
+    
+    private TestUtils() {
+        // static methods only
+    }
+
+    static ContentElement parse(ContentParser contentParser, File file) throws IOException {
+        return parse(contentParser, new ParserOptions(), file);
+    }
+
+    static ContentElement parse(ContentParser contentParser, ParserOptions parserOptions, File file) throws IOException {
+        try (FileInputStream fis = new FileInputStream(file);
+             BufferedInputStream bis = new BufferedInputStream(fis)) {
+            ContentElementHandler handler = new ContentElementHandler();
+            contentParser.parse(handler, bis, parserOptions);
+            return handler.getRoot();
+        }
+    }
+    
+    static ContentElement parse(ContentParser contentParser, String jsonContent) throws IOException {
+        try (ByteArrayInputStream is = new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8))) {
+            ContentElementHandler handler = new ContentElementHandler();
+            contentParser.parse(handler, is, new ParserOptions());
+            return handler.getRoot();
+        }
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElement.java b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElement.java
new file mode 100644
index 0000000..0a4457e
--- /dev/null
+++ b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElement.java
@@ -0,0 +1,52 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal.mapsupport;
+
+import java.util.Map;
+
+/**
+ * Represents a resource or node in the content hierarchy.
+ */
+public interface ContentElement {
+
+    /**
+     * @return Resource name. The root resource has no name (null).
+     */
+    String getName();
+    
+    /**
+     * Properties of this resource.
+     * @return Properties (keys, values)
+     */
+    Map<String, Object> getProperties();
+    
+    /**
+     * Get children of current resource. The Map preserves the ordering of children.
+     * @return Children (child names, child objects)
+     */
+    Map<String, ContentElement> getChildren();
+    
+    /**
+     * Get child or descendant
+     * @param path Relative path to address child or one of it's descendants (use "/" as hierarchy separator).
+     * @return Child or null if no child found with this path
+     */
+    ContentElement getChild(String path);
+    
+}
diff --git a/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementHandler.java b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementHandler.java
new file mode 100644
index 0000000..bc21ec0
--- /dev/null
+++ b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementHandler.java
@@ -0,0 +1,69 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal.mapsupport;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.contentparser.api.ContentHandler;
+
+/**
+ * {@link ContentHandler} implementation that produces a tree of {@link ContentElement} items.
+ */
+public class ContentElementHandler implements ContentHandler {
+    
+    private ContentElement root;
+    private Pattern PATH_PATTERN = Pattern.compile("^((/[^/]+)*)(/([^/]+))$"); 
+
+    @Override
+    public void resource(String path, Map<String, Object> properties) {
+        if (StringUtils.equals(path, "/")) {
+            root = new ContentElementImpl(null, properties);
+        }
+        else {
+            if (root == null) {
+                throw new RuntimeException("Root resource not set.");
+            }
+            Matcher matcher = PATH_PATTERN.matcher(path);
+            if (!matcher.matches()) {
+                throw new RuntimeException("Unexpected path:" + path);
+            }
+            String relativeParentPath = StringUtils.stripStart(matcher.group(1), "/");
+            String name = matcher.group(4);
+            ContentElement parent;
+            if (StringUtils.isEmpty(relativeParentPath)) {
+                parent = root;
+            }
+            else {
+                parent = root.getChild(relativeParentPath);
+            }
+            if (parent == null) {
+                throw new RuntimeException("Parent '" + relativeParentPath + "' does not exist.");
+            }
+            parent.getChildren().put(name, new ContentElementImpl(name, properties));
+        }
+    }
+    
+    public ContentElement getRoot() {
+        return root;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementImpl.java b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementImpl.java
new file mode 100644
index 0000000..ddbf9bd
--- /dev/null
+++ b/src/test/java/org/apache/sling/contentparser/json/internal/mapsupport/ContentElementImpl.java
@@ -0,0 +1,68 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.contentparser.json.internal.mapsupport;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+final class ContentElementImpl implements ContentElement {
+    
+    private final String name;
+    private final Map<String, Object> properties;
+    private final Map<String, ContentElement> children = new LinkedHashMap<>();
+    
+    public ContentElementImpl(String name, Map<String, Object> properties) {
+        this.name = name;
+        this.properties = properties;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public Map<String, ContentElement> getChildren() {
+        return children;
+    }
+
+    @Override
+    public ContentElement getChild(String path) {
+        String name = StringUtils.substringBefore(path, "/");
+        ContentElement child = children.get(name);
+        if (child == null) {
+          return null;
+        }
+        String remainingPath = StringUtils.substringAfter(path, "/");
+        if (StringUtils.isEmpty(remainingPath)) {
+          return child;
+        }
+        else {
+          return child.getChild(remainingPath);
+        }
+    }
+
+}
diff --git a/src/test/resources/content-test/content.json b/src/test/resources/content-test/content.json
new file mode 100644
index 0000000..8890044
--- /dev/null
+++ b/src/test/resources/content-test/content.json
@@ -0,0 +1,274 @@
+/* Comment example */
+{
+  "jcr:primaryType": "app:Page",
+  "jcr:createdBy": "admin",
+  "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+  /* Comment example */
+  "jcr:content": {
+    "jcr:primaryType": "app:PageContent",  /* Comment example */
+    "jcr:createdBy": "admin",
+    "jcr:title": "English",
+    "app:template": "sample/templates/homepage",
+    "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+    "app:lastModified": "Tue Apr 22 2014 15:11:24 GMT+0200",
+    "dateISO8601String": "2014-04-22T15:11:24.000+02:00",
+    "pageTitle": "Sample Homepage",
+    "sling:resourceType": "sample/components/homepage",
+    "sling:resourceSuperType": "sample/components/supertype",
+    "app:designPath": "/etc/designs/sample",
+    "app:lastModifiedBy": "admin",
+    "utf8Property": "äöü߀",
+    "jcr:reference:refpro1": "abc",
+    "jcr:path:pathprop1": "def",
+    /* should be ignored */
+    "security:acl": [
+        { "principal": "TestGroup1", "granted": ["jcr:read","jcr:write"] },
+        { "principal": "TestUser1", "granted": ["jcr:read"], "denied": ["jcr:write"] }
+    ],
+    /* should be ignored */
+    "security:principals": [
+        { "name": "TestUser1", "password": "mypassword", "extraProp1": "extraProp1Value" },
+        { "name": "TestGroup1", "isgroup": "true", "members": ["TestUser1"], "extraProp1": "extraProp1Value" }
+    ],
+    "par": {
+      "jcr:primaryType": "nt:unstructured",
+      "sling:resourceType": "foundation/components/parsys",
+      "colctrl": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "layout": "2;colctrl-lt0",
+        "jcr:created": "Mon Aug 23 2010 22:02:24 GMT+0200",
+        "jcr:lastModified": "Mon Aug 23 2010 22:02:35 GMT+0200",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      },
+      "image": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "fileReference": "/content/dam/sample/portraits/jane_doe.jpg",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:03:39 GMT+0200",
+        "width": "340",
+        "jcr:lastModified": "Sun Oct 31 2010 21:39:50 GMT+0100",
+        "sling:resourceType": "foundation/components/image",
+        "file": {
+          "jcr:primaryType": "nt:file",
+          "jcr:createdBy": "admin",
+          "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+          "jcr:content": {
+            "jcr:primaryType": "nt:resource",
+            "jcr:lastModifiedBy": "anonymous",
+            "jcr:mimeType": "image/jpeg",
+            "jcr:lastModified": "Thu Aug 07 2014 16:32:59 GMT+0200",
+            ":jcr:data": 24377,
+            "jcr:uuid": "eda76d00-b2cd-4b59-878f-c33f71ceaddc"
+          }
+        }
+      },
+      "title_1": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:title": "Strategic Consulting",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:12:08 GMT+0200",
+        "jcr:lastModified": "Wed Oct 27 2010 21:33:24 GMT+0200",
+        "sling:resourceType": "sample/components/title"
+      },
+      "text_1": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Sun Oct 31 2010 21:48:04 GMT+0100",
+        "text": "<p><span class=\"Apple-style-span\" style=\"font-size: 12px;\">In&nbsp;today's competitive market, organizations can face several key geometric challenges:<\/span><\/p>\n<ul>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Polyhedral Sectioning<\/span><\/li>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Triangulation&nbsp;<\/span><\/li>\n<li><span class=\"Apple-style-span\" style=\"font-size: 12px;\">Trigonometric Calculation<\/span><\ [...]
+        "jcr:lastModified": "Sun Oct 31 2010 21:49:06 GMT+0100",
+        "sling:resourceType": "foundation/components/text",
+        "textIsRich": "true"
+      },
+      "col_break12825937554040": {
+        "jcr:primaryType": "nt:unstructured",
+        "controlType": "break",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      },
+      "image_0": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "fileReference": "/content/dam/sample/offices/clean_room.jpg",
+        "height": "226",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:04:46 GMT+0200",
+        "jcr:lastModified": "Fri Nov 05 2010 10:38:15 GMT+0100",
+        "sling:resourceType": "foundation/components/image",
+        "imageRotate": "0",
+        "file": {
+          "jcr:primaryType": "nt:file",
+          "jcr:createdBy": "admin",
+          "jcr:created": "Thu Aug 07 2014 16:32:59 GMT+0200",
+          "jcr:content": {
+            "jcr:primaryType": "nt:resource",
+            "jcr:lastModifiedBy": "anonymous",
+            "jcr:mimeType": "image/jpeg",
+            "jcr:lastModified": "Thu Aug 07 2014 16:32:59 GMT+0200",
+            ":jcr:data": 21142,
+            "jcr:uuid": "6139077f-191f-4337-aaef-55456ebe6784"
+          }
+        }
+      },
+      "title_2": {
+        "jcr:createdBy": "admin",
+        "jcr:title": "Shape Technology",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:12:13 GMT+0200",
+        "jcr:lastModified": "Tue Oct 26 2010 21:16:29 GMT+0200",
+        "sling:resourceType": "sample/components/title"
+      },
+      "text_0": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Mon Aug 23 2010 22:16:30 GMT+0200",
+        "text": "<p>The Sample investment in R&amp;D has done more than solidify our industry leadership role, we have now outpaced our competitors to such an extent that we are in an altogether new space.<\/p>\n<p>This is why our high quality polygons and polyhedra provide the only turnkey solutions across the whole range of euclidean geometry. And our mathematicians are working on the next generation of fractal curves to bring you shapes that are unthinkable today.<\/p>\n<p><\/p>\n<p>< [...]
+        "jcr:lastModified": "Mon Nov 08 2010 20:39:00 GMT+0100",
+        "sling:resourceType": "foundation/components/text",
+        "textIsRich": "true"
+      },
+      "col_end12825937444810": {
+        "jcr:primaryType": "nt:unstructured",
+        "controlType": "end",
+        "sling:resourceType": "foundation/components/parsys/colctrl"
+      }
+    },
+    "header": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:title": "trust our experience\r\nto manage your business",
+      "imageReference": "/content/dam/sample/header.png",
+      "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc eget neque. Nunc condimentum ipsum et orci. Aenean est. Cras eget diam. read more",
+      "sling:resourceType": "sample/components/header"
+    },
+    "newslist": {
+      "jcr:primaryType": "nt:unstructured",
+      "headline": "trust our experience\nto manage your business",
+      "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc eget neque. Nunc condimentum ipsum et orci. Aenean est. Cras eget diam. read more",
+      "sling:resourceType": "sample/components/listchildren",
+      "listroot": "/content/sample/en/about/news"
+    },
+    "lead": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:title": "World Leader in Applied Geometry ",
+      "jcr:lastModifiedBy": "admin",
+      "text": "Lead Text",
+      "title": "Lead Title",
+      "jcr:description": "Sample has been selling and servicing shapes for over 2000 years. From our beginnings as a small vendor of squares and rectangles we have grown our business into a leading global provider of platonic solids and fractals. Join us as we lead geometry into the future.",
+      "jcr:lastModified": "Wed Jan 19 2011 14:35:29 GMT+0100",
+      "sling:resourceType": "sample/components/lead",
+      "app:annotations": {"jcr:primaryType": "nt:unstructured"}
+    },
+    "image": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:lastModifiedBy": "admin",
+      "jcr:lastModified": "Wed Oct 27 2010 21:30:59 GMT+0200",
+      "imageRotate": "0"
+    },
+    "carousel": {
+      "jcr:primaryType": "nt:unstructured",
+      "playSpeed": "6000",
+      "jcr:lastModifiedBy": "admin",
+      "pages": [
+        "/content/sample/en/events/techsummit",
+        "/content/sample/en/events/userconf",
+        "/content/sample/en/events/shapecon",
+        "/content/sample/en/events/dsc"
+      ],
+      "jcr:lastModified": "Tue Oct 05 2010 14:14:27 GMT+0200",
+      "transTime": "1000",
+      "sling:resourceType": "foundation/components/carousel",
+      "listFrom": "static"
+    },
+    "rightpar": {
+      "jcr:primaryType": "nt:unstructured",
+      "sling:resourceType": "foundation/components/parsys",
+      "teaser": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:createdBy": "admin",
+        "jcr:lastModifiedBy": "admin",
+        "jcr:created": "Tue Jan 25 2011 11:30:09 GMT+0100",
+        "campaignpath": "/content/campaigns/sample",
+        "jcr:lastModified": "Wed Feb 02 2011 08:40:30 GMT+0100",
+        "sling:resourceType": "personalization/components/teaser"
+      }
+    }
+  },
+  "toolbar": {
+    "jcr:primaryType": "app:Page",
+    "jcr:createdBy": "admin",
+    "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+    "jcr:content": {
+      "jcr:primaryType": "app:PageContent",
+      "subtitle": "Contains the toolbar",
+      "jcr:createdBy": "admin",
+      "jcr:title": "Toolbar",
+      "app:template": "sample/templates/contentpage",
+      "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+      "app:lastModified": "Wed Aug 25 2010 22:51:02 GMT+0200",
+      "hideInNav": "true",
+      "sling:resourceType": "sample/components/contentpage",
+      "app:lastModifiedBy": "admin",
+      "par": {
+        "jcr:primaryType": "nt:unstructured",
+        "sling:resourceType": "foundation/components/parsys"
+      },
+      "rightpar": {
+        "jcr:primaryType": "nt:unstructured",
+        "sling:resourceType": "foundation/components/iparsys",
+        "iparsys_fake_par": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/iparsys/par"
+        }
+      }
+    },
+    "profiles": {
+      "jcr:primaryType": "app:Page",
+      "jcr:createdBy": "admin",
+      "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+      "jcr:content": {
+        "jcr:primaryType": "app:PageContent",
+        "jcr:mixinTypes": ["type1","type2"],
+        "jcr:createdBy": "admin",
+        "jcr:title": "Profiles",
+        "app:template": "sample/templates/contentpage",
+        "jcr:created": "Thu Aug 07 2014 16:33:00 GMT+0200",
+        "app:lastModified": "Thu Nov 05 2009 20:27:13 GMT+0100",
+        "hideInNav": true,
+        "sling:resourceType": "sample/components/contentpage",
+        "app:lastModifiedBy": "admin",
+        "longProp": 1234567890123,
+        "decimalProp": 1.2345,
+        "booleanProp": true,
+        "longPropMulti": [1234567890123,55],
+        "decimalPropMulti": [1.2345,1.1],
+        "booleanPropMulti": [true,false],
+        "stringPropMulti": ["aa","bb","cc"],
+        "par": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/parsys",
+          "textimage": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "foundation/components/textimage"
+          },
+          "mygadgets": {
+            "jcr:primaryType": "nt:unstructured",
+            "gadgets": "http://customer.meteogroup.de/meteogroup/gadgets/wetter24.xml\nhttp://germanweatherradar.googlecode.com/svn/trunk/german-weather-radar.xml\nhttp://www.digitalpowered.info/gadget/ski.pictures.xml\nhttp://www.canbuffi.de/gadgets/clock/clock.xml",
+            "sling:resourceType": "personalization/components/mygadgets"
+          }
+        },
+        "rightpar": {
+          "jcr:primaryType": "nt:unstructured",
+          "sling:resourceType": "foundation/components/iparsys",
+          "iparsys_fake_par": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "foundation/components/iparsys/par"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/resources/invalid-test/contentWithObjectList.json b/src/test/resources/invalid-test/contentWithObjectList.json
new file mode 100644
index 0000000..e4527a0
--- /dev/null
+++ b/src/test/resources/invalid-test/contentWithObjectList.json
@@ -0,0 +1,14 @@
+{
+  "prop1": "value1",
+  "childObject": {
+    "prop2": "value2"
+  },
+  "childObjectList": [
+    {
+      "prop1": "value1"
+    },
+    {
+      "prop2": "value2"
+    }
+  ]
+}
diff --git a/src/test/resources/invalid-test/invalid.json b/src/test/resources/invalid-test/invalid.json
new file mode 100644
index 0000000..59fc86c
--- /dev/null
+++ b/src/test/resources/invalid-test/invalid.json
@@ -0,0 +1 @@
+This is invalid json.