You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/10/20 14:41:02 UTC

[sling-org-apache-sling-jcr-contentparser] 03/38: SLING-6592 switch to Stream API for content parsing, remove content representation API

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

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

commit 536aed4674ef83223fe8cbbb496b7498081ad75e
Author: Stefan Seifert <ss...@apache.org>
AuthorDate: Fri Mar 17 20:59:21 2017 +0000

    SLING-6592 switch to Stream API for content parsing, remove content representation API
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1787499 13f79535-47bb-0310-9956-ffa450edef68
---
 .../{ContentParser.java => ContentHandler.java}    | 21 +++----
 .../sling/jcr/contentparser/ContentParser.java     |  9 ++-
 .../jcr/contentparser/ContentParserFactory.java    |  2 +-
 .../sling/jcr/contentparser/ContentType.java       |  2 +-
 .../sling/jcr/contentparser/ParserOptions.java     |  4 +-
 .../contentparser/impl/JcrXmlContentParser.java    | 67 ++++++++++++++-------
 .../jcr/contentparser/impl/JsonContentParser.java  | 44 ++++++++++----
 .../sling/jcr/contentparser/impl/ParserHelper.java | 13 +++-
 .../sling/jcr/contentparser/package-info.java      |  2 +-
 .../impl/JcrXmlContentParserTest.java              | 55 ++++++++++-------
 .../contentparser/impl/JsonContentParserTest.java  | 61 ++++++++++++-------
 .../sling/jcr/contentparser/impl/TestUtils.java    | 27 ++-------
 .../impl/mapsupport/ContentElement.java}           | 44 +++++++-------
 .../impl/mapsupport/ContentElementHandler.java     | 69 ++++++++++++++++++++++
 .../impl/mapsupport/ContentElementImpl.java        | 68 +++++++++++++++++++++
 15 files changed, 348 insertions(+), 140 deletions(-)

diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java b/src/main/java/org/apache/sling/jcr/contentparser/ContentHandler.java
similarity index 62%
copy from src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java
copy to src/main/java/org/apache/sling/jcr/contentparser/ContentHandler.java
index 1d9595a..7582338 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/ContentHandler.java
@@ -18,23 +18,20 @@
  */
 package org.apache.sling.jcr.contentparser;
 
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.Map;
 
 /**
- * Parses repository content from a file.
- * Implementations have to be thread-safe.
+ * Handler that gets notified while parsing content with {@link ContentParser}.
+ * The resources are always reported in order of their paths as found in the content fragment.
+ * Parents are always reported before their children.
  */
