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/11/07 09:46:32 UTC

[sling-org-apache-sling-jcr-contentloader] 06/36: SLING-548: Rewrite of content loading from creating mock node/property objects to event based loading. This mechanism allows to create nodes and properties on the fly while reading from the input stream. (ViP) Enable tests again (they were in the wrong directory)

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

rombert pushed a commit to annotated tag org.apache.sling.jcr.contentloader-2.0.4-incubator
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-contentloader.git

commit 899bdff16a60cf4201e3c3e17c79b9c6fe653121
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Mon Jun 23 09:16:03 2008 +0000

    SLING-548: Rewrite of content loading from creating mock node/property objects to event based loading. This mechanism allows to create nodes and properties on the fly while reading from the input stream. (ViP)
    Enable tests again (they were in the wrong directory)
    
    git-svn-id: https://svn.apache.org/repos/asf/incubator/sling/trunk/jcr/contentloader@670488 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |   6 +-
 .../{NodeReader.java => ContentCreator.java}       |  25 +-
 .../jcr/contentloader/internal/ContentLoader.java  | 294 ++++++++++++++++
 .../{NodeReader.java => ContentReader.java}        |  17 +-
 .../jcr/contentloader/internal/ImportProvider.java |   2 +-
 .../jcr/contentloader/internal/JsonReader.java     |  80 +++--
 .../sling/jcr/contentloader/internal/Loader.java   | 356 +++++---------------
 .../contentloader/internal/NodeDescription.java    | 153 ---------
 .../jcr/contentloader/internal/PathEntry.java      |  50 ++-
 .../internal/PropertyDescription.java              | 119 -------
 .../jcr/contentloader/internal/XmlReader.java      | 202 ++++++++---
 .../jcr/contentloader/internal/JsonReaderTest.java | 374 ---------------------
 .../jcr/contentloader/internal/JsonReaderTest.java | 252 ++++++++++++++
 13 files changed, 913 insertions(+), 1017 deletions(-)

diff --git a/pom.xml b/pom.xml
index 16fdfc8..3705a01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -123,10 +123,14 @@
             <artifactId>kxml2</artifactId>
             <scope>provided</scope>
         </dependency>
-        
+    <!-- Testing -->        
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.jmock</groupId>
+            <artifactId>jmock-junit4</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentCreator.java
similarity index 59%
copy from src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
copy to src/main/java/org/apache/sling/jcr/contentloader/internal/ContentCreator.java
index 2653114..ade7562 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentCreator.java
@@ -18,14 +18,25 @@
  */
 package org.apache.sling.jcr.contentloader.internal;
 
-import java.io.IOException;
-import java.io.InputStream;
+import javax.jcr.RepositoryException;
 
