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:45:55 UTC

[sling-org-apache-sling-jcr-contentloader] 02/32: SLING-400: Move content loading to own bundle.

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.2-incubator
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-contentloader.git

commit e7c3f36531d364510618407bd2ca8cf50d34d4a0
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Wed Apr 30 07:05:59 2008 +0000

    SLING-400: Move content loading to own bundle.
    
    git-svn-id: https://svn.apache.org/repos/asf/incubator/sling/trunk/jcr/contentloader@652308 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  10 +-
 .../internal/ContentLoaderService.java             | 327 ++++++++
 .../jcr/contentloader/internal/ImportProvider.java |  27 +
 .../jcr/contentloader/internal/JsonReader.java     | 190 +++++
 .../sling/jcr/contentloader/internal/Loader.java   | 924 +++++++++++++++++++++
 .../contentloader/internal/NodeDescription.java    | 153 ++++
 .../jcr/contentloader/internal/NodeReader.java     |  31 +
 .../jcr/contentloader/internal/PathEntry.java      |  91 ++
 .../internal/PropertyDescription.java              | 119 +++
 .../jcr/contentloader/internal/XmlReader.java      | 169 ++++
 10 files changed, 2036 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index ef1f7e7..8b9958a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
                 <configuration>
                     <instructions>
                         <Private-Package>
-                            org.apache.sling.jcr.resource.internal.*,
+                            org.apache.sling.jcr.contentloader.internal.*,
                             org.kxml2.io, org.xmlpull.v1
                         </Private-Package>
 
@@ -83,12 +83,13 @@
             <artifactId>org.osgi.compendium</artifactId>
         </dependency>
         <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>servlet-api</artifactId>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.jcr.api</artifactId>
+            <version>2.0.0-incubator-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.api</artifactId>
+            <artifactId>org.apache.sling.commons.json</artifactId>
             <version>2.0.0-incubator-SNAPSHOT</version>
         </dependency>
         <dependency>
@@ -100,7 +101,6 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
-
         <dependency>
             <groupId>net.sf.kxml</groupId>
             <artifactId>kxml2</artifactId>
diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java
new file mode 100644
index 0000000..57874f3
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java
@@ -0,0 +1,327 @@
+/*
+ * 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.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.lock.LockException;
+
+import org.apache.sling.commons.mime.MimeTypeService;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.SynchronousBundleListener;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ContentLoaderService</code> is the service
+ * providing the following functionality:
+ * <ul>
+ * <li>Bundle listener to load initial content.
+ * <li>Fires OSGi EventAdmin events on behalf of internal helper objects
+ * </ul>
+ *
+ * @scr.component metatype="false"
+ * @scr.property name="service.description" value="Sling
+ *               Content Loader Implementation"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ */
+public class ContentLoaderService implements SynchronousBundleListener {
+
+    public static final String PROPERTY_CONTENT_LOADED = "content-loaded";
+
+    public static final String BUNDLE_CONTENT_NODE = "/var/sling/bundle-content";
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * The JCR Repository we access to resolve resources
+     *
+     * @scr.reference
+     */
+    private SlingRepository repository;
+
+    /**
+     * The MimeTypeService used by the initial content initialContentLoader to
+     * resolve MIME types for files to be installed.
+     *
+     * @scr.reference
+     */
+    private MimeTypeService mimeTypeService;
+
+    /**
+     * Administrative sessions used to check item existence.
+     */
+    private Session adminSession;
+
+    /**
+     * The initial content loader which is called to load initial content up
+     * into the repository when the providing bundle is installed.
+     */
+    private Loader initialContentLoader;
+
+    // ---------- BundleListener -----------------------------------------------
+
+    /**
+     * Loads and unloads any content provided by the bundle whose state
+     * changed. If the bundle has been started, the content is loaded. If
+     * the bundle is about to stop, the content are unloaded.
+     *
+     * @param event The <code>BundleEvent</code> representing the bundle state
+     *            change.
+     */
+    public void bundleChanged(BundleEvent event) {
+
+        //
+        // NOTE:
+        // This is synchronous - take care to not block the system !!
+        //
+
+        switch (event.getType()) {
+            case BundleEvent.STARTING:
+                // register content when the bundle content is available
+                // as node types are registered when the bundle is installed
+                // we can safely add the content at this point.
+                try {
+                    Session session = getAdminSession();
+                    initialContentLoader.registerBundle(session, event.getBundle(), false);
+                } catch (Throwable t) {
+                    log.error(
+                        "bundleChanged: Problem loading initial content of bundle "
+                            + event.getBundle().getSymbolicName() + " ("
+                            + event.getBundle().getBundleId() + ")", t);
+                }
+                break;
+            case BundleEvent.UPDATED:
+                try {
+                    Session session = getAdminSession();
+                    initialContentLoader.registerBundle(session, event.getBundle(), true);
+                } catch (Throwable t) {
+                    log.error(
+                        "bundleChanged: Problem updating initial content of bundle "
+                            + event.getBundle().getSymbolicName() + " ("
+                            + event.getBundle().getBundleId() + ")", t);
+                }
+                break;
+            case BundleEvent.STOPPED:
+                try {
+                    Session session = getAdminSession();
+                    initialContentLoader.unregisterBundle(session, event.getBundle());
+                } catch (Throwable t) {
+                    log.error(
+                        "bundleChanged: Problem unloading initial content of bundle "
+                            + event.getBundle().getSymbolicName() + " ("
+                            + event.getBundle().getBundleId() + ")", t);
+                }
+                break;
+        }
+    }
+
+    // ---------- Implementation helpers --------------------------------------
+
+    /** Returns the MIME type from the MimeTypeService for the given name */
+    public String getMimeType(String name) {
+        // local copy to not get NPE despite check for null due to concurrent
+        // unbind
+        MimeTypeService mts = mimeTypeService;
+        return (mts != null) ? mts.getMimeType(name) : null;
+    }
+
+    protected void createRepositoryPath(final Session writerSession, final String repositoryPath)
+    throws RepositoryException {
+        if ( !writerSession.itemExists(repositoryPath) ) {
+            Node node = writerSession.getRootNode();
+            String path = repositoryPath.substring(1);
+            int pos = path.lastIndexOf('/');
+            if ( pos != -1 ) {
+                final StringTokenizer st = new StringTokenizer(path.substring(0, pos), "/");
+                while ( st.hasMoreTokens() ) {
+                    final String token = st.nextToken();
+                    if ( !node.hasNode(token) ) {
+                        node.addNode(token, "sling:Folder");
+                        node.save();
+                    }
+                    node = node.getNode(token);
+                }
+                path = path.substring(pos + 1);
+            }
+            if ( !node.hasNode(path) ) {
+                node.addNode(path, "sling:Folder");
+                node.save();
+            }
+        }
+    }
+
+    // ---------- SCR Integration ---------------------------------------------
+
+    /** Activates this component, called by SCR before registering as a service */
+    protected void activate(ComponentContext componentContext) {
+        this.initialContentLoader = new Loader(this);
+
+        componentContext.getBundleContext().addBundleListener(this);
+
+        try {
+            final Session session = getAdminSession();
+            this.createRepositoryPath(session, ContentLoaderService.BUNDLE_CONTENT_NODE);
+            log.debug(
+                    "Activated - attempting to load content from all "
+                    + "bundles which are neither INSTALLED nor UNINSTALLED");
+
+            int ignored = 0;
+            Bundle[] bundles = componentContext.getBundleContext().getBundles();
+            for (Bundle bundle : bundles) {
+                if ((bundle.getState() & (Bundle.INSTALLED | Bundle.UNINSTALLED)) == 0) {
+                    // load content for bundles which are neither INSTALLED nor
+                    // UNINSTALLED
+                    initialContentLoader.registerBundle(session, bundle, false);
+                } else {
+                    ignored++;
+                }
+
+            }
+
+            log.debug(
+                    "Out of {} bundles, {} were not in a suitable state for initial content loading",
+                    bundles.length, ignored
+                    );
+
+        } catch (Throwable t) {
+            log.error("activate: Problem while loading initial content and"
+                + " registering mappings for existing bundles", t);
+        }
+    }
+
+    /** Deativates this component, called by SCR to take out of service */
+    protected void deactivate(ComponentContext componentContext) {
+        componentContext.getBundleContext().removeBundleListener(this);
+
+        if ( this.initialContentLoader != null ) {
+            this.initialContentLoader.dispose();
+            this.initialContentLoader = null;
+        }
+
+        if ( adminSession != null ) {
+            this.adminSession.logout();
+            this.adminSession = null;
+        }
+    }
+
+    // ---------- internal helper ----------------------------------------------
+
+    /** Returns the JCR repository used by this service. */
+    protected SlingRepository getRepository() {
+        return repository;
+    }
+
+    /**
+     * Returns an administrative session to the default workspace.
+     */
+    private synchronized Session getAdminSession()
+    throws RepositoryException {
+        if ( adminSession == null ) {
+            adminSession = getRepository().loginAdministrative(null);
+        }
+        return adminSession;
+    }
+
+    /**
+     * Return the bundle content info and make an exclusive lock.
+     * @param session
+     * @param bundle
+     * @return The map of bundle content info or null.
+     * @throws RepositoryException
+     */
+    public Map<String, Object> getBundleContentInfo(final Session session, final Bundle bundle)
+    throws RepositoryException {
+        final String nodeName = bundle.getSymbolicName();
+        final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE);
+        if ( !parentNode.hasNode(nodeName) ) {
+            try {
+                final Node bcNode = parentNode.addNode(nodeName, "nt:unstructured");
+                bcNode.addMixin("mix:lockable");
+                parentNode.save();
+            } catch (RepositoryException re) {
+                // for concurrency issues (running in a cluster) we ignore exceptions
+                this.log.warn("Unable to create node " + nodeName, re);
+                session.refresh(true);
+            }
+        }
+        final Node bcNode = parentNode.getNode(nodeName);
+        if ( bcNode.isLocked() ) {
+            return null;
+        }
+        try {
+            bcNode.lock(false, true);
+        } catch (LockException le) {
+            return null;
+        }
+        final Map<String, Object> info = new HashMap<String, Object>();
+        if ( bcNode.hasProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED) ) {
+            info.put(ContentLoaderService.PROPERTY_CONTENT_LOADED,
+                    bcNode.getProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED).getBoolean());
+        } else {
+            info.put(ContentLoaderService.PROPERTY_CONTENT_LOADED, false);
+        }
+        return info;
+    }
+
+    public void unlockBundleContentInto(final Session session,
+                                        final Bundle  bundle,
+                                        final boolean contentLoaded)
+    throws RepositoryException {
+        final String nodeName = bundle.getSymbolicName();
+        final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE);
+        final Node bcNode = parentNode.getNode(nodeName);
+        if ( contentLoaded ) {
+            bcNode.setProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED, contentLoaded);
+            bcNode.setProperty("content-load-time", Calendar.getInstance());
+            bcNode.setProperty("content-loaded-by", bundle.getBundleContext().getProperty("sling.id"));
+            bcNode.setProperty("content-unload-time", (String)null);
+            bcNode.setProperty("content-unloaded-by", (String)null);
+            bcNode.save();
+        }
+        bcNode.unlock();
+    }
+
+    public void contentIsUninstalled(final Session session,
+                                     final Bundle  bundle) {
+        final String nodeName = bundle.getSymbolicName();
+        try {
+            final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE);
+            if ( parentNode.hasNode(nodeName) ) {
+                final Node bcNode = parentNode.getNode(nodeName);
+                bcNode.setProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED, false);
+                bcNode.setProperty("content-unload-time", Calendar.getInstance());
+                bcNode.setProperty("content-unloaded-by", bundle.getBundleContext().getProperty("sling.id"));
+                bcNode.save();
+            }
+        } catch (RepositoryException re) {
+            this.log.error("Unable to update bundle content info.", re);
+        }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..e28fd55
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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.IOException;
+
+interface ImportProvider {
+
+    NodeReader 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
new file mode 100644
index 0000000..4b5a1e5
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java
@@ -0,0 +1,190 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.PropertyType;
+
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+
+
+/**
+ * The <code>JsonReader</code> TODO
+ */
+class JsonReader implements NodeReader {
+
+    private static final Set<String> ignoredNames = new HashSet<String>();
+    static {
+        ignoredNames.add("jcr:primaryType");
+        ignoredNames.add("jcr:mixinTypes");
+        ignoredNames.add("jcr:uuid");
+        ignoredNames.add("jcr:baseVersion");
+        ignoredNames.add("jcr:predecessors");
+        ignoredNames.add("jcr:successors");
+        ignoredNames.add("jcr:checkedOut");
+        ignoredNames.add("jcr:created");
+    }
+
+    static final ImportProvider PROVIDER = new ImportProvider() {
+        private JsonReader jsonReader;
+
+        public NodeReader getReader() {
+            if (jsonReader == null) {
+                jsonReader = new JsonReader();
+            }
+            return jsonReader;
+        }
+    };
+    
+    public NodeDescription parse(InputStream ins) throws IOException {
+        try {
+            String jsonString = toString(ins).trim();
+            if (!jsonString.startsWith("{")) {
+                jsonString = "{" + jsonString + "}";
+            }
+
+            JSONObject json = new JSONObject(jsonString);
+            String name = json.optString("name", null); // allow for no name !
+            return this.createNode(name, json);
+
+        } 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));
+        }
+
+        Object mixinsObject = obj.opt("jcr:mixinTypes");
+        if (mixinsObject instanceof JSONArray) {
+            JSONArray mixins = (JSONArray) mixinsObject;
+            for (int i = 0; i < mixins.length(); i++) {
+                node.addMixinNodeType(mixins.getString(i));
+            }
+        }
+
+        // add properties and nodes
+        JSONArray names = obj.names();
+        for (int i = 0; names != null && i < names.length(); i++) {
+            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);
+                } else {
+                    PropertyDescription prop = createProperty(n, o);
+                    node.addProperty(prop);
+                }
+            }
+        }
+        return node;
+    }
+
+    protected PropertyDescription createProperty(String name, Object value)
+            throws JSONException {
+        PropertyDescription property = new PropertyDescription();
+        property.setName(name);
+
+        // assume simple value
+        if (value instanceof JSONArray) {
+            // multivalue
+            JSONArray array = (JSONArray) value;
+            if (array.length() > 0) {
+                for (int i = 0; i < array.length(); i++) {
+                    property.addValue(array.get(i));
+                }
+                value = array.opt(0);
+            } else {
+                property.addValue(null);
+                value = null;
+            }
+
+        } else {
+            // single value
+            property.setValue(String.valueOf(value));
+        }
+        // set type
+        property.setType(getType(value));
+
+        return property;
+    }
+
+    protected String getType(Object object) {
+        if (object instanceof Double || object instanceof Float) {
+            return PropertyType.TYPENAME_DOUBLE;
+        } else if (object instanceof Number) {
+            return PropertyType.TYPENAME_LONG;
+        } else if (object instanceof Boolean) {
+            return PropertyType.TYPENAME_BOOLEAN;
+        }
+
+        // fall back to default
+        return PropertyType.TYPENAME_STRING;
+    }
+    
+    private String toString(InputStream ins) throws IOException {
+        if (!ins.markSupported()) {
+            ins = new BufferedInputStream(ins);
+        }
+        
+        String encoding;
+        ins.mark(5);
+        int c = ins.read();
+        if (c == '#') {
+            // character encoding following
+            StringBuffer buf = new StringBuffer();
+            for (c = ins.read(); !Character.isWhitespace((char) c); c = ins.read()) {
+                buf.append((char) c);
+            }
+            encoding = buf.toString();
+        } else {
+            ins.reset();
+            encoding = "UTF-8";
+        }
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buf = new byte[1024];
+        int rd;
+        while ( (rd = ins.read(buf)) >= 0) {
+            bos.write(buf, 0, rd);
+        }
+        bos.close(); // just to comply with the contract
+        
+        return new String(bos.toByteArray(), encoding);
+    }
+}
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
new file mode 100644
index 0000000..300b2f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java
@@ -0,0 +1,924 @@
+/*
+ * 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 static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW;
+
+import java.io.IOException;
+import java.io.InputStream;
+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;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+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;
+
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>Loader</code> loads initial content from the bundle.
+ */
+public class Loader {
+
+    public static final String EXT_XML = ".xml";
+
+    public static final String EXT_JCR_XML = ".jcr.xml";
+
+    public static final String EXT_JSON = ".json";
+
+    public static final String ROOT_DESCRIPTOR = "/ROOT";
+
+    // default content type for createFile()
+    private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(Loader.class);
+
+    private ContentLoaderService jcrContentHelper;
+
+    private Map<String, ImportProvider> importProviders;
+
+    private Map<String, List<String>> delayedReferences;
+
+    // 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>();
+        importProviders.put(EXT_JCR_XML, null);
+        importProviders.put(EXT_JSON, JsonReader.PROVIDER);
+        importProviders.put(EXT_XML, XmlReader.PROVIDER);
+    }
+
+    public void dispose() {
+        this.delayedReferences = null;
+        if (this.delayedBundles != null) {
+            this.delayedBundles.clear();
+            this.delayedBundles = null;
+        }
+        this.jcrContentHelper = null;
+        this.importProviders.clear();
+    }
+
+    /**
+     * Register a bundle and install its content.
+     * @param session
+     * @param bundle
+     */
+    public void registerBundle(final Session session, final Bundle bundle, final boolean isUpdate) {
+        log.debug("Registering bundle {} for content loading.", bundle.getSymbolicName());
+        if (this.registerBundleInternal(session, bundle, false, isUpdate)) {
+            // handle delayed bundles, might help now
+            int currentSize = -1;
+            for (int i = this.delayedBundles.size(); i > 0
+                && currentSize != this.delayedBundles.size()
+                && !this.delayedBundles.isEmpty(); i--) {
+                for (Iterator<Bundle> di = this.delayedBundles.iterator(); di.hasNext();) {
+                    Bundle delayed = di.next();
+                    if (this.registerBundleInternal(session, delayed, true, false)) {
+                        di.remove();
+                    }
+                }
+                currentSize = this.delayedBundles.size();
+            }
+        } else {
+            // add to delayed bundles - if this is not an update!
+            if ( !isUpdate ) {
+                this.delayedBundles.add(bundle);
+            }
+        }
+    }
+
+    private boolean registerBundleInternal(final Session session,
+                                           final Bundle  bundle,
+                                           final boolean isRetry,
+                                           final boolean isUpdate) {
+        // check if bundle has initial content
+        final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle);
+        if (pathIter == null) {
+            log.debug("Bundle {} has no initial content",
+                bundle.getSymbolicName());
+            return true;
+        }
+
+        try {
+            // check if the content has already been loaded
+            final Map<String, Object> bundleContentInfo = this.jcrContentHelper.getBundleContentInfo(session, bundle);
+            // if we don't get an info, someone else is currently loading
+            if ( bundleContentInfo == null ) {
+                return false;
+            }
+
+            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 {
+                    this.installContent(session, bundle, pathIter);
+                    if (isRetry) {
+                        // log success of retry
+                        log.info(
+                            "Retrytring to load initial content for bundle {} succeeded.",
+                            bundle.getSymbolicName());
+                    }
+                }
+                success = true;
+                return true;
+            } finally {
+                this.jcrContentHelper.unlockBundleContentInto(session, bundle, success);
+            }
+        } catch (RepositoryException re) {
+            // if we are retrying we already logged this message once, so we
+            // won't log it again
+            if (!isRetry) {
+                log.error("Cannot load initial content for bundle "
+                    + bundle.getSymbolicName() + " : " + re.getMessage(), re);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 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 (this.delayedBundles.contains(bundle)) {
+            this.delayedBundles.remove(bundle);
+        } else {
+            if ( pathIter != null ) {
+                this.uninstallContent(session, bundle, pathIter);
+                this.jcrContentHelper.contentIsUninstalled(session, bundle);
+            }
+        }
+    }
+
+    // ---------- internal -----------------------------------------------------
+
+    private void installContent(Session session, Bundle bundle, final Iterator<PathEntry> pathIter)
+    throws RepositoryException {
+        try {
+            log.debug("Installing initial content from bundle {}",
+                bundle.getSymbolicName());
+            while (pathIter.hasNext() ) {
+                final PathEntry entry = pathIter.next();
+                this.installFromPath(bundle, entry.getPath(), entry.isOverwrite(), session.getRootNode());
+            }
+
+            // persist modifications now
+            session.save();
+            log.debug("Done installing initial content from bundle {}",
+                bundle.getSymbolicName());
+        } finally {
+            try {
+                if (session.hasPendingChanges()) {
+                    session.refresh(false);
+                }
+            } catch (RepositoryException re) {
+                log.warn(
+                    "Failure to rollback partial initial content for bundle {}",
+                    bundle.getSymbolicName(), re);
+            }
+        }
+
+    }
+
+    /**
+     * 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,
+                                 final Node parent)
+    throws RepositoryException {
+        @SuppressWarnings("unchecked")
+        Enumeration<String> entries = bundle.getEntryPaths(path);
+        if (entries == null) {
+            log.info("install: No initial content entries at {}", path);
+            return;
+        }
+
+        Set<URL> ignoreEntry = new HashSet<URL>();
+
+        // potential root node import/extension
+        URL rootNodeDescriptor = importRootNode(parent.getSession(), bundle, path);
+        if (rootNodeDescriptor != null) {
+            ignoreEntry.add(rootNodeDescriptor);
+        }
+
+        while (entries.hasMoreElements()) {
+            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 = this.getName(base);
+
+                URL nodeDescriptor = null;
+                for (String ext : importProviders.keySet()) {
+                    nodeDescriptor = bundle.getEntry(base + ext);
+                    if (nodeDescriptor != null) {
+                        break;
+                    }
+                }
+
+                // if we have a descriptor, which has not been processed yet,
+                // otherwise call createFolder, which creates an nt:folder or
+                // returns an existing node (created by a descriptor)
+                Node node = null;
+                if (nodeDescriptor != null
+                    && !ignoreEntry.contains(nodeDescriptor)) {
+                    node = this.createNode(parent, name, nodeDescriptor, overwrite);
+                    ignoreEntry.add(nodeDescriptor);
+                } else {
+                    node = this.createFolder(parent, name, overwrite);
+                }
+
+                // walk down the line
+                if (node != null) {
+                    this.installFromPath(bundle, entry, overwrite, node);
+                }
+
+            } else {
+                // file => create file
+                URL file = bundle.getEntry(entry);
+                if (ignoreEntry.contains(file)) {
+                    // this is a consumed node descriptor
+                    continue;
+                }
+
+                // install if it is a descriptor
+                boolean foundProvider = false;
+                final Iterator<String> ipIter = this.importProviders.keySet().iterator();
+                while ( !foundProvider && ipIter.hasNext() ) {
+                    final String ext = ipIter.next();
+                    if ( entry.endsWith(ext) ) {
+                        foundProvider = true;
+                    }
+                }
+                if (foundProvider) {
+                    if (this.createNode(parent, this.getName(entry), file, overwrite) != null) {
+                        ignoreEntry.add(file);
+                        continue;
+                    }
+                }
+
+                // otherwise just place as file
+                try {
+                    this.createFile(parent, file);
+                } catch (IOException ioe) {
+                    log.warn("Cannot create file node for {}", file, ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * 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.
+     * @return The node pointing to the folder.
+     * @throws RepositoryException
+     */
+    private Node createFolder(Node parent, String name, final boolean overwrite)
+    throws RepositoryException {
+        if (parent.hasNode(name)) {
+            if ( overwrite ) {
+                parent.getNode(name).remove();
+            } else {
+                return parent.getNode(name);
+            }
+        }
+
+        return parent.addNode(name, "nt:folder");
+    }
+
+    private Node createNode(Node parent, String name, URL nodeXML, boolean overwrite)
+    throws RepositoryException {
+
+        InputStream ins = null;
+        try {
+            // special treatment for system view imports
+            if (nodeXML.getPath().toLowerCase().endsWith(EXT_JCR_XML)) {
+                return importSystemView(parent, name, nodeXML);
+            }
+
+            NodeReader nodeReader = null;
+            for (Map.Entry<String, ImportProvider> e: importProviders.entrySet()) {
+                if (nodeXML.getPath().toLowerCase().endsWith(e.getKey())) {
+                    nodeReader = e.getValue().getReader();
+                    break;
+                }
+            }
+
+            // cannot find out the type
+            if (nodeReader == null) {
+                return null;
+            }
+
+            ins = nodeXML.openStream();
+            NodeDescription clNode = nodeReader.parse(ins);
+
+            // 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 this.createNode(parent, clNode, overwrite);
+        } catch (RepositoryException re) {
+            throw re;
+        } catch (Throwable t) {
+            throw new RepositoryException(t.getMessage(), t);
+        } finally {
+            if (ins != null) {
+                try {
+                    ins.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Delete the node from the initial content.
+     * @param parent
+     * @param name
+     * @param nodeXML
+     * @throws RepositoryException
+     */
+    private void deleteNode(Node parent, String name)
+    throws RepositoryException {
+        if ( parent.hasNode(name) ) {
+            parent.getNode(name).remove();
+        }
+    }
+
+    private Node createNode(Node parentNode,
+                            NodeDescription clNode,
+                            final boolean overwrite)
+    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);
+    }
+
+    private Node setupNode(Node node,
+            NodeDescription clNode)
+            throws RepositoryException {
+
+        // ammend mixin node types
+        if (clNode.getMixinNodeTypes() != null) {
+            for (String mixin : clNode.getMixinNodeTypes()) {
+                if (!node.isNodeType(mixin)) {
+                    node.addMixin(mixin);
+                }
+            }
+        }
+
+        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 = this.getUUID(node.getSession(), propPath,
+                        prop.getValue());
+                    if (uuid != null) {
+                        node.setProperty(prop.getName(), uuid, type);
+                    }
+                } else {
+                    node.setProperty(prop.getName(), prop.getValue(), type);
+                }
+            }
+        }
+
+        if (clNode.getChildren() != null) {
+            for (NodeDescription child : clNode.getChildren()) {
+                this.createNode(node, child, false);
+            }
+        }
+
+        this.resolveReferences(node);
+
+        return node;
+    }
+
+    /**
+     * Create a file from the given url.
+     * @param parent
+     * @param source
+     * @throws IOException
+     * @throws RepositoryException
+     */
+    private void createFile(Node parent, URL source)
+    throws IOException, RepositoryException {
+        String name = this.getName(source.getPath());
+        if (parent.hasNode(name)) {
+            return;
+        }
+
+        URLConnection conn = source.openConnection();
+        long lastModified = conn.getLastModified();
+        String type = conn.getContentType();
+        InputStream data = conn.getInputStream();
+
+        // ensure content type
+        if (type == null) {
+            type = this.jcrContentHelper.getMimeType(name);
+            if (type == null) {
+                log.info(
+                    "createFile: Cannot find content type for {}, using {}",
+                    source.getPath(), DEFAULT_CONTENT_TYPE);
+                type = DEFAULT_CONTENT_TYPE;
+            }
+        }
+
+        // ensure sensible last modification date
+        if (lastModified <= 0) {
+            lastModified = System.currentTimeMillis();
+        }
+
+        Node file = parent.addNode(name, "nt:file");
+        Node content = file.addNode("jcr:content", "nt:resource");
+        content.setProperty("jcr:mimeType", type);
+        content.setProperty("jcr:lastModified", lastModified);
+        content.setProperty("jcr:data", data);
+    }
+
+    /**
+     * Delete the file from the given url.
+     * @param parent
+     * @param source
+     * @throws IOException
+     * @throws RepositoryException
+     */
+    private void deleteFile(Node parent, URL source)
+    throws IOException, RepositoryException {
+        String name = this.getName(source.getPath());
+        if (parent.hasNode(name)) {
+            parent.getNode(name).remove();
+        }
+    }
+
+    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 = this.delayedReferences.get(referencePath);
+            if (current == null) {
+                current = new ArrayList<String>();
+                this.delayedReferences.put(referencePath, current);
+            }
+            current.add(propPath);
+        }
+
+        // no UUID found
+        return null;
+    }
+
+    private void resolveReferences(Node node) throws RepositoryException {
+        List<String> props = this.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 = this.getName(property);
+            Node parentNode = this.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
+     * slash is contained). To support names containing unsupported characters
+     * such as colon (<code>:</code>), names may be URL encoded (see
+     * <code>java.net.URLEncoder</code>) using the <i>UTF-8</i> character
+     * 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.
+     */
+    private String getName(String path) {
+        int lastSlash = path.lastIndexOf('/');
+        String name = (lastSlash < 0) ? path : path.substring(lastSlash + 1);
+
+        // check for encoded characters (%xx)
+        // has encoded characters, need to decode
+        if (name.indexOf('%') >= 0) {
+            try {
+                return URLDecoder.decode(name, "UTF-8");
+            } catch (UnsupportedEncodingException uee) {
+                // actually unexpected because UTF-8 is required by the spec
+                log.error("Cannot decode "
+                    + name
+                    + " beause the platform has no support for UTF-8, using undecoded");
+            } catch (Exception e) {
+                // IllegalArgumentException or failure to decode
+                log.error("Cannot decode " + name + ", using undecoded", e);
+            }
+        }
+
+        // not encoded or problems decoding, return the name unmodified
+        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 void uninstallContent(final Session session, final Bundle bundle, final Iterator<PathEntry> pathIter) {
+        try {
+            log.debug("Uninstalling initial content from bundle {}",
+                bundle.getSymbolicName());
+            while (pathIter.hasNext() ) {
+                final PathEntry entry = pathIter.next();
+                if ( entry.isOverwrite() ) {
+                    this.uninstallFromPath(bundle, entry.getPath(), session.getRootNode());
+                } else {
+                    log.debug("Ignoring to uninstall content at {}, overwrite flag is not set.", entry.getPath());
+                }
+            }
+
+            // persist modifications now
+            session.save();
+            log.debug("Done uninstalling initial content from bundle {}",
+                bundle.getSymbolicName());
+        } catch (RepositoryException re) {
+            log.error("Unable to uninstall initial content from bundle " + bundle.getSymbolicName(), re);
+        } finally {
+            try {
+                if (session.hasPendingChanges()) {
+                    session.refresh(false);
+                }
+            } catch (RepositoryException re) {
+                log.warn(
+                    "Failure to rollback uninstaling initial content for bundle {}",
+                    bundle.getSymbolicName(), re);
+            }
+        }
+    }
+
+    /**
+     * Handle content uninstallation for a single path.
+     * @param bundle The bundle containing the content.
+     * @param path   The path
+     * @param parent The parent node.
+     * @throws RepositoryException
+     */
+    private void uninstallFromPath(final Bundle bundle,
+                                   final String path,
+                                   final Node parent)
+    throws RepositoryException {
+        @SuppressWarnings("unchecked")
+        Enumeration<String> entries = bundle.getEntryPaths(path);
+        if (entries == null) {
+            return;
+        }
+
+        Set<URL> ignoreEntry = new HashSet<URL>();
+
+        // potential root node import/extension
+        Descriptor rootNodeDescriptor = this.getRootNodeDescriptor(bundle, path);
+        if (rootNodeDescriptor != null) {
+            ignoreEntry.add(rootNodeDescriptor.rootNodeDescriptor);
+        }
+
+        while (entries.hasMoreElements()) {
+            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 = this.getName(base);
+
+                URL nodeDescriptor = null;
+                for (String ext : importProviders.keySet()) {
+                    nodeDescriptor = bundle.getEntry(base + ext);
+                    if (nodeDescriptor != null) {
+                        break;
+                    }
+                }
+
+                final Node node;
+                boolean delete = false;
+                if (nodeDescriptor != null
+                    && !ignoreEntry.contains(nodeDescriptor)) {
+                    node = (parent.hasNode(toPlainName(name)) ? parent.getNode(toPlainName(name)) : null);
+                    delete = true;
+                } else {
+                    node = (parent.hasNode(name) ? parent.getNode(name) : null);
+                }
+
+                if ( node != null ) {
+                    // walk down the line
+                    this.uninstallFromPath(bundle, entry, node);
+                }
+
+                if (delete) {
+                    this.deleteNode(parent, toPlainName(name));
+                    ignoreEntry.add(nodeDescriptor);
+                }
+
+            } else {
+                // file => create file
+                URL file = bundle.getEntry(entry);
+                if (ignoreEntry.contains(file)) {
+                    // this is a consumed node descriptor
+                    continue;
+                }
+
+                // uninstall if it is a descriptor
+                boolean foundProvider = false;
+                final Iterator<String> ipIter = this.importProviders.keySet().iterator();
+                while ( !foundProvider && ipIter.hasNext() ) {
+                    final String ext = ipIter.next();
+                    if ( entry.endsWith(ext) ) {
+                        foundProvider = true;
+                    }
+                }
+                if (foundProvider) {
+                    this.deleteNode(parent, toPlainName(this.getName(entry)));
+                    ignoreEntry.add(file);
+                    continue;
+                }
+
+                // otherwise just delete the file
+                try {
+                    this.deleteFile(parent, file);
+                } catch (IOException ioe) {
+                    log.warn("Cannot delete file node for {}", file, ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * 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>
+     *         if the import fails due to XML format errors.
+     * @throws IOException If an IO error occurrs reading the XML file.
+     */
+    private Node importSystemView(Node parent, String name, URL nodeXML)
+            throws IOException {
+
+        InputStream ins = null;
+        try {
+
+            // check whether we have the content already, nothing to do then
+            name = toPlainName(name);
+            if (parent.hasNode(name)) {
+                log.debug(
+                    "importSystemView: Node {} for XML {} already exists, nothing to to",
+                    name, nodeXML);
+                return parent.getNode(name);
+            }
+
+            ins = nodeXML.openStream();
+            Session session = parent.getSession();
+            session.importXML(parent.getPath(), ins, IMPORT_UUID_CREATE_NEW);
+
+            // additionally check whether the expected child node exists
+            return (parent.hasNode(name)) ? parent.getNode(name) : null;
+
+        } catch (InvalidSerializedDataException isde) {
+
+            // the xml might not be System or Document View export, fall back
+            // to old-style XML reading
+            log.info(
+                "importSystemView: XML {} does not seem to be system view export, trying old style",
+                nodeXML);
+            return null;
+
+        } catch (RepositoryException re) {
+
+            // any other repository related issue...
+            log.info(
+                "importSystemView: Repository issue loading XML {}, trying old style",
+                nodeXML);
+            return null;
+
+        } finally {
+            if (ins != null) {
+                try {
+                    ins.close();
+                } catch (IOException ignore) {
+                    // ignore
+                }
+            }
+        }
+
+    }
+
+    protected static final class Descriptor {
+        public URL rootNodeDescriptor;
+        public NodeReader nodeReader;
+    }
+
+    /**
+     * Return the root node descriptor.
+     */
+    private Descriptor getRootNodeDescriptor(final Bundle bundle, final String path) {
+        URL rootNodeDescriptor = null;
+
+        for (Map.Entry<String, ImportProvider> e : importProviders.entrySet()) {
+            if (e.getValue() != null) {
+                rootNodeDescriptor = bundle.getEntry(path + ROOT_DESCRIPTOR + e.getKey());
+                if (rootNodeDescriptor != null) {
+                    try {
+                        final Descriptor d = new Descriptor();
+                        d.rootNodeDescriptor = rootNodeDescriptor;
+                        d.nodeReader = e.getValue().getReader();
+                        return d;
+                    } catch (IOException ioe) {
+                        this.log.error("Unable to setup node reader for " + e.getKey(), ioe);
+                        return null;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Imports mixin nodes and properties (and optionally child nodes) of the
+     * root node.
+     */
+    private URL importRootNode(Session session, Bundle bundle, String path)
+    throws RepositoryException {
+        final Descriptor descriptor = this.getRootNodeDescriptor(bundle, path);
+        // no root descriptor found
+        if (descriptor == null) {
+            return null;
+        }
+
+        InputStream ins = null;
+        try {
+
+            ins = descriptor.rootNodeDescriptor.openStream();
+            NodeDescription clNode = descriptor.nodeReader.parse(ins);
+
+            setupNode(session.getRootNode(), clNode);
+
+            return descriptor.rootNodeDescriptor;
+        } catch (RepositoryException re) {
+            throw re;
+        } catch (Throwable t) {
+            throw new RepositoryException(t.getMessage(), t);
+        } finally {
+            if (ins != null) {
+                try {
+                    ins.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+
+    }
+
+    private String toPlainName(String name) {
+        String providerExt = null;
+        final Iterator<String> ipIter = this.importProviders.keySet().iterator();
+        while ( providerExt == null && ipIter.hasNext() ) {
+            final String ext = ipIter.next();
+            if ( name.endsWith(ext) ) {
+                providerExt = ext;
+            }
+        }
+        if (providerExt != null) {
+            return name.substring(0, name.length() - providerExt.length());
+        }
+        return name;
+
+    }
+}
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
new file mode 100644
index 0000000..043da37
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java
@@ -0,0 +1,153 @@
+/*
+ * 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/NodeReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
new file mode 100644
index 0000000..2653114
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java
@@ -0,0 +1,31 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+
+/**
+ * The <code>NodeReader</code> TODO
+ */
+interface NodeReader {
+
+    NodeDescription parse(InputStream ins) throws IOException;
+
+}
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
new file mode 100644
index 0000000..b9a0633
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java
@@ -0,0 +1,91 @@
+/*
+ * 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.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.osgi.framework.Bundle;
+
+/**
+ * A path entry from the manifest for initial content.
+ */
+public class PathEntry {
+
+    /** The manifest header to specify initial content to be loaded. */
+    public static final String CONTENT_HEADER = "Sling-Initial-Content";
+
+    /** The overwrite flag specifying if content should be overwritten or just initially added. */
+    public static final String OVERWRITE_FLAG = "overwrite";
+
+    /** The path for the initial content. */
+    private final String path;
+
+    /** Should existing content be overwritten? */
+    private final boolean overwrite;
+
+    public static Iterator<PathEntry> getContentPaths(final Bundle bundle) {
+        final List<PathEntry> entries = new ArrayList<PathEntry>();
+
+        final String root = (String) bundle.getHeaders().get(CONTENT_HEADER);
+        if (root != null) {
+            final StringTokenizer tokener = new StringTokenizer(root, ",");
+            while (tokener.hasMoreTokens()) {
+                final String path = tokener.nextToken().trim();
+                entries.add(new PathEntry(path));
+            }
+        }
+
+        if ( entries.size() == 0 ) {
+            return null;
+        }
+        return entries.iterator();
+    }
+
+    public PathEntry(String path) {
+        // check for overwrite flag
+        boolean overwrite = false;
+        int flagPos = path.indexOf(";");
+        if ( flagPos != -1 ) {
+            final StringTokenizer flagTokenizer = new StringTokenizer(path.substring(flagPos+1), ";");
+            while ( flagTokenizer.hasMoreTokens() ) {
+                final String token = flagTokenizer.nextToken();
+                int pos = token.indexOf(":=");
+                if ( pos != -1 ) {
+                    if ( token.substring(0, pos).equals(OVERWRITE_FLAG) ) {
+                        overwrite = Boolean.valueOf(token.substring(pos+2));
+                    }
+                }
+            }
+            path = path.substring(0, flagPos);
+        }
+        this.path = path;
+        this.overwrite = overwrite;
+    }
+
+    public String getPath() {
+        return this.path;
+    }
+
+    public boolean isOverwrite() {
+        return this.overwrite;
+    }
+}
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
new file mode 100644
index 0000000..4fd7823
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java
@@ -0,0 +1,119 @@
+/*
+ * 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
new file mode 100644
index 0000000..a96155e
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java
@@ -0,0 +1,169 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+
+import org.kxml2.io.KXmlParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class XmlReader implements NodeReader {
+
+    /*
+     * <node> <primaryNodeType>type</primaryNodeType> <mixinNodeTypes>
+     * <mixinNodeType>mixtype1</mixinNodeType> <mixinNodeType>mixtype2</mixinNodeType>
+     * </mixinNodeTypes> <properties> <property> <name>propName</name>
+     * <value>propValue</value> <type>propType</type> </property> <!-- more
+     * --> </properties> </node>
+     */
+
+    private static final String ELEM_NODE = "node";
+
+    private static final String ELEM_PRIMARY_NODE_TYPE = "primaryNodeType";
+
+    private static final String ELEM_MIXIN_NODE_TYPE = "mixinNodeType";
+
+    private static final String ELEM_PROPERTY = "property";
+
+    private static final String ELEM_NAME = "name";
+
+    private static final String ELEM_VALUE = "value";
+
+    private static final String ELEM_VALUES = "values";
+
+    private static final String ELEM_TYPE = "type";
+
+    static final ImportProvider PROVIDER = new ImportProvider() {
+        private XmlReader xmlReader;
+
+        public NodeReader getReader() throws IOException {
+            if (xmlReader == null) {
+                try {
+                    xmlReader = new XmlReader();
+                } catch (Throwable t) {
+                    throw (IOException) new IOException(t.getMessage()).initCause(t);
+                }
+            }
+            return xmlReader;
+        }
+    };
+
+    private KXmlParser xmlParser;
+
+    XmlReader() {
+        this.xmlParser = new KXmlParser();
+    }
+
+    // ---------- XML content access -------------------------------------------
+
+    public synchronized NodeDescription parse(InputStream ins) throws IOException {
+        try {
+            return this.parseInternal(ins);
+        } 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;
+
+        // set the parser input, use null encoding to force detection with
+        // <?xml?>
+        this.xmlParser.setInput(ins, null);
+
+        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();
+                } else if (ELEM_NODE.equals(currentElement)) {
+                    if (currentNode != null) nodes.add(currentNode);
+                    currentNode = new NodeDescription();
+                }
+
+            } else if (eventType == XmlPullParser.END_TAG) {
+
+                String qName = this.xmlParser.getName();
+                String content = contentBuffer.toString().trim();
+                contentBuffer.delete(0, contentBuffer.length());
+
+                if (ELEM_PROPERTY.equals(qName)) {
+                    currentNode.addProperty(currentProperty);
+                    currentProperty = null;
+
+                } else if (ELEM_NAME.equals(qName)) {
+                    if (currentProperty != null) {
+                        currentProperty.setName(content);
+                    } else if (currentNode != null) {
+                        currentNode.setName(content);
+                    }
+
+                } else if (ELEM_VALUE.equals(qName)) {
+                    if (currentProperty.isMultiValue()) {
+                        currentProperty.addValue(content);
+                    } else {
+                        currentProperty.setValue(content);
+                    }
+
+                } else if (ELEM_VALUES.equals(qName)) {
+                    currentProperty.addValue(null);
+                    currentProperty.setValue(null);
+
+                } else if (ELEM_TYPE.equals(qName)) {
+                    currentProperty.setType(content);
+
+                } else if (ELEM_NODE.equals(qName)) {
+                    if (!nodes.isEmpty()) {
+                        NodeDescription parent = nodes.removeLast();
+                        parent.addChild(currentNode);
+                        currentNode = parent;
+                    }
+
+                } else if (ELEM_PRIMARY_NODE_TYPE.equals(qName)) {
+                    currentNode.setPrimaryNodeType(content);
+
+                } else if (ELEM_MIXIN_NODE_TYPE.equals(qName)) {
+                    currentNode.addMixinNodeType(content);
+                }
+
+                currentElement = elements.removeLast();
+
+            } else if (eventType == XmlPullParser.TEXT) {
+                contentBuffer.append(this.xmlParser.getText());
+            }
+
+            eventType = this.xmlParser.next();
+        }
+
+        return currentNode;
+    }
+}

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