-public interface ContentParser {
+public interface ContentHandler {
 
     /**
-     * Parse content.
-     * @param is Stream with serialized content
-     * @return Content as Map
-     * @throws IOException When I/O error occurs.
-     * @throws ParseException When parsing error occurs.
+     * Resource found in parsed content.
+     * @param path Path of resource inside the content fragment. The root resource from the content fragment has a path "/".
+     * @param properties Resource properties
      */
-    Map<String,Object> parse(InputStream is) throws IOException, ParseException;
-
+    void resource(String path, Map<String,Object> properties);
+    
 }
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java b/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java
index 1d9595a..fa6877d 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/ContentParser.java
@@ -20,7 +20,6 @@ package org.apache.sling.jcr.contentparser;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Map;
 
 /**
  * Parses repository content from a file.
@@ -29,12 +28,12 @@ import java.util.Map;
 public interface ContentParser {
 
     /**
-     * Parse content.
-     * @param is Stream with serialized content
-     * @return Content as Map
+     * Parse content in a "stream-based" way. Each resource that is found in the content is reported to the contentHandler.
+     * @param contentHandler Content handler that accepts the parsed content.
+     * @param inputStream Stream with serialized content
      * @throws IOException When I/O error occurs.
      * @throws ParseException When parsing error occurs.
      */
-    Map<String,Object> parse(InputStream is) throws IOException, ParseException;
+    void parse(ContentHandler contentHandler, InputStream inputStream) throws IOException, ParseException;
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ContentParserFactory.java b/src/main/java/org/apache/sling/jcr/contentparser/ContentParserFactory.java
index 7ae64e2..7357ec5 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ContentParserFactory.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/ContentParserFactory.java
@@ -22,7 +22,7 @@ import org.apache.sling.jcr.contentparser.impl.JcrXmlContentParser;
 import org.apache.sling.jcr.contentparser.impl.JsonContentParser;
 
 /**
- * Factory for content file parsers.
+ * Factory for content parsers.
  */
 public final class ContentParserFactory {
 
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java b/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java
index 167722f..acba05d 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java
@@ -29,7 +29,7 @@ public enum ContentType {
     JSON("json"),
 
     /**
-     * JCR XML content.
+     * JCR XML content (FileVault XML).
      */
     JCR_XML("jcr.xml");
 
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 0911dac..4dba088 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/ParserOptions.java
@@ -24,7 +24,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 /**
- * Options for content filer parser.
+ * Options for content parser.
  */
 public final class ParserOptions {
     
@@ -60,7 +60,7 @@ public final class ParserOptions {
     }
 
     /**
-     * Some content file formats like JSON do not contain information to identify date/time values.
+     * Some content formats like JSON do not contain information to identify date/time values.
      * Instead they have to be detected by heuristics by trying to parse every string value.
      * This mode is disabled by default.
      * @param value Activate calendar value detection
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParser.java b/src/main/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParser.java
index 2af30e7..9b9486c 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParser.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParser.java
@@ -20,16 +20,20 @@ package org.apache.sling.jcr.contentparser.impl;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.Deque;
 import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.HashSet;
 import java.util.Map;
-import java.util.Stack;
+import java.util.Set;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.jackrabbit.util.ISO9075;
+import org.apache.sling.jcr.contentparser.ContentHandler;
 import org.apache.sling.jcr.contentparser.ContentParser;
 import org.apache.sling.jcr.contentparser.ParseException;
 import org.apache.sling.jcr.contentparser.ParserOptions;
@@ -54,15 +58,14 @@ public final class JcrXmlContentParser implements ContentParser {
     }
     
     @Override
-    public Map<String,Object> parse(InputStream is) throws IOException, ParseException {
+    public void parse(ContentHandler handler, InputStream is) throws IOException, ParseException {
         try {
-            XmlHandler xmlHandler = new XmlHandler();
+            XmlHandler xmlHandler = new XmlHandler(handler);
             SAXParser parser = saxParserFactory.newSAXParser();
             parser.parse(is, xmlHandler);
             if (xmlHandler.hasError()) {
                 throw xmlHandler.getError();
             }
-            return xmlHandler.getContent();
         }
         catch (ParserConfigurationException | SAXException ex) {
             throw new ParseException("Error parsing JCR XML content.", ex);
@@ -82,12 +85,13 @@ public final class JcrXmlContentParser implements ContentParser {
      * Parses XML stream to Map.
      */
     class XmlHandler extends DefaultHandler {
-        private final Map<String,Object> content = new LinkedHashMap<>();
-        private final Stack<Map<String,Object>> elements = new Stack<>();
+        private final ContentHandler contentHandler;
+        private final Deque<String> paths = new ArrayDeque<>();
+        private final Set<String> ignoredPaths = new HashSet<>();
         private SAXParseException error;
         
-        public Map<String,Object> getContent() {
-            return content;
+        public XmlHandler(ContentHandler contentHandler) {
+            this.contentHandler = contentHandler;
         }
         
         public boolean hasError() {
@@ -102,36 +106,55 @@ public final class JcrXmlContentParser implements ContentParser {
         public void startElement(String uri, String localName, String qName, Attributes attributes)
                 throws SAXException {
             
-            // prepare map for element
-            Map<String,Object> element;
-            if (elements.isEmpty()) {
-                element = content;
+            String resourceName = decodeName(qName);
+
+            // generate path for element
+            String path;
+            if (paths.isEmpty()) {
+                path = "/";
             }
             else {
-                element = new HashMap<>();
-                String resourceName = decodeName(qName);
-                if (!helper.ignoreResource(resourceName)) {
-                    elements.peek().put(resourceName, element);
+                path = helper.concatenatePath(paths.peek(), resourceName);
+                if (helper.ignoreResource(resourceName)) {
+                    ignoredPaths.add(path);
                 }
             }
-            elements.push(element);
+            paths.push(path);
+            
+            // skip further processing if this path or a parent path is ignored
+            if (isIgnoredPath(path)) {
+                return;
+            }
             
-            // get attributes
+            // get properties
+            Map<String,Object> properties = new HashMap<>();
             for (int i=0; i<attributes.getLength(); i++) {
                 String propertyName = helper.cleanupPropertyName(decodeName(attributes.getQName(i)));
                 if (!helper.ignoreProperty(propertyName)) {
                     Object value = JcrXmlValueConverter.parseValue(propertyName, attributes.getValue(i));
                     if (value != null) {
-                        element.put(propertyName, value);
+                        properties.put(propertyName, value);
                     }
                 }
             }
+            helper.ensureDefaultPrimaryType(properties);
+            contentHandler.resource(path, properties);
+        }
+        
+        private boolean isIgnoredPath(String path) {
+            if (StringUtils.isEmpty(path)) {
+                return false;
+            }
+            if (ignoredPaths.contains(path)) {
+                return true;
+            }
+            String parentPath = StringUtils.substringBeforeLast(path, "/");
+            return isIgnoredPath(parentPath);
         }
 
         @Override
         public void endElement(String uri, String localName, String qName) throws SAXException {
-            Map<String,Object> element = elements.pop();
-            helper.ensureDefaultPrimaryType(element);
+            paths.pop();
         }
 
         @Override
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 a17e91e..093fbec 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
@@ -35,6 +35,7 @@ import javax.json.JsonString;
 import javax.json.JsonValue;
 import javax.json.stream.JsonParsingException;
 
+import org.apache.sling.jcr.contentparser.ContentHandler;
 import org.apache.sling.jcr.contentparser.ContentParser;
 import org.apache.sling.jcr.contentparser.ParseException;
 import org.apache.sling.jcr.contentparser.ParserOptions;
@@ -45,7 +46,12 @@ import org.apache.sling.jcr.contentparser.ParserOptions;
  */
 public final class JsonContentParser implements ContentParser {
     
-    private final ParserHelper helper;    
+    private final ParserHelper helper;
+    /*
+     * 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.
+     */
     private final JsonReaderFactory jsonReaderFactory;
     
     public JsonContentParser(ParserOptions options) {
@@ -57,22 +63,25 @@ public final class JsonContentParser implements ContentParser {
     }
     
     @Override
-    public Map<String,Object> parse(InputStream is) throws IOException, ParseException {
+    public void parse(ContentHandler handler, InputStream is) throws IOException, ParseException {
         try (JsonReader reader = jsonReaderFactory.createReader(is)) {
-            return toMap(reader.readObject());
+            parse(handler, reader.readObject(), "/");
         }
         catch (JsonParsingException ex) {
             throw new ParseException("Error parsing JSON content.", ex);
         }
     }
-    
-    private Map<String,Object> toMap(JsonObject object) {
-        Map<String,Object> map = new LinkedHashMap<>();
+
+    private void parse(ContentHandler handler, JsonObject object, 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 = convertValue(entry.getValue());
+            boolean isResource = (value instanceof JsonObject);
             boolean ignore = false;
-            if (value instanceof Map) {
+            if (isResource) {
                 ignore = helper.ignoreResource(childName);
             }
             else {
@@ -80,11 +89,24 @@ public final class JsonContentParser implements ContentParser {
                 ignore = helper.ignoreProperty(childName);
             }
             if (!ignore) {
-                map.put(childName, value);
+                if (isResource) {
+                    children.put(childName, (JsonObject)value);
+                }
+                else {
+                    properties.put(childName, value);
+                }
             }
         }
-        helper.ensureDefaultPrimaryType(map);
-        return map;
+        helper.ensureDefaultPrimaryType(properties);
+        
+        // report current JSON object
+        handler.resource(path, properties);
+        
+        // parse and report children
+        for (Map.Entry<String,JsonObject> entry : children.entrySet()) {
+            String childPath = helper.concatenatePath(path, entry.getKey());;
+            parse(handler, entry.getValue(), childPath);
+        }
     }
     
     private Object convertValue(JsonValue value) {
@@ -120,7 +142,7 @@ public final class JsonContentParser implements ContentParser {
                 }
                 return helper.convertSingleTypeArray(values);
             case OBJECT:
-                return toMap((JsonObject)value);
+                return (JsonObject)value;
             default:
                 throw new ParseException("Unexpected JSON value type: " + value.getValueType());
         }
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/impl/ParserHelper.java b/src/main/java/org/apache/sling/jcr/contentparser/impl/ParserHelper.java
index 160cd0e..69b0f48 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/impl/ParserHelper.java
+++ b/src/main/java/org/apache/sling/jcr/contentparser/impl/ParserHelper.java
@@ -27,6 +27,8 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import javax.json.JsonObject;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.jcr.contentparser.ParseException;
 import org.apache.sling.jcr.contentparser.ParserOptions;
@@ -113,7 +115,7 @@ class ParserHelper {
             if (value == null) {
                 throw new ParseException("Multivalue array must not contain null values.");
             }
-            if (value instanceof Map) {
+            if (value instanceof Map || value instanceof JsonObject) {
                 throw new ParseException("Multivalue array must not contain maps/objects.");
             }
             if (itemType == null) {
@@ -131,4 +133,13 @@ class ParserHelper {
         return convertedArray;
     }
     
+    public String concatenatePath(String parentPath, String name) {
+        if (StringUtils.endsWith(parentPath, "/")) {
+            return parentPath + name;
+        }
+        else {
+            return parentPath + "/" + name;
+        }
+    }
+    
 }
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 21b7f41..1b6b2a8 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
@@ -17,7 +17,7 @@
  * under the License.
  */
 /**
- * Parser for repository content stored in files (e.g. JSON, JCR XML).
+ * Parser for repository content serialized e.g. as JSON or JCR XML.
  */
 @org.osgi.annotation.versioning.Version("1.0.0")
 package org.apache.sling.jcr.contentparser;
diff --git a/src/test/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParserTest.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParserTest.java
index 1faaf4e..99eca51 100644
--- a/src/test/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParserTest.java
+++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/JcrXmlContentParserTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.sling.jcr.contentparser.impl;
 
-import static org.apache.sling.jcr.contentparser.impl.TestUtils.getDeep;
 import static org.apache.sling.jcr.contentparser.impl.TestUtils.parse;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -37,8 +36,8 @@ import org.apache.sling.jcr.contentparser.ContentParserFactory;
 import org.apache.sling.jcr.contentparser.ContentType;
 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.Ignore;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableSet;
@@ -52,14 +51,13 @@ public class JcrXmlContentParserTest {
         file = new File("src/test/resources/content-test/content.jcr.xml");
     }
 
-    @SuppressWarnings("unchecked")
     @Test
     public void testParseJcrXml() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JCR_XML);
-        Map<String,Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
         assertNotNull(content);
-        assertEquals("app:Page", content.get("jcr:primaryType"));
-        assertEquals("app:PageContent", ((Map<String,Object>)content.get("jcr:content")).get("jcr:primaryType"));
+        assertEquals("app:Page", content.getProperties().get("jcr:primaryType"));
+        assertEquals("app:PageContent", content.getChild("jcr:content").getProperties().get("jcr:primaryType"));
     }
 
     @Test(expected=ParseException.class)
@@ -72,8 +70,8 @@ public class JcrXmlContentParserTest {
     @Test
     public void testDataTypes() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JCR_XML);
-        Map<String,Object> content = parse(underTest, file);
-        Map<String,Object> props = getDeep(content, "jcr:content");
+        ContentElement content = parse(underTest, file);
+        Map<String,Object> props = content.getChild("jcr:content").getProperties();
         
         assertEquals("en", props.get("jcr:title"));
         assertEquals(true, props.get("includeAside"));
@@ -105,26 +103,43 @@ public class JcrXmlContentParserTest {
         ContentParser underTest = ContentParserFactory.create(ContentType.JCR_XML, new ParserOptions()
                 .ignoreResourceNames(ImmutableSet.of("teaserbar", "aside"))
                 .ignorePropertyNames(ImmutableSet.of("longProp", "jcr:title")));
-        Map<String,Object> content = parse(underTest, file);
-        Map<String,Object> props = getDeep(content, "jcr:content");
+        ContentElement content = parse(underTest, file);
+        ContentElement child = content.getChild("jcr:content");
         
-        assertEquals("HOME", props.get("navTitle"));
-        assertNull(props.get("jcr:title"));
-        assertNull(props.get("longProp"));
+        assertEquals("HOME", child.getProperties().get("navTitle"));
+        assertNull(child.getProperties().get("jcr:title"));
+        assertNull(child.getProperties().get("longProp"));
         
-        assertNull(props.get("teaserbar"));
-        assertNull(props.get("aside"));
-        assertNotNull(props.get("content"));
+        assertNull(child.getChildren().get("teaserbar"));
+        assertNull(child.getChildren().get("aside"));
+        assertNotNull(child.getChildren().get("content"));
+    }
+
+    @Test
+    public void testGetChild() throws Exception {
+        ContentParser underTest = ContentParserFactory.create(ContentType.JCR_XML);
+        ContentElement content = parse(underTest, file);
+        assertNull(content.getName());
+        
+        ContentElement deepChild = content.getChild("jcr:content/teaserbar/teaserbaritem");
+        assertEquals("teaserbaritem", deepChild.getName());
+        assertEquals("samples/sample-app/components/content/teaserbar/teaserbarItem", deepChild.getProperties().get("sling:resourceType"));
+
+        ContentElement invalidChild = content.getChild("non/existing/path");
+        assertNull(invalidChild);
+
+        invalidChild = content.getChild("/jcr:content");
+        assertNull(invalidChild);
     }
 
     @Test
-    @Ignore
     public void testSameNamePropertyAndSubResource() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JCR_XML);
-        Map<String,Object> content = parse(underTest, file);
-        Map<String,Object> props = getDeep(content, "jcr:content/teaserbar");
+        ContentElement content = parse(underTest, file);
+        ContentElement child = content.getChild("jcr:content/teaserbar");
         // teaserbaritem is a direct property as well as a sub resource
-        assertEquals("test", props.get("teaserbaritem"));
+        assertEquals("test", child.getProperties().get("teaserbaritem"));
+        assertNotNull(child.getChildren().get("teaserbaritem"));
     }
 
 }
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 3aa9531..706cb6e 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
@@ -18,7 +18,6 @@
  */
 package org.apache.sling.jcr.contentparser.impl;
 
-import static org.apache.sling.jcr.contentparser.impl.TestUtils.getDeep;
 import static org.apache.sling.jcr.contentparser.impl.TestUtils.parse;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -36,6 +35,7 @@ import org.apache.sling.jcr.contentparser.ContentParserFactory;
 import org.apache.sling.jcr.contentparser.ContentType;
 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;
 
@@ -53,17 +53,17 @@ public class JsonContentParserTest {
     @Test
     public void testPageJcrPrimaryType() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
 
-        assertEquals("app:Page", content.get("jcr:primaryType"));
+        assertEquals("app:Page", content.getProperties().get("jcr:primaryType"));
     }
 
     @Test
     public void testDataTypes() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
 
-        Map<String, Object> props = getDeep(content, "toolbar/profiles/jcr:content");
+        Map<String, Object> props = content.getChild("toolbar/profiles/jcr:content").getProperties();
         assertEquals(true, props.get("hideInNav"));
 
         assertEquals(1234567890123L, props.get("longProp"));
@@ -78,9 +78,9 @@ public class JsonContentParserTest {
     @Test
     public void testContentProperties() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
 
-        Map<String, Object> props = getDeep(content, "jcr:content/header");
+        Map<String, Object> props = content.getChild("jcr:content/header").getProperties();
         assertEquals("/content/dam/sample/header.png", props.get("imageReference"));
     }
 
@@ -88,9 +88,9 @@ public class JsonContentParserTest {
     public void testCalendar() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON,
                 new ParserOptions().detectCalendarValues(true));
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
 
-        Map<String, Object> props = getDeep(content, "jcr:content");
+        Map<String, Object> props = content.getChild("jcr:content").getProperties();
 
         Calendar calendar = (Calendar) props.get("app:lastModified");
         assertNotNull(calendar);
@@ -109,9 +109,9 @@ public class JsonContentParserTest {
     @Test
     public void testUTF8Chars() throws Exception {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
 
-        Map<String, Object> props = getDeep(content, "jcr:content");
+        Map<String, Object> props = content.getChild("jcr:content").getProperties();
 
         assertEquals("äöü߀", props.get("utf8Property"));
     }
@@ -120,7 +120,7 @@ public class JsonContentParserTest {
     public void testParseInvalidJson() throws Exception {
         file = new File("src/test/resources/invalid-test/invalid.json");
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
         assertNull(content);
     }
 
@@ -128,7 +128,7 @@ public class JsonContentParserTest {
     public void testParseInvalidJsonWithObjectList() throws Exception {
         file = new File("src/test/resources/invalid-test/contentWithObjectList.json");
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
-        Map<String, Object> content = parse(underTest, file);
+        ContentElement content = parse(underTest, file);
         assertNull(content);
     }
 
@@ -137,18 +137,35 @@ public class JsonContentParserTest {
         ContentParser underTest = ContentParserFactory.create(ContentType.JSON,
                 new ParserOptions().ignoreResourceNames(ImmutableSet.of("header", "newslist"))
                         .ignorePropertyNames(ImmutableSet.of("jcr:title")));
-        Map<String, Object> content = parse(underTest, file);
-        Map<String, Object> props = getDeep(content, "jcr:content");
+        ContentElement content = parse(underTest, file);
+        ContentElement child = content.getChild("jcr:content");
 
-        assertEquals("Sample Homepage", props.get("pageTitle"));
-        assertNull(props.get("jcr:title"));
+        assertEquals("Sample Homepage", child.getProperties().get("pageTitle"));
+        assertNull(child.getProperties().get("jcr:title"));
 
-        assertNull(props.get("header"));
-        assertNull(props.get("newslist"));
-        assertNotNull(props.get("lead"));
+        assertNull(child.getChildren().get("header"));
+        assertNull(child.getChildren().get("newslist"));
+        assertNotNull(child.getChildren().get("lead"));
 
-        assertEquals("abc", props.get("refpro1"));
-        assertEquals("def", props.get("pathprop1"));
+        assertEquals("abc", child.getProperties().get("refpro1"));
+        assertEquals("def", child.getProperties().get("pathprop1"));
+    }
+
+    @Test
+    public void testGetChild() throws Exception {
+        ContentParser underTest = ContentParserFactory.create(ContentType.JSON);
+        ContentElement content = parse(underTest, 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);
     }
 
 }
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 f91c5ee..be395b9 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
@@ -22,10 +22,10 @@ import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.util.Map;
 
-import org.apache.commons.lang3.StringUtils;
 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;
 
 public final class TestUtils {
     
@@ -33,27 +33,12 @@ public final class TestUtils {
         // static methods only
     }
 
-    @SuppressWarnings("unchecked")
-    public static Map<String, Object> getDeep(Map<String, Object> map, String path) {
-      String name = StringUtils.substringBefore(path, "/");
-      Object object = map.get(name);
-      if (object == null || !(object instanceof Map)) {
-        return null;
-      }
-      String remainingPath = StringUtils.substringAfter(path, "/");
-      Map<String, Object> childMap = (Map<String, Object>)object;
-      if (StringUtils.isEmpty(remainingPath)) {
-        return childMap;
-      }
-      else {
-        return getDeep(childMap, remainingPath);
-      }
-    }
-    
-    public static Map<String,Object> parse(ContentParser contentParser, File file) throws IOException {
+    public static ContentElement parse(ContentParser contentParser, File file) throws IOException {
         try (FileInputStream fis = new FileInputStream(file);
                 BufferedInputStream bis = new BufferedInputStream(fis)) {
-            return contentParser.parse(bis);
+            ContentElementHandler handler = new ContentElementHandler();
+            contentParser.parse(handler, bis);
+            return handler.getRoot();
         }
     }
     
diff --git a/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/mapsupport/ContentElement.java
similarity index 50%
copy from src/main/java/org/apache/sling/jcr/contentparser/ContentType.java
copy to src/test/java/org/apache/sling/jcr/contentparser/impl/mapsupport/ContentElement.java
index 167722f..6584487 100644
--- a/src/main/java/org/apache/sling/jcr/contentparser/ContentType.java
+++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/mapsupport/ContentElement.java
@@ -16,35 +16,37 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.sling.jcr.contentparser;
+package org.apache.sling.jcr.contentparser.impl.mapsupport;
+
+import java.util.Map;
 
 /**
- * Content types.
+ * Represents a resource or node in the content hierarchy.
  */
-public enum ContentType {
+public interface ContentElement {
 
     /**
-     * JSON content.
+     * @return Resource name. The root resource has no name (null).
      */
-    JSON("json"),
-
+    String getName();
+    
     /**
-     * JCR XML content.
+     * Properties of this resource.
+     * @return Properties (keys, values)
      */
-    JCR_XML("jcr.xml");
-
-
-    private final String extension;
-
-    private ContentType(String extension) {
-        this.extension = extension;
-    }
-
+    Map<String, Object> getProperties();
+    
     /**
-     * @return Extension
+     * Get children of current resource. The Map preserves the ordering of children.
+     * @return Children (child names, child objects)
      */
-    public String getExtension() {
-        return extension;
-    }
-
+    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/jcr/contentparser/impl/mapsupport/ContentElementHandler.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/mapsupport/ContentElementHandler.java
new file mode 100644
index 0000000..190adad
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/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.jcr.contentparser.impl.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.jcr.contentparser.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/jcr/contentparser/impl/mapsupport/ContentElementImpl.java b/src/test/java/org/apache/sling/jcr/contentparser/impl/mapsupport/ContentElementImpl.java
new file mode 100644
index 0000000..3956c98
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/contentparser/impl/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.jcr.contentparser.impl.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);
+        }
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.