-/**
- * The <code>NodeReader</code> TODO
- */
-interface NodeReader {
+interface ContentCreator {
+
+    void createNode(String name,
+                    String primaryNodeType,
+                    String[] mixinNodeTypes)
+    throws RepositoryException;
+
+    void finishNode()
+    throws RepositoryException;
 
-    NodeDescription parse(InputStream ins) throws IOException;
+    void createProperty(String name,
+                        int    propertyType,
+                        String value)
+    throws RepositoryException;
 
+    void createProperty(String name,
+                        int    propertyType,
+                        String[] values)
+    throws RepositoryException;
 }
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoader.java
new file mode 100644
index 0000000..3e0a629
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoader.java
@@ -0,0 +1,294 @@
+/*
+ * 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.contentloader.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+/**
+ * The <code>ContentLoader</code> creates the nodes and properties.
+ */
+public class ContentLoader implements ContentCreator {
+
+    private PathEntry configuration;
+
+    private final Stack<Node> parentNodeStack = new Stack<Node>();
+
+    /** The list of versionables. */
+    private final List<Node> versionables = new ArrayList<Node>();
+
+    /** Delayed references during content loading for the reference property. */
+    private final Map<String, List<String>> delayedReferences = new HashMap<String, List<String>>();
+
+    private String defaultRootName;
+
+    private Node rootNode;
+
+    private boolean isRootNodeImport;
+
+    /**
+     * Initialize this component.
+     * If the defaultRootName is null, we are in ROOT_NODE import mode.
+     * @param pathEntry
+     * @param parentNode
+     * @param defaultRootName
+     */
+    public void init(final PathEntry pathEntry,
+                     final Node parentNode,
+                     final String defaultRootName) {
+        this.configuration = pathEntry;
+        this.parentNodeStack.clear();
+        this.parentNodeStack.push(parentNode);
+        this.defaultRootName = defaultRootName;
+        this.rootNode = null;
+        isRootNodeImport = defaultRootName == null;
+    }
+
+    public List<Node> getVersionables() {
+        return this.versionables;
+    }
+
+    public void clear() {
+        this.versionables.clear();
+    }
+
+    public Node getRootNode() {
+        return this.rootNode;
+    }
+
+
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentCreator#createNode(java.lang.String, java.lang.String, java.lang.String[])
+     */
+    public void createNode(String name,
+                           String primaryNodeType,
+                           String[] mixinNodeTypes)
+    throws RepositoryException {
+        final Node parentNode = this.parentNodeStack.peek();
+        if ( name == null ) {
+            if ( this.parentNodeStack.size() > 1 ) {
+                throw new RepositoryException("Node needs to have a name.");
+            }
+            name = this.defaultRootName;
+        }
+
+        // if we are in root node import mode, we don't create the root top level node!
+        if ( !isRootNodeImport || this.parentNodeStack.size() > 1 ) {
+            // if node already exists but should be overwritten, delete it
+            if (this.configuration.isOverwrite() && parentNode.hasNode(name)) {
+                parentNode.getNode(name).remove();
+            }
+
+            // ensure repository node
+            Node node;
+            if (parentNode.hasNode(name)) {
+
+                // use existing node
+                node = parentNode.getNode(name);
+
+            } else if (primaryNodeType == null) {
+
+                // node explicit node type, use repository default
+                node = parentNode.addNode(name);
+
+            } else {
+
+                // explicit primary node type
+                node = parentNode.addNode(name, primaryNodeType);
+            }
+
+            // ammend mixin node types
+            if (mixinNodeTypes != null) {
+                for (final String mixin : mixinNodeTypes) {
+                    if (!node.isNodeType(mixin)) {
+                        node.addMixin(mixin);
+                    }
+                }
+            }
+
+            // check if node is versionable
+            final boolean addToVersionables = this.configuration.isCheckin()
+                                        && node.isNodeType("mix:versionable");
+            if ( addToVersionables ) {
+                this.versionables.add(node);
+            }
+
+            this.parentNodeStack.push(node);
+            if ( this.rootNode == null ) {
+                this.rootNode = node;
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentCreator#createProperty(java.lang.String, int, java.lang.String)
+     */
+    public void createProperty(String name, int propertyType, String value)
+    throws RepositoryException {
+        final Node node = this.parentNodeStack.peek();
+        // check if the property already exists, don't overwrite it in this case
+        if (node.hasProperty(name)
+            && !node.getProperty(name).isNew()) {
+            return;
+        }
+
+        if ( propertyType == PropertyType.REFERENCE ) {
+            // need to resolve the reference
+            String propPath = node.getPath() + "/" + name;
+            String uuid = getUUID(node.getSession(), propPath, value);
+            if (uuid != null) {
+                node.setProperty(name, uuid, propertyType);
+            }
+
+        } else if ("jcr:isCheckedOut".equals(name)) {
+
+            // don't try to write the property but record its state
+            // for later checkin if set to false
+            final boolean checkedout = Boolean.valueOf(value);
+            if (!checkedout) {
+                if ( !this.versionables.contains(node) ) {
+                    this.versionables.add(node);
+                }
+            }
+        } else {
+            node.setProperty(name, value, propertyType);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentCreator#createProperty(java.lang.String, int, java.lang.String[])
+     */
+    public void createProperty(String name, int propertyType, String[] values)
+    throws RepositoryException {
+        final Node node = this.parentNodeStack.peek();
+        // check if the property already exists, don't overwrite it in this case
+        if (node.hasProperty(name)
+            && !node.getProperty(name).isNew()) {
+            return;
+        }
+        node.setProperty(name, values, propertyType);
+    }
+
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentCreator#finishNode()
+     */
+    public void finishNode()
+    throws RepositoryException {
+        final Node node = this.parentNodeStack.pop();
+        // resolve REFERENCE property values pointing to this node
+        resolveReferences(node);
+    }
+
+    private String getUUID(Session session, String propPath,
+                          String referencePath)
+    throws RepositoryException {
+        if (session.itemExists(referencePath)) {
+            Item item = session.getItem(referencePath);
+            if (item.isNode()) {
+                Node refNode = (Node) item;
+                if (refNode.isNodeType("mix:referenceable")) {
+                    return refNode.getUUID();
+                }
+            }
+        } else {
+            // not existing yet, keep for delayed setting
+            List<String> current = delayedReferences.get(referencePath);
+            if (current == null) {
+                current = new ArrayList<String>();
+                delayedReferences.put(referencePath, current);
+            }
+            current.add(propPath);
+        }
+
+        // no UUID found
+        return null;
+    }
+
+    private void resolveReferences(Node node) throws RepositoryException {
+        List<String> props = delayedReferences.remove(node.getPath());
+        if (props == null || props.size() == 0) {
+            return;
+        }
+
+        // check whether we can set at all
+        if (!node.isNodeType("mix:referenceable")) {
+            return;
+        }
+
+        Session session = node.getSession();
+        String uuid = node.getUUID();
+
+        for (String property : props) {
+            String name = getName(property);
+            Node parentNode = getParentNode(session, property);
+            if (parentNode != null) {
+                parentNode.setProperty(name, uuid, PropertyType.REFERENCE);
+            }
+        }
+    }
+
+    /**
+     * Gets the name part of the <code>path</code>. The name is
+     * the part of the path after the last slash (or the complete path if no
+     * slash is contained).
+     *
+     * @param path The path from which to extract the name part.
+     * @return The name part.
+     */
+    private String getName(String path) {
+        int lastSlash = path.lastIndexOf('/');
+        String name = (lastSlash < 0) ? path : path.substring(lastSlash + 1);
+
+        return name;
+    }
+
+    private Node getParentNode(Session session, String path)
+            throws RepositoryException {
+        int lastSlash = path.lastIndexOf('/');
+
+        // not an absolute path, cannot find parent
+        if (lastSlash < 0) {
+            return null;
+        }
+
+        // node below root
+        if (lastSlash == 0) {
+            return session.getRootNode();
+        }
+
+        // item in the hierarchy
+        path = path.substring(0, lastSlash);
+        if (!session.itemExists(path)) {
+            return null;
+        }
+
+        Item item = session.getItem(path);
+        return (item.isNode()) ? (Node) item : null;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReader.java
similarity index 62%
rename from src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
rename to src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReader.java
index 2653114..b36fa7d 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentReader.java
@@ -21,11 +21,22 @@ package org.apache.sling.jcr.contentloader.internal;
 import java.io.IOException;
 import java.io.InputStream;
 
+import javax.jcr.RepositoryException;
+
 /**
- * The <code>NodeReader</code> TODO
+ * The <code>ContentReader</code>
+ * A content reader is provided by an {@link ImportProvider}.
  */
-interface NodeReader {
+interface ContentReader {
 
-    NodeDescription parse(InputStream ins) throws IOException;
+    /**
+     * Read the content from the input stream and create the
+     * content throught the provided content creator.
+     * The content reader should not close the input stream, this is
+     * done by the calling component!
+     * @param ins The input stream.
+     * @throws IOException
+     */
+    void parse(InputStream ins, ContentCreator creator) throws IOException, RepositoryException;
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java
index e28fd55..2cf8340 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java
@@ -22,6 +22,6 @@ import java.io.IOException;
 
 interface ImportProvider {
 
-    NodeReader getReader() throws IOException;
+    ContentReader getReader() throws IOException;
     
 }
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java
index ef5c4b4..26fe520 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java
@@ -26,6 +26,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
 
 import org.apache.sling.commons.json.JSONArray;
 import org.apache.sling.commons.json.JSONException;
@@ -35,7 +36,7 @@ import org.apache.sling.commons.json.JSONObject;
 /**
  * The <code>JsonReader</code> TODO
  */
-class JsonReader implements NodeReader {
+class JsonReader implements ContentReader {
 
     private static final Set<String> ignoredNames = new HashSet<String>();
     static {
@@ -52,7 +53,7 @@ class JsonReader implements NodeReader {
     static final ImportProvider PROVIDER = new ImportProvider() {
         private JsonReader jsonReader;
 
-        public NodeReader getReader() {
+        public ContentReader getReader() {
             if (jsonReader == null) {
                 jsonReader = new JsonReader();
             }
@@ -60,7 +61,11 @@ class JsonReader implements NodeReader {
         }
     };
 
-    public NodeDescription parse(InputStream ins) throws IOException {
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentReader#parse(java.io.InputStream, org.apache.sling.jcr.contentloader.internal.ContentCreator)
+     */
+    public void parse(InputStream ins, ContentCreator contentCreator)
+    throws IOException, RepositoryException {
         try {
             String jsonString = toString(ins).trim();
             if (!jsonString.startsWith("{")) {
@@ -68,92 +73,85 @@ class JsonReader implements NodeReader {
             }
 
             JSONObject json = new JSONObject(jsonString);
-            return this.createNode(null, json);
+            this.createNode(null, json, contentCreator);
 
         } catch (JSONException je) {
             throw (IOException) new IOException(je.getMessage()).initCause(je);
         }
     }
 
-    protected NodeDescription createNode(String name, JSONObject obj) throws JSONException {
-        NodeDescription node = new NodeDescription();
-        node.setName(name);
-
-        Object primaryType = obj.opt("jcr:primaryType");
-        if (primaryType != null) {
-            node.setPrimaryNodeType(String.valueOf(primaryType));
+    protected void createNode(String name, JSONObject obj, ContentCreator contentCreator)
+    throws JSONException, RepositoryException {
+        Object primaryTypeObj = obj.opt("jcr:primaryType");
+        String primaryType = null;
+        if (primaryTypeObj != null) {
+            primaryType = String.valueOf(primaryTypeObj);
         }
 
+        String[] mixinTypes = null;
         Object mixinsObject = obj.opt("jcr:mixinTypes");
         if (mixinsObject instanceof JSONArray) {
             JSONArray mixins = (JSONArray) mixinsObject;
+            mixinTypes = new String[mixins.length()];
             for (int i = 0; i < mixins.length(); i++) {
-                node.addMixinNodeType(mixins.getString(i));
+                mixinTypes[i] = mixins.getString(i);
             }
         }
 
+        contentCreator.createNode(name, primaryType, mixinTypes);
+
         // add properties and nodes
         JSONArray names = obj.names();
         for (int i = 0; names != null && i < names.length(); i++) {
-            String n = names.getString(i);
+            final String n = names.getString(i);
             // skip well known objects
             if (!ignoredNames.contains(n)) {
                 Object o = obj.get(n);
                 if (o instanceof JSONObject) {
-                    NodeDescription child = this.createNode(n, (JSONObject) o);
-                    node.addChild(child);
-                } else if (o instanceof JSONArray) {
-                    PropertyDescription prop = createProperty(n, o);
-                    node.addProperty(prop);
+                    this.createNode(n, (JSONObject) o, contentCreator);
                 } else {
-                    PropertyDescription prop = createProperty(n, o);
-                    node.addProperty(prop);
+                    this.createProperty(n, o, contentCreator);
                 }
             }
         }
-        return node;
+        contentCreator.finishNode();
     }
 
-    protected PropertyDescription createProperty(String name, Object value)
-            throws JSONException {
-        PropertyDescription property = new PropertyDescription();
-        property.setName(name);
-
+    protected void createProperty(String name, Object value, ContentCreator contentCreator)
+    throws JSONException, RepositoryException {
         // assume simple value
         if (value instanceof JSONArray) {
             // multivalue
-            JSONArray array = (JSONArray) value;
+            final JSONArray array = (JSONArray) value;
             if (array.length() > 0) {
+                final String values[] = new String[array.length()];
                 for (int i = 0; i < array.length(); i++) {
-                    property.addValue(array.get(i));
+                    values[i] = array.get(i).toString();
                 }
-                value = array.opt(0);
+                final int propertyType = getType(values[0]);
+                contentCreator.createProperty(name, propertyType, values);
             } else {
-                property.addValue(null);
-                value = null;
+                contentCreator.createProperty(name, PropertyType.STRING, new String[0]);
             }
 
         } else {
             // single value
-            property.setValue(String.valueOf(value));
+            final int propertyType = getType(value);
+            contentCreator.createProperty(name, propertyType, String.valueOf(value));
         }
-        // set type
-        property.setType(getType(value));
-
-        return property;
     }
 
-    protected String getType(Object object) {
+    protected int getType(Object object) {
         if (object instanceof Double || object instanceof Float) {
-            return PropertyType.TYPENAME_DOUBLE;
+            return PropertyType.DOUBLE;
         } else if (object instanceof Number) {
-            return PropertyType.TYPENAME_LONG;
+            return PropertyType.LONG;
         } else if (object instanceof Boolean) {
-            return PropertyType.TYPENAME_BOOLEAN;
+            return PropertyType.BOOLEAN;
         }
 
         // fall back to default
-        return PropertyType.TYPENAME_STRING;
+        return PropertyType.STRING;
     }
 
     private String toString(InputStream ins) throws IOException {
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java
index 53f9d2f..9e5efe8 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java
@@ -26,7 +26,6 @@ import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLDecoder;
-import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -40,7 +39,6 @@ import java.util.Set;
 import javax.jcr.InvalidSerializedDataException;
 import javax.jcr.Item;
 import javax.jcr.Node;
-import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 
@@ -71,14 +69,13 @@ public class Loader {
 
     private Map<String, ImportProvider> importProviders;
 
-    private Map<String, List<String>> delayedReferences;
+    private ContentLoader contentCreator = new ContentLoader();
 
     // bundles whose registration failed and should be retried
     private List<Bundle> delayedBundles;
 
     public Loader(ContentLoaderService jcrContentHelper) {
         this.jcrContentHelper = jcrContentHelper;
-        this.delayedReferences = new HashMap<String, List<String>>();
         this.delayedBundles = new LinkedList<Bundle>();
 
         importProviders = new LinkedHashMap<String, ImportProvider>();
@@ -88,7 +85,6 @@ public class Loader {
     }
 
     public void dispose() {
-        delayedReferences = null;
         if (delayedBundles != null) {
             delayedBundles.clear();
             delayedBundles = null;
@@ -99,12 +95,13 @@ public class Loader {
 
     /**
      * Register a bundle and install its content.
-     * 
+     *
      * @param session
      * @param bundle
      */
-    public void registerBundle(final Session session, final Bundle bundle,
-            final boolean isUpdate) {
+    public void registerBundle(final Session session,
+                               final Bundle bundle,
+                               final boolean isUpdate) {
 
         log.debug("Registering bundle {} for content loading.",
             bundle.getSymbolicName());
@@ -147,11 +144,11 @@ public class Loader {
         }
 
         try {
-            
+
             // check if the content has already been loaded
             final Map<String, Object> bundleContentInfo = jcrContentHelper.getBundleContentInfo(
                 session, bundle);
-            
+
             // if we don't get an info, someone else is currently loading
             if (bundleContentInfo == null) {
                 return false;
@@ -159,36 +156,36 @@ public class Loader {
 
             boolean success = false;
             try {
-                
+
                 final boolean contentAlreadyLoaded = ((Boolean) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED)).booleanValue();
-                
+
                 if (!isUpdate && contentAlreadyLoaded) {
-                    
+
                     log.info("Content of bundle already loaded {}.",
                         bundle.getSymbolicName());
-                    
+
                 } else {
-                    
+
                     installContent(session, bundle, pathIter,
                         contentAlreadyLoaded);
-                    
+
                     if (isRetry) {
                         // log success of retry
                         log.info(
                             "Retrytring to load initial content for bundle {} succeeded.",
                             bundle.getSymbolicName());
                     }
-                    
+
                 }
-                
+
                 success = true;
                 return true;
-                
+
             } finally {
                 jcrContentHelper.unlockBundleContentInfo(session, bundle,
                     success);
             }
-            
+
         } catch (RepositoryException re) {
             // if we are retrying we already logged this message once, so we
             // won't log it again
@@ -203,49 +200,46 @@ public class Loader {
 
     /**
      * Unregister a bundle. Remove installed content.
-     * 
+     *
      * @param bundle The bundle.
      */
     public void unregisterBundle(final Session session, final Bundle bundle) {
-        
+
         // check if bundle has initial content
         final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle);
         if (delayedBundles.contains(bundle)) {
-            
+
             delayedBundles.remove(bundle);
-            
+
         } else {
-            
+
             if (pathIter != null) {
                 uninstallContent(session, bundle, pathIter);
                 jcrContentHelper.contentIsUninstalled(session, bundle);
             }
-            
+
         }
     }
 
     // ---------- internal -----------------------------------------------------
 
-    private void installContent(final Session session, final Bundle bundle,
-            final Iterator<PathEntry> pathIter,
-            final boolean contentAlreadyLoaded) throws RepositoryException {
+    private void installContent(final Session session,
+                                final Bundle bundle,
+                                final Iterator<PathEntry> pathIter,
+                                final boolean contentAlreadyLoaded)
+    throws RepositoryException {
         log.debug("Installing initial content from bundle {}",
             bundle.getSymbolicName());
         try {
 
-            // the nodes marked to be checked-in after import
-            List<Node> versionables = new ArrayList<Node>();
-
             while (pathIter.hasNext()) {
                 final PathEntry entry = pathIter.next();
                 if (!contentAlreadyLoaded || entry.isOverwrite()) {
-                    
-                    Node targetNode = getTargetNode(session, entry.getTarget());
+
+                    final Node targetNode = getTargetNode(session, entry.getTarget());
 
                     if (targetNode != null) {
-                        installFromPath(bundle, entry.getPath(),
-                            entry.isOverwrite(), versionables,
-                            entry.isCheckin(), targetNode);
+                        installFromPath(bundle, entry.getPath(), entry, targetNode);
                     }
                 }
             }
@@ -254,7 +248,7 @@ public class Loader {
             session.save();
 
             // finally checkin versionable nodes
-            for (Node versionable : versionables) {
+            for (final Node versionable : this.contentCreator.getVersionables()) {
                 versionable.checkin();
             }
 
@@ -268,6 +262,7 @@ public class Loader {
                     "Failure to rollback partial initial content for bundle {}",
                     bundle.getSymbolicName(), re);
             }
+            this.contentCreator.clear();
         }
         log.debug("Done installing initial content from bundle {}",
             bundle.getSymbolicName());
@@ -276,18 +271,19 @@ public class Loader {
 
     /**
      * Handle content installation for a single path.
-     * 
+     *
      * @param bundle The bundle containing the content.
      * @param path The path
      * @param overwrite Should the content be overwritten.
      * @param parent The parent node.
      * @throws RepositoryException
      */
-    private void installFromPath(final Bundle bundle, final String path,
-            final boolean overwrite, List<Node> versionables,
-            final boolean checkin, final Node parent)
-            throws RepositoryException {
-        
+    private void installFromPath(final Bundle bundle,
+                                 final String path,
+                                 final PathEntry configuration,
+                                 final Node parent)
+    throws RepositoryException {
+
         @SuppressWarnings("unchecked")
         Enumeration<String> entries = bundle.getEntryPaths(path);
         if (entries == null) {
@@ -298,8 +294,7 @@ public class Loader {
         Map<URL, Node> processedEntries = new HashMap<URL, Node>();
 
         // potential root node import/extension
-        URL rootNodeDescriptor = importRootNode(parent.getSession(), bundle,
-            path, versionables, checkin);
+        URL rootNodeDescriptor = importRootNode(parent.getSession(), bundle, path, configuration);
         if (rootNodeDescriptor != null) {
             processedEntries.put(rootNodeDescriptor,
                 parent.getSession().getRootNode());
@@ -309,7 +304,7 @@ public class Loader {
             final String entry = entries.nextElement();
             log.debug("Processing initial content entry {}", entry);
             if (entry.endsWith("/")) {
-                
+
                 // dir, check for node descriptor , else create dir
                 String base = entry.substring(0, entry.length() - 1);
                 String name = getName(base);
@@ -330,21 +325,20 @@ public class Loader {
                     node = processedEntries.get(nodeDescriptor);
                     if (node == null) {
                         node = createNode(parent, name, nodeDescriptor,
-                            overwrite, versionables, checkin);
+                            configuration);
                         processedEntries.put(nodeDescriptor, node);
                     }
                 } else {
-                    node = createFolder(parent, name, overwrite);
+                    node = createFolder(parent, name, configuration.isOverwrite());
                 }
 
                 // walk down the line
                 if (node != null) {
-                    installFromPath(bundle, entry, overwrite, versionables,
-                        checkin, node);
+                    installFromPath(bundle, entry, configuration, node);
                 }
 
             } else {
-                
+
                 // file => create file
                 URL file = bundle.getEntry(entry);
                 if (processedEntries.containsKey(file)) {
@@ -363,8 +357,7 @@ public class Loader {
                 }
                 if (foundProvider) {
                     Node node = null;
-                    if ((node = createNode(parent, getName(entry), file, overwrite,
-                        versionables, checkin)) != null) {
+                    if ((node = createNode(parent, getName(entry), file, configuration)) != null) {
                         processedEntries.put(file, node);
                         continue;
                     }
@@ -380,20 +373,34 @@ public class Loader {
         }
     }
 
-    private Node createNode(Node parent, String name, URL nodeXML,
-            boolean overwrite, List<Node> versionables, boolean checkin)
-            throws RepositoryException {
-
+    /**
+     * Create a new node from a content resource found in the bundle.
+     * @param parent The parent node
+     * @param name   The name of the new content node
+     * @param resourceUrl The resource url.
+     * @param overwrite Should the content be overwritten?
+     * @param versionables
+     * @param checkin
+     * @return
+     * @throws RepositoryException
+     */
+    private Node createNode(Node parent,
+                            String name,
+                            URL resourceUrl,
+                            PathEntry configuration)
+    throws RepositoryException {
+        final String resourcePath = resourceUrl.getPath().toLowerCase();
         InputStream ins = null;
         try {
             // special treatment for system view imports
-            if (nodeXML.getPath().toLowerCase().endsWith(EXT_JCR_XML)) {
-                return importSystemView(parent, name, nodeXML);
+            if (resourcePath.endsWith(EXT_JCR_XML)) {
+                return importSystemView(parent, name, resourceUrl);
             }
 
-            NodeReader nodeReader = null;
+            // get the node reader for this resource
+            ContentReader nodeReader = null;
             for (Map.Entry<String, ImportProvider> e : importProviders.entrySet()) {
-                if (nodeXML.getPath().toLowerCase().endsWith(e.getKey())) {
+                if (resourcePath.endsWith(e.getKey())) {
                     nodeReader = e.getValue().getReader();
                     break;
                 }
@@ -404,20 +411,11 @@ public class Loader {
                 return null;
             }
 
-            ins = nodeXML.openStream();
-            NodeDescription clNode = nodeReader.parse(ins);
+            this.contentCreator.init(configuration, parent, toPlainName(name));
+            ins = resourceUrl.openStream();
+            nodeReader.parse(ins, this.contentCreator);
 
-            // nothing has been parsed
-            if (clNode == null) {
-                return null;
-            }
-
-            if (clNode.getName() == null) {
-                // set the name without the [last] extension (xml or json)
-                clNode.setName(toPlainName(name));
-            }
-
-            return createNode(parent, clNode, overwrite, versionables, checkin);
+            return this.contentCreator.getRootNode();
         } catch (RepositoryException re) {
             throw re;
         } catch (Throwable t) {
@@ -434,7 +432,7 @@ public class Loader {
 
     /**
      * Delete the node from the initial content.
-     * 
+     *
      * @param parent
      * @param name
      * @param nodeXML
@@ -447,118 +445,9 @@ public class Loader {
         }
     }
 
-    private Node createNode(Node parentNode, NodeDescription clNode,
-            final boolean overwrite, List<Node> versionables, boolean checkin)
-            throws RepositoryException {
-        
-        // if node already exists but should be overwritten, delete it
-        if (overwrite && parentNode.hasNode(clNode.getName())) {
-            parentNode.getNode(clNode.getName()).remove();
-        }
-
-        // ensure repository node
-        Node node;
-        if (parentNode.hasNode(clNode.getName())) {
-
-            // use existing node
-            node = parentNode.getNode(clNode.getName());
-
-        } else if (clNode.getPrimaryNodeType() == null) {
-
-            // node explicit node type, use repository default
-            node = parentNode.addNode(clNode.getName());
-
-        } else {
-
-            // explicit primary node type
-            node = parentNode.addNode(clNode.getName(),
-                clNode.getPrimaryNodeType());
-        }
-
-        return setupNode(node, clNode, versionables, checkin);
-    }
-
-    private Node setupNode(Node node, NodeDescription clNode,
-            List<Node> versionables, boolean checkin)
-            throws RepositoryException {
-
-        // ammend mixin node types
-        if (clNode.getMixinNodeTypes() != null) {
-            for (String mixin : clNode.getMixinNodeTypes()) {
-                if (!node.isNodeType(mixin)) {
-                    node.addMixin(mixin);
-                }
-            }
-        }
-
-        // check if node is versionable
-        boolean addToVersionables = checkin
-            && node.isNodeType("mix:versionable");
-
-        if (clNode.getProperties() != null) {
-            for (PropertyDescription prop : clNode.getProperties()) {
-                if (node.hasProperty(prop.getName())
-                    && !node.getProperty(prop.getName()).isNew()) {
-                    continue;
-                }
-
-                int type = PropertyType.valueFromName(prop.getType());
-                if (prop.isMultiValue()) {
-
-                    String[] values = prop.getValues().toArray(
-                        new String[prop.getValues().size()]);
-                    node.setProperty(prop.getName(), values, type);
-
-                } else if (type == PropertyType.REFERENCE) {
-
-                    // need to resolve the reference
-                    String propPath = node.getPath() + "/" + prop.getName();
-                    String uuid = getUUID(node.getSession(), propPath,
-                        prop.getValue());
-                    if (uuid != null) {
-                        node.setProperty(prop.getName(), uuid, type);
-                    }
-
-                } else if ("jcr:isCheckedOut".equals(prop.getName())) {
-
-                    // don't try to write the property but record its state
-                    // for later checkin if set to false
-                    boolean checkedout = Boolean.valueOf(prop.getValue());
-                    if (!checkedout) {
-                        addToVersionables = true;
-                    }
-
-                } else {
-
-                    node.setProperty(prop.getName(), prop.getValue(), type);
-
-                }
-            }
-        }
-
-        // add the current node to the list of versionables to be checked
-        // in at the end. This is done if checkin is true and the node is
-        // versionable or if the jcr:isCheckedOut property is false
-        if (addToVersionables) {
-            versionables.add(node);
-        }
-
-        // create child nodes from the descriptor
-        if (clNode.getChildren() != null) {
-            for (NodeDescription child : clNode.getChildren()) {
-                createNode(node, child, false, versionables, checkin);
-            }
-        }
-
-        // resolve REFERENCE property values pointing to this node
-        resolveReferences(node);
-
-        return node;
-    }
-
     /**
      * Create a folder
-     * 
+     *
      * @param parent The parent node.
      * @param name The name of the folder
      * @param overwrite If set to true, an existing folder is removed first.
@@ -580,7 +469,7 @@ public class Loader {
 
     /**
      * Create a file from the given url.
-     * 
+     *
      * @param parent
      * @param source
      * @throws IOException
@@ -623,7 +512,7 @@ public class Loader {
 
     /**
      * Delete the file from the given url.
-     * 
+     *
      * @param parent
      * @param source
      * @throws IOException
@@ -637,53 +526,6 @@ public class Loader {
         }
     }
 
-    private String getUUID(Session session, String propPath,
-            String referencePath) throws RepositoryException {
-        if (session.itemExists(referencePath)) {
-            Item item = session.getItem(referencePath);
-            if (item.isNode()) {
-                Node refNode = (Node) item;
-                if (refNode.isNodeType("mix:referenceable")) {
-                    return refNode.getUUID();
-                }
-            }
-        } else {
-            // not existing yet, keep for delayed setting
-            List<String> current = delayedReferences.get(referencePath);
-            if (current == null) {
-                current = new ArrayList<String>();
-                delayedReferences.put(referencePath, current);
-            }
-            current.add(propPath);
-        }
-
-        // no UUID found
-        return null;
-    }
-
-    private void resolveReferences(Node node) throws RepositoryException {
-        List<String> props = delayedReferences.remove(node.getPath());
-        if (props == null || props.size() == 0) {
-            return;
-        }
-
-        // check whether we can set at all
-        if (!node.isNodeType("mix:referenceable")) {
-            return;
-        }
-
-        Session session = node.getSession();
-        String uuid = node.getUUID();
-
-        for (String property : props) {
-            String name = getName(property);
-            Node parentNode = getParentNode(session, property);
-            if (parentNode != null) {
-                parentNode.setProperty(name, uuid, PropertyType.REFERENCE);
-            }
-        }
-    }
-
     /**
      * Gets and decods the name part of the <code>path</code>. The name is
      * the part of the path after the last slash (or the complete path if no
@@ -693,7 +535,7 @@ public class Loader {
      * encoding. In this case, this method decodes the name using the
      * <code>java.netURLDecoder</code> class with the <i>UTF-8</i> character
      * encoding.
-     * 
+     *
      * @param path The path from which to extract the name part.
      * @return The URL decoded name part.
      */
@@ -721,30 +563,6 @@ public class Loader {
         return name;
     }
 
-    private Node getParentNode(Session session, String path)
-            throws RepositoryException {
-        int lastSlash = path.lastIndexOf('/');
-
-        // not an absolute path, cannot find parent
-        if (lastSlash < 0) {
-            return null;
-        }
-
-        // node below root
-        if (lastSlash == 0) {
-            return session.getRootNode();
-        }
-
-        // item in the hierarchy
-        path = path.substring(0, lastSlash);
-        if (!session.itemExists(path)) {
-            return null;
-        }
-
-        Item item = session.getItem(path);
-        return (item.isNode()) ? (Node) item : null;
-    }
-
     private Node getTargetNode(Session session, String path)
             throws RepositoryException {
 
@@ -800,7 +618,7 @@ public class Loader {
 
     /**
      * Handle content uninstallation for a single path.
-     * 
+     *
      * @param bundle The bundle containing the content.
      * @param path The path
      * @param parent The parent node.
@@ -897,7 +715,7 @@ public class Loader {
      * Import the XML file as JCR system or document view import. If the XML
      * file is not a valid system or document view export/import file,
      * <code>false</code> is returned.
-     * 
+     *
      * @param parent The parent node below which to import
      * @param nodeXML The URL to the XML file to import
      * @return <code>true</code> if the import succeeds, <code>false</code>
@@ -958,14 +776,14 @@ public class Loader {
     protected static final class Descriptor {
         public URL rootNodeDescriptor;
 
-        public NodeReader nodeReader;
+        public ContentReader nodeReader;
     }
 
     /**
      * Return the root node descriptor.
      */
     private Descriptor getRootNodeDescriptor(final Bundle bundle,
-            final String path) {
+                                             final String path) {
         URL rootNodeDescriptor = null;
 
         for (Map.Entry<String, ImportProvider> e : importProviders.entrySet()) {
@@ -993,9 +811,8 @@ public class Loader {
      * Imports mixin nodes and properties (and optionally child nodes) of the
      * root node.
      */
-    private URL importRootNode(Session session, Bundle bundle, String path,
-            List<Node> versionables, boolean checkin)
-            throws RepositoryException {
+    private URL importRootNode(Session session, Bundle bundle, String path, PathEntry configuration)
+    throws RepositoryException {
         final Descriptor descriptor = getRootNodeDescriptor(bundle, path);
         // no root descriptor found
         if (descriptor == null) {
@@ -1006,9 +823,8 @@ public class Loader {
         try {
 
             ins = descriptor.rootNodeDescriptor.openStream();
-            NodeDescription clNode = descriptor.nodeReader.parse(ins);
-
-            setupNode(session.getRootNode(), clNode, versionables, checkin);
+            this.contentCreator.init(configuration, session.getRootNode(), null);
+            descriptor.nodeReader.parse(ins, this.contentCreator);
 
             return descriptor.rootNodeDescriptor;
         } catch (RepositoryException re) {
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java
deleted file mode 100644
index 043da37..0000000
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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.contentloader.internal;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class NodeDescription {
-
-    private String name;
-    private String primaryNodeType;
-    private Set<String> mixinNodeTypes;
-    private List<PropertyDescription> properties;
-    private List<NodeDescription> children;
-
-    /**
-     * @return the children
-     */
-    List<NodeDescription> getChildren() {
-        return children;
-    }
-
-    /**
-     * @param children the children to set
-     */
-    void addChild(NodeDescription child) {
-        if (child != null) {
-            if (children == null) {
-                children = new ArrayList<NodeDescription>();
-            }
-
-            children.add(child);
-        }
-    }
-
-    /**
-     * @return the mixinNodeTypes
-     */
-    Set<String> getMixinNodeTypes() {
-        return mixinNodeTypes;
-    }
-
-    /**
-     * @param mixinNodeTypes the mixinNodeTypes to set
-     */
-    void addMixinNodeType(String mixinNodeType) {
-        if (mixinNodeType != null && mixinNodeType.length() > 0) {
-            if (mixinNodeTypes == null) {
-                mixinNodeTypes = new HashSet<String>();
-            }
-
-            mixinNodeTypes.add(mixinNodeType);
-        }
-    }
-
-    /**
-     * @return the name
-     */
-    String getName() {
-        return name;
-    }
-
-    /**
-     * @param name the name to set
-     */
-    void setName(String name) {
-        this.name = name;
-    }
-
-    /**
-     * @return the primaryNodeType
-     */
-    String getPrimaryNodeType() {
-        return primaryNodeType;
-    }
-
-    /**
-     * @param primaryNodeType the primaryNodeType to set
-     */
-    void setPrimaryNodeType(String primaryNodeType) {
-        this.primaryNodeType = primaryNodeType;
-    }
-
-    /**
-     * @return the properties
-     */
-    List<PropertyDescription> getProperties() {
-        return properties;
-    }
-
-    /**
-     * @param properties the properties to set
-     */
-    void addProperty(PropertyDescription property) {
-        if (property != null) {
-            if (properties == null) {
-                properties = new ArrayList<PropertyDescription>();
-            }
-
-            properties.add(property);
-        }
-    }
-
-    public int hashCode() {
-        int code = getName().hashCode() * 17;
-        if (getPrimaryNodeType() != null) {
-            code += getPrimaryNodeType().hashCode();
-        }
-        return code;
-    }
-
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        } else if (!(obj instanceof NodeDescription)) {
-            return false;
-        }
-
-        NodeDescription other = (NodeDescription) obj;
-        return getName().equals(other.getName())
-            && equals(getPrimaryNodeType(), other.getPrimaryNodeType())
-            && equals(getMixinNodeTypes(), other.getMixinNodeTypes())
-            && equals(getProperties(), other.getProperties())
-            && equals(getChildren(), other.getChildren());
-    }
-
-    public String toString() {
-        return "Node " + getName() + ", primary=" + getPrimaryNodeType()
-            + ", mixins=" + getMixinNodeTypes();
-    }
-
-    private boolean equals(Object o1, Object o2) {
-        return (o1 == null) ? o2 == null : o1.equals(o2);
-    }
-}
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
index cfb8c24..937462b 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
@@ -54,6 +54,14 @@ public class PathEntry {
      */
     public static final String CHECKIN_DIRECTIVE = "checkin";
 
+    /**
+     * The expand directive specifying whether the available {@link ImportProvider}s
+     * should be used during content loading. This is a boolean value that
+     * defaults to true.
+     * @since 2.0.4
+     */
+    public static final String EXPAND_DIRECTIVE = "expand";
+
     /** The path for the initial content. */
     private final String path;
 
@@ -62,10 +70,13 @@ public class PathEntry {
 
     /** Should existing content be uninstalled? */
     private final boolean uninstall;
-    
+
     /** Should versionable nodes be checked in? */
     private final boolean checkin;
 
+    /** Should archives be expanded? @since 2.0.4 */
+    private final boolean expand;
+
     /**
      * Target path where initial content will be loaded. If it´s null then
      * target node is the root node
@@ -90,32 +101,49 @@ public class PathEntry {
     }
 
     public PathEntry(ManifestHeader.Entry entry) {
+        this.path = entry.getValue();
+
         // check for directives
+
+        // overwrite directive
         final String overwriteValue = entry.getDirectiveValue(OVERWRITE_DIRECTIVE);
-        final String uninstallValue = entry.getDirectiveValue(UNINSTALL_DIRECTIVE);
-        final String pathValue = entry.getDirectiveValue(PATH_DIRECTIVE);
-        final String checkinValue = entry.getDirectiveValue(CHECKIN_DIRECTIVE);
-        boolean overwriteFlag = false;
         if (overwriteValue != null) {
-            overwriteFlag = Boolean.valueOf(overwriteValue);
+            this.overwrite = Boolean.valueOf(overwriteValue);
+        } else {
+            this.overwrite = false;
         }
-        this.path = entry.getValue();
-        this.overwrite = overwriteFlag;
+
+        // uninstall directive
+        final String uninstallValue = entry.getDirectiveValue(UNINSTALL_DIRECTIVE);
         if (uninstallValue != null) {
             this.uninstall = Boolean.valueOf(uninstallValue);
         } else {
             this.uninstall = this.overwrite;
         }
+
+        // path directive
+        final String pathValue = entry.getDirectiveValue(PATH_DIRECTIVE);
         if (pathValue != null) {
             this.target = pathValue;
         } else {
             this.target = null;
         }
+
+        // checkin directive
+        final String checkinValue = entry.getDirectiveValue(CHECKIN_DIRECTIVE);
         if (checkinValue != null) {
             this.checkin = Boolean.valueOf(checkinValue);
         } else {
             this.checkin = false;
         }
+
+        // expand directive
+        final String expandValue = entry.getDirectiveValue(EXPAND_DIRECTIVE);
+        if ( expandValue != null ) {
+            this.expand = Boolean.valueOf(expandValue);
+        } else {
+            this.expand = true;
+        }
     }
 
     public String getPath() {
@@ -133,7 +161,11 @@ public class PathEntry {
     public boolean isCheckin() {
         return this.checkin;
     }
-    
+
+    public boolean isExpand() {
+        return this.expand;
+    }
+
     public String getTarget() {
         return target;
     }
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java
deleted file mode 100644
index 4fd7823..0000000
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.contentloader.internal;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.jcr.PropertyType;
-
-class PropertyDescription {
-    private String name;
-    private String value;
-    private List<String> values;
-    private String type = PropertyType.TYPENAME_STRING; // default type to string
-
-    /**
-     * @return the name
-     */
-    String getName() {
-        return this.name;
-    }
-
-    /**
-     * @param name the name to set
-     */
-    void setName(String name) {
-        this.name = name;
-    }
-
-    /**
-     * @return the type
-     */
-    String getType() {
-        return this.type;
-    }
-
-    /**
-     * @param type the type to set
-     */
-    void setType(String type) {
-        this.type = type;
-    }
-
-    /**
-     * @return the value
-     */
-    String getValue() {
-        return this.value;
-    }
-
-    /**
-     * @param value the value to set
-     */
-    void setValue(String value) {
-        this.value = value;
-    }
-
-    /**
-     * @return the values
-     */
-    List<String> getValues() {
-        return this.values;
-    }
-
-    /**
-     * @param values the values to set
-     */
-    void addValue(Object value) {
-        if (this.values == null) {
-            this.values = new ArrayList<String>();
-        }
-
-        if (value != null) {
-            this.values.add(value.toString());
-        }
-    }
-
-    boolean isMultiValue() {
-        return this.values != null;
-    }
-
-    public int hashCode() {
-        return this.getName().hashCode() * 17 + this.getType().hashCode();
-    }
-
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        } else if (!(obj instanceof PropertyDescription)) {
-            return false;
-        }
-
-        PropertyDescription other = (PropertyDescription) obj;
-        return this.getName().equals(other.getName())
-            && this.getType().equals(other.getType())
-            && this.equals(this.getValues(), other.getValues())
-            && this.equals(this.getValue(), other.getValue());
-    }
-
-    private boolean equals(Object o1, Object o2) {
-        return (o1 == null) ? o2 == null : o1.equals(o2);
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java
index a96155e..98dd4a6 100644
--- a/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java
@@ -20,13 +20,47 @@ package org.apache.sling.jcr.contentloader.internal;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
 
 import org.kxml2.io.KXmlParser;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-public class XmlReader implements NodeReader {
+/**
+ * This reader reads an xml file defining the content.
+ * The xml format should have this format:
+ * <node>
+ *   <name>the name of the node</name>
+ *   <primaryNodeType>type</primaryNodeType>
+ *   <mixinNodeTypes>
+ *     <mixinNodeType>mixtype1</mixinNodeType>
+ *     <mixinNodeType>mixtype2</mixinNodeType>
+ *   </mixingNodeTypes>
+ *   <properties>
+ *     <property>
+ *       <name>propName</name>
+ *       <value>propValue</value>
+ *           or
+ *       <values>
+ *         <value/> for multi value properties
+ *       </values>
+ *       <type>propType</type>
+ *     </property>
+ *     <!-- more properties -->
+ *   </properties>
+ *   <nodes>
+ *     <!-- child nodes -->
+ *     <node>
+ *       ..
+ *     </node>
+ *   </nodes>
+ * </node>
+ */
+public class XmlReader implements ContentReader {
 
     /*
      * <node> <primaryNodeType>type</primaryNodeType> <mixinNodeTypes>
@@ -55,7 +89,7 @@ public class XmlReader implements NodeReader {
     static final ImportProvider PROVIDER = new ImportProvider() {
         private XmlReader xmlReader;
 
-        public NodeReader getReader() throws IOException {
+        public ContentReader getReader() throws IOException {
             if (xmlReader == null) {
                 try {
                     xmlReader = new XmlReader();
@@ -75,39 +109,46 @@ public class XmlReader implements NodeReader {
 
     // ---------- XML content access -------------------------------------------
 
-    public synchronized NodeDescription parse(InputStream ins) throws IOException {
+
+    /**
+     * @see org.apache.sling.jcr.contentloader.internal.ContentReader#parse(java.io.InputStream, org.apache.sling.jcr.contentloader.internal.ContentCreator)
+     */
+    public synchronized void parse(InputStream ins, ContentCreator creator)
+    throws IOException, RepositoryException {
         try {
-            return this.parseInternal(ins);
+            this.parseInternal(ins, creator);
         } catch (XmlPullParserException xppe) {
             throw (IOException) new IOException(xppe.getMessage()).initCause(xppe);
         }
     }
 
-    private NodeDescription parseInternal(InputStream ins) throws IOException,
-            XmlPullParserException {
-        String currentElement = "<root>";
-        LinkedList<String> elements = new LinkedList<String>();
-        NodeDescription currentNode = null;
-        LinkedList<NodeDescription> nodes = new LinkedList<NodeDescription>();
-        StringBuffer contentBuffer = new StringBuffer();
-        PropertyDescription currentProperty = null;
+    private void parseInternal(InputStream ins, ContentCreator creator)
+    throws IOException, XmlPullParserException, RepositoryException {
+        final StringBuffer contentBuffer = new StringBuffer();
 
         // set the parser input, use null encoding to force detection with
         // <?xml?>
         this.xmlParser.setInput(ins, null);
 
+        NodeDescription.SHARED.clear();
+        PropertyDescription.SHARED.clear();
+
+        NodeDescription currentNode = null;
+        PropertyDescription currentProperty = null;
+        String currentElement;
+
         int eventType = this.xmlParser.getEventType();
         while (eventType != XmlPullParser.END_DOCUMENT) {
             if (eventType == XmlPullParser.START_TAG) {
 
-                elements.add(currentElement);
                 currentElement = this.xmlParser.getName();
 
                 if (ELEM_PROPERTY.equals(currentElement)) {
-                    currentProperty = new PropertyDescription();
+                    currentNode = NodeDescription.create(currentNode, creator);
+                    currentProperty = PropertyDescription.SHARED;
                 } else if (ELEM_NODE.equals(currentElement)) {
-                    if (currentNode != null) nodes.add(currentNode);
-                    currentNode = new NodeDescription();
+                    currentNode = NodeDescription.create(currentNode, creator);
+                    currentNode = NodeDescription.SHARED;
                 }
 
             } else if (eventType == XmlPullParser.END_TAG) {
@@ -117,53 +158,136 @@ public class XmlReader implements NodeReader {
                 contentBuffer.delete(0, contentBuffer.length());
 
                 if (ELEM_PROPERTY.equals(qName)) {
-                    currentNode.addProperty(currentProperty);
-                    currentProperty = null;
+                    currentProperty = PropertyDescription.create(currentProperty, creator);
 
                 } else if (ELEM_NAME.equals(qName)) {
                     if (currentProperty != null) {
-                        currentProperty.setName(content);
+                        currentProperty.name = content;
                     } else if (currentNode != null) {
-                        currentNode.setName(content);
+                        currentNode.name = content;
                     }
 
                 } else if (ELEM_VALUE.equals(qName)) {
-                    if (currentProperty.isMultiValue()) {
-                        currentProperty.addValue(content);
-                    } else {
-                        currentProperty.setValue(content);
-                    }
+                    currentProperty.addValue(content);
 
                 } else if (ELEM_VALUES.equals(qName)) {
-                    currentProperty.addValue(null);
-                    currentProperty.setValue(null);
+                    currentProperty.isMultiValue = true;
 
                 } else if (ELEM_TYPE.equals(qName)) {
-                    currentProperty.setType(content);
+                    currentProperty.type = content;
 
                 } else if (ELEM_NODE.equals(qName)) {
-                    if (!nodes.isEmpty()) {
-                        NodeDescription parent = nodes.removeLast();
-                        parent.addChild(currentNode);
-                        currentNode = parent;
-                    }
+                    currentNode = NodeDescription.create(currentNode, creator);
+                    creator.finishNode();
 
                 } else if (ELEM_PRIMARY_NODE_TYPE.equals(qName)) {
-                    currentNode.setPrimaryNodeType(content);
+                    if ( currentNode == null ) {
+                        throw new IOException("Element is not allowed at this location: " + qName);
+                    }
+                    currentNode.primaryNodeType = content;
 
                 } else if (ELEM_MIXIN_NODE_TYPE.equals(qName)) {
-                    currentNode.addMixinNodeType(content);
+                    if ( currentNode == null ) {
+                        throw new IOException("Element is not allowed at this location: " + qName);
+                    }
+                    currentNode.addMixinType(content);
                 }
 
-                currentElement = elements.removeLast();
-
             } else if (eventType == XmlPullParser.TEXT) {
                 contentBuffer.append(this.xmlParser.getText());
             }
 
             eventType = this.xmlParser.next();
         }
+    }
+
+    protected static final class NodeDescription {
+
+        public static NodeDescription SHARED = new NodeDescription();
+
+        public String name;
+        public String primaryNodeType;
+        public List<String> mixinTypes;
+
+        public static NodeDescription create(NodeDescription desc, ContentCreator creator)
+        throws RepositoryException {
+            if ( desc != null ) {
+                creator.createNode(desc.name, desc.primaryNodeType, desc.getMixinTypes());
+                desc.clear();
+            }
+            return null;
+        }
+
+        public void addMixinType(String v) {
+            if ( this.mixinTypes == null ) {
+                this.mixinTypes = new ArrayList<String>();
+            }
+            this.mixinTypes.add(v);
+        }
+
+
+        private String[] getMixinTypes() {
+            if ( this.mixinTypes == null || this.mixinTypes.size() == 0) {
+                return null;
+            }
+            return mixinTypes.toArray(new String[this.mixinTypes.size()]);
+        }
+
+        private void clear() {
+            this.name = null;
+            this.primaryNodeType = null;
+            if ( this.mixinTypes != null ) {
+                this.mixinTypes.clear();
+            }
+        }
+    }
+
+    protected static final class PropertyDescription {
+
+        public static PropertyDescription SHARED = new PropertyDescription();
+
+        public static PropertyDescription create(PropertyDescription desc, ContentCreator creator)
+        throws RepositoryException {
+            int type = (desc.type == null ? PropertyType.STRING : PropertyType.valueFromName(desc.type));
+            if ( desc.isMultiValue ) {
+                creator.createProperty(desc.name, type, desc.getPropertyValues());
+            } else {
+                String value = null;
+                if ( desc.values != null && desc.values.size() == 1 ) {
+                    value = desc.values.get(0);
+                }
+                creator.createProperty(desc.name, type, value);
+            }
+            desc.clear();
+            return null;
+        }
+
+        public String name;
+        public String type;
+        public List<String> values;
+        public boolean isMultiValue;
+
+        public void addValue(String v) {
+            if ( this.values == null ) {
+                this.values = new ArrayList<String>();
+            }
+            this.values.add(v);
+        }
+
+        private String[] getPropertyValues() {
+            if ( this.values == null || this.values.size() == 0) {
+                return null;
+            }
+            return values.toArray(new String[this.values.size()]);
+        }
 
-        return currentNode;
+        private void clear() {
+            this.name = null;
+            this.type = null;
+            if ( this.values != null ) {
+                this.values.clear();
+            }
+            this.isMultiValue = false;
+        }
     }
 }
diff --git a/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java b/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
deleted file mode 100644
index a975d3e..0000000
--- a/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * 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.contentloader.internal;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.jcr.PropertyType;
-
-import junit.framework.TestCase;
-
-import org.apache.sling.commons.json.JSONArray;
-import org.apache.sling.commons.json.JSONException;
-import org.apache.sling.commons.json.JSONObject;
-import org.apache.sling.jcr.contentloader.internal.JsonReader;
-import org.apache.sling.jcr.contentloader.internal.NodeDescription;
-import org.apache.sling.jcr.contentloader.internal.PropertyDescription;
-
-public class JsonReaderTest extends TestCase {
-
-    JsonReader jsonReader;
-
-    protected void setUp() throws Exception {
-        super.setUp();
-        this.jsonReader = new JsonReader();
-    }
-
-    protected void tearDown() throws Exception {
-        this.jsonReader = null;
-        super.tearDown();
-    }
-
-    public void testEmptyObject() {
-        try {
-            this.parse("");
-        } catch (IOException ioe) {
-            fail("Expected IOException from empty JSON");
-        }
-    }
-
-    public void testEmpty() throws IOException {
-        NodeDescription node = this.parse("{}");
-        assertNotNull("Expecting node", node);
-        assertNull("No name expected", node.getName());
-    }
-
-    public void testDefaultPrimaryNodeType() throws IOException {
-        String json = "{}";
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertNull(node.getPrimaryNodeType());
-        assertNull("No mixins expected", node.getMixinNodeTypes());
-        assertNull("No properties expected", node.getProperties());
-        assertNull("No children expected", node.getChildren());
-    }
-
-    public void testDefaultPrimaryNodeTypeWithSurroundWhitespace() throws IOException {
-        String json = "     {  }     ";
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertNull(node.getPrimaryNodeType());
-        assertNull("No mixins expected", node.getMixinNodeTypes());
-        assertNull("No properties expected", node.getProperties());
-        assertNull("No children expected", node.getChildren());
-    }
-
-    public void testDefaultPrimaryNodeTypeWithoutEnclosingBraces() throws IOException {
-        String json = "";
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertNull(node.getPrimaryNodeType());
-        assertNull("No mixins expected", node.getMixinNodeTypes());
-        assertNull("No properties expected", node.getProperties());
-        assertNull("No children expected", node.getChildren());
-    }
-
-    public void testDefaultPrimaryNodeTypeWithoutEnclosingBracesWithSurroundWhitespace() throws IOException {
-        String json = "             ";
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertNull(node.getPrimaryNodeType());
-        assertNull("No mixins expected", node.getMixinNodeTypes());
-        assertNull("No properties expected", node.getProperties());
-        assertNull("No children expected", node.getChildren());
-    }
-
-    public void testExplicitePrimaryNodeType() throws IOException {
-        String type = "xyz:testType";
-        String json = "{ \"jcr:primaryType\": \"" + type + "\" }";
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(type, node.getPrimaryNodeType());
-    }
-
-    public void testMixinNodeTypes1() throws JSONException, IOException {
-        Set<Object> mixins = this.toSet(new Object[]{ "xyz:mix1" });
-        String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}";
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(mixins, node.getMixinNodeTypes());
-    }
-
-    public void testMixinNodeTypes2() throws JSONException, IOException {
-        Set<Object> mixins = this.toSet(new Object[]{ "xyz:mix1", "abc:mix2" });
-        String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}";
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(mixins, node.getMixinNodeTypes());
-    }
-
-    public void testPropertiesNone() throws IOException, JSONException {
-        List<PropertyDescription> properties = null;
-        String json = "{ \"properties\": " + this.toJsonObject(properties) + "}";
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(properties, node.getProperties());
-    }
-
-    public void testPropertiesSingleValue() throws IOException, JSONException {
-        List<PropertyDescription> properties = new ArrayList<PropertyDescription>();
-        PropertyDescription prop = new PropertyDescription();
-        prop.setName("p1");
-        prop.setValue("v1");
-        properties.add(prop);
-        
-        String json = this.toJsonObject(properties).toString();
-        
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties()));
-    }
-    
-    public void testPropertiesTwoSingleValue() throws IOException, JSONException {
-        List<PropertyDescription> properties = new ArrayList<PropertyDescription>();
-        PropertyDescription prop = new PropertyDescription();
-        prop.setName("p1");
-        prop.setValue("v1");
-        properties.add(prop);
-        prop = new PropertyDescription();
-        prop.setName("p2");
-        prop.setValue("v2");
-        properties.add(prop);
-
-        String json = this.toJsonObject(properties).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties()));
-    }
-
-    public void testPropertiesMultiValue() throws IOException, JSONException {
-        List<PropertyDescription> properties = new ArrayList<PropertyDescription>();
-        PropertyDescription prop = new PropertyDescription();
-        prop.setName("p1");
-        prop.addValue("v1");
-        properties.add(prop);
-
-        String json = this.toJsonObject(properties).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties()));
-    }
-
-    public void testPropertiesMultiValueEmpty() throws IOException, JSONException {
-        List<PropertyDescription> properties = new ArrayList<PropertyDescription>();
-        PropertyDescription prop = new PropertyDescription();
-        prop.setName("p1");
-        prop.addValue(null); // empty multivalue property
-        properties.add(prop);
-
-        String json = this.toJsonObject(properties).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties()));
-    }
-
-    public void testChildrenNone() throws IOException, JSONException {
-        List<NodeDescription> nodes = null;
-        String json = this.toJsonObject(nodes).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(nodes, node.getChildren());
-    }
-
-    public void testChild() throws IOException, JSONException {
-        List<NodeDescription> nodes = new ArrayList<NodeDescription>();
-        NodeDescription child = new NodeDescription();
-        child.setName("p1");
-        nodes.add(child);
-
-        String json = this.toJsonObject(nodes).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(nodes, node.getChildren());
-    }
-
-    public void testChildWithMixin() throws IOException, JSONException {
-        List<NodeDescription> nodes = new ArrayList<NodeDescription>();
-        NodeDescription child = new NodeDescription();
-        child.setName("p1");
-        child.addMixinNodeType("p1:mix");
-        nodes.add(child);
-
-        String json = this.toJsonObject(nodes).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(nodes, node.getChildren());
-    }
-
-    public void testTwoChildren() throws IOException, JSONException {
-        List<NodeDescription> nodes = new ArrayList<NodeDescription>();
-        NodeDescription child = new NodeDescription();
-        child.setName("p1");
-        nodes.add(child);
-        child = new NodeDescription();
-        child.setName("p2");
-        nodes.add(child);
-
-        String json = this.toJsonObject(nodes).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(nodes, node.getChildren());
-    }
-
-    public void testChildWithProperty() throws IOException, JSONException {
-        List<NodeDescription> nodes = new ArrayList<NodeDescription>();
-        NodeDescription child = new NodeDescription();
-        child.setName("c1");
-        PropertyDescription prop = new PropertyDescription();
-        prop.setName("c1p1");
-        prop.setValue("c1v1");
-        child.addProperty(prop);
-        nodes.add(child);
-
-        String json = this.toJsonObject(nodes).toString();
-
-        NodeDescription node = this.parse(json);
-        assertNotNull("Expecting node", node);
-        assertEquals(nodes, node.getChildren());
-    }
-
-    //---------- internal helper ----------------------------------------------
-
-    private NodeDescription parse(String json) throws IOException {
-        String charSet = "ISO-8859-1";
-        json = "#" + charSet + "\r\n" + json;
-        InputStream ins = new ByteArrayInputStream(json.getBytes(charSet));
-        return this.jsonReader.parse(ins);
-    }
-
-    private Set<Object> toSet(Object[] content) {
-        Set<Object> set = new HashSet<Object>();
-        for (int i=0; content != null && i < content.length; i++) {
-            set.add(content[i]);
-        }
-
-        return set;
-    }
-
-    private JSONArray toJsonArray(Collection<?> set) throws JSONException {
-        List<Object> list = new ArrayList<Object>();
-        for (Object item : set) {
-            if (item instanceof NodeDescription) {
-                list.add(this.toJsonObject((NodeDescription) item));
-            } else {
-                list.add(item);
-            }
-        }
-        return new JSONArray(list);
-    }
-
-    private JSONObject toJsonObject(Collection<?> set) throws JSONException {
-        JSONObject obj = new JSONObject();
-        if (set != null) {
-            for (Object next: set) {
-                String name = this.getName(next);
-                obj.putOpt(name, this.toJsonObject(next));
-            }
-        }
-        return obj;
-    }
-
-    private Object toJsonObject(Object object) throws JSONException {
-        if (object instanceof NodeDescription) {
-            return this.toJsonObject((NodeDescription) object);
-        } else if (object instanceof PropertyDescription) {
-            return this.toJsonObject((PropertyDescription) object);
-        }
-
-        // fall back to string representation
-        return String.valueOf(object);
-    }
-
-    private JSONObject toJsonObject(NodeDescription node) throws JSONException {
-        JSONObject obj = new JSONObject();
-
-        if (node.getPrimaryNodeType() != null) {
-            obj.putOpt("jcr:primaryType", node.getPrimaryNodeType());
-        }
-
-        if (node.getMixinNodeTypes() != null) {
-            obj.putOpt("jcr:mixinTypes", this.toJsonArray(node.getMixinNodeTypes()));
-        }
-
-        if (node.getProperties() != null) {
-            for (PropertyDescription prop : node.getProperties()) {
-                obj.put(prop.getName(), toJsonObject(prop));
-            }
-        }
-
-        if (node.getChildren() != null) {
-            for (NodeDescription child : node.getChildren()) {
-                obj.put(child.getName(), toJsonObject(child));
-            }
-        }
-
-        return obj;
-    }
-
-    private Object toJsonObject(PropertyDescription property) throws JSONException {
-        if (!property.isMultiValue() && PropertyType.TYPENAME_STRING.equals(property.getType())) {
-            return this.toJsonObject(property.getValue());
-        }
-        Object obj;
-        if (property.isMultiValue()) {
-            obj = this.toJsonArray(property.getValues());
-        } else {
-            obj = this.toJsonObject(property.getValue());
-        }
-
-        return obj;
-    }
-
-    private String getName(Object object) {
-        if (object instanceof NodeDescription) {
-            return ((NodeDescription) object).getName();
-        } else if (object instanceof PropertyDescription) {
-            return ((PropertyDescription) object).getName();
-        }
-
-        // fall back to string representation
-        return String.valueOf(object);
-    }
-}
diff --git a/src/test/java/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java b/src/test/java/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
new file mode 100644
index 0000000..5b3463f
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.contentloader.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.Sequence;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.runner.RunWith;
+
+@RunWith(JMock.class)
+public class JsonReaderTest {
+
+    JsonReader jsonReader;
+
+    Mockery mockery = new JUnit4Mockery();
+
+    ContentCreator creator;
+
+    Sequence mySequence;
+
+    @org.junit.Before public void setUp() throws Exception {
+        this.jsonReader = new JsonReader();
+        this.creator = this.mockery.mock(ContentCreator.class);
+        this.mySequence = this.mockery.sequence("my-sequence");
+    }
+
+    @org.junit.After public void tearDown() throws Exception {
+        this.jsonReader = null;
+    }
+
+    @org.junit.Test public void testEmptyObject() throws Exception {
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse("");
+    }
+
+    @org.junit.Test public void testEmpty() throws IOException, RepositoryException {
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse("{}");
+    }
+
+    @org.junit.Test public void testDefaultPrimaryNodeTypeWithSurroundWhitespace() throws Exception {
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        String json = "     {  }     ";
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testDefaultPrimaryNodeTypeWithoutEnclosingBracesWithSurroundWhitespace() throws Exception {
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        String json = "             ";
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testExplicitePrimaryNodeType() throws Exception {
+        final String type = "xyz:testType";
+        String json = "{ \"jcr:primaryType\": \"" + type + "\" }";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, type, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testMixinNodeTypes1() throws Exception {
+        final String[] mixins = new String[]{ "xyz:mix1" };
+        String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, mixins); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testMixinNodeTypes2() throws Exception {
+        final String[] mixins = new String[]{ "xyz:mix1", "abc:mix2" };
+        String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, mixins); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testPropertiesEmpty() throws Exception {
+        String json = "{ \"property\": \"\"}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createProperty("property", PropertyType.STRING, ""); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testPropertiesSingleValue() throws Exception {
+        String json = "{ \"p1\": \"v1\"}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createProperty("p1", PropertyType.STRING, "v1"); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testPropertiesTwoSingleValue() throws Exception {
+        String json = "{ \"p1\": \"v1\", \"p2\": \"v2\"}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createProperty("p1", PropertyType.STRING, "v1"); inSequence(mySequence);
+            allowing(creator).createProperty("p2", PropertyType.STRING, "v2"); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testPropertiesMultiValue() throws Exception {
+        String json = "{ \"p1\": [\"v1\"]}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createProperty("p1", PropertyType.STRING, new String[] {"v1"}); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testPropertiesMultiValueEmpty() throws Exception {
+        String json = "{ \"p1\": []}";
+
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createProperty("p1", PropertyType.STRING, new String[0]); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testChild() throws Exception {
+        String json = "{ " +
+                      " c1 : {}" +
+                      "}";
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createNode("c1", null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testChildWithMixin() throws Exception {
+        String json = "{ " +
+        " c1 : {" +
+              "\"jcr:mixinTypes\" : [\"xyz:TestType\"]" +
+              "}" +
+        "}";
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createNode("c1", null, new String[] {"xyz:TestType"}); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testTwoChildren() throws Exception {
+        String json = "{ " +
+        " c1 : {}," +
+        " c2 : {}" +
+        "}";
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createNode("c1", null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+            allowing(creator).createNode("c2", null, null); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    @org.junit.Test public void testChildWithProperty() throws Exception {
+        String json = "{ " +
+        " c1 : {" +
+        "      c1p1 : \"v1\"" +
+              "}" +
+        "}";
+        this.mockery.checking(new Expectations() {{
+            allowing(creator).createNode(null, null, null); inSequence(mySequence);
+            allowing(creator).createNode("c1", null, null); inSequence(mySequence);
+            allowing(creator).createProperty("c1p1", PropertyType.STRING, "v1");
+            allowing(creator).finishNode(); inSequence(mySequence);
+            allowing(creator).finishNode(); inSequence(mySequence);
+        }});
+        this.parse(json);
+    }
+
+    //---------- internal helper ----------------------------------------------
+
+    private void parse(String json) throws IOException, RepositoryException {
+        String charSet = "ISO-8859-1";
+        json = "#" + charSet + "\r\n" + json;
+        InputStream ins = new ByteArrayInputStream(json.getBytes(charSet));
+        this.jsonReader.parse(ins, this.creator);
+    }
+
+    private JSONArray toJsonArray(String[] array) throws JSONException {
+        return new JSONArray(Arrays.asList(array));
+    }
+}

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