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

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


The following commit(s) were added to refs/heads/master by this push:
     new d38064d  SLING-11718 : Migrate to Jakarta JSON API
d38064d is described below

commit d38064d848486d7cd7ae9fdfe42e908fca2d76a8
Author: Carsten Ziegeler <[email protected]>
AuthorDate: Fri Dec 9 08:31:20 2022 +0100

    SLING-11718 : Migrate to Jakarta JSON API
---
 pom.xml                                            |  21 +-
 .../contentloader/internal/readers/JsonReader.java |  18 +-
 .../internal/readers/OrderedJsonReader.java        |  12 +-
 .../jcr/contentloader/internal/JsonReaderTest.java |   6 +-
 .../jcr/contentloader/it/BundleContentLoader.java  | 831 +++++++++++++++++++++
 .../contentloader/it/ContentloaderTestSupport.java |   1 +
 6 files changed, 861 insertions(+), 28 deletions(-)

diff --git a/pom.xml b/pom.xml
index f4ef636..f0df8fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -243,7 +243,7 @@
         <dependency>
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
-            <version>2.4</version>
+            <version>2.11.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -272,9 +272,9 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>org.apache.geronimo.specs</groupId>
-            <artifactId>geronimo-json_1.0_spec</artifactId>
-            <version>1.0-alpha-1</version>
+            <groupId>jakarta.json</groupId>
+            <artifactId>jakarta.json-api</artifactId>
+            <version>2.0.2</version>
             <scope>provided</scope>
         </dependency>
 
@@ -311,21 +311,22 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
-            <version>3.2.2</version>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-core</artifactId>
+            <version>1.2.19</version>
+            <classifier>jakarta</classifier>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+            <artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
             <version>3.2.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.commons.johnzon</artifactId>
-            <version>1.0.0</version>
+            <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+            <version>3.2.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git 
a/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/JsonReader.java
 
b/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/JsonReader.java
index 1379c8a..7bd0617 100644
--- 
a/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/JsonReader.java
+++ 
b/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/JsonReader.java
@@ -38,14 +38,14 @@ import javax.jcr.RepositoryException;
 import javax.jcr.Value;
 import javax.jcr.ValueFactory;
 import javax.jcr.ValueFormatException;
-import javax.json.Json;
-import javax.json.JsonArray;
-import javax.json.JsonException;
-import javax.json.JsonNumber;
-import javax.json.JsonObject;
-import javax.json.JsonString;
-import javax.json.JsonValue;
-import javax.json.JsonValue.ValueType;
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonException;
+import jakarta.json.JsonNumber;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.JsonValue.ValueType;
 
 import 
org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionDefinition;
 import 
org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
@@ -169,7 +169,7 @@ public class JsonReader implements ContentReader {
         }
         Map<String, Object> config = new HashMap<>();
         config.put("org.apache.johnzon.supports-comments", true);
-        try (javax.json.JsonReader reader = 
Json.createReaderFactory(config).createReader(new 
StringReader(tickToDoubleQuote(jsonString)))) {
+        try (jakarta.json.JsonReader reader = 
Json.createReaderFactory(config).createReader(new 
StringReader(tickToDoubleQuote(jsonString)))) {
             JsonObject json = reader.readObject();
             this.createNode(null, json, contentCreator);
             contentCreator.finish();
diff --git 
a/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/OrderedJsonReader.java
 
b/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/OrderedJsonReader.java
index b833941..65ea83c 100644
--- 
a/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/OrderedJsonReader.java
+++ 
b/src/main/java/org/apache/sling/jcr/contentloader/internal/readers/OrderedJsonReader.java
@@ -21,12 +21,12 @@ package org.apache.sling.jcr.contentloader.internal.readers;
 import java.util.Map;
 
 import javax.jcr.RepositoryException;
-import javax.json.Json;
-import javax.json.JsonArray;
-import javax.json.JsonException;
-import javax.json.JsonObject;
-import javax.json.JsonObjectBuilder;
-import javax.json.JsonValue;
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonException;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonValue;
 
 import org.apache.sling.jcr.contentloader.ContentCreator;
 import org.apache.sling.jcr.contentloader.ContentReader;
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
index aa2f5df..3ee80ec 100644
--- 
a/src/test/java/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
+++ 
b/src/test/java/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java
@@ -24,9 +24,9 @@ import java.util.LinkedHashMap;
 
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonWriter;
+import jakarta.json.Json;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonWriter;
 
 import org.apache.sling.jcr.contentloader.ContentCreator;
 import org.apache.sling.jcr.contentloader.internal.readers.JsonReader;
diff --git 
a/src/test/java/org/apache/sling/jcr/contentloader/it/BundleContentLoader.java 
b/src/test/java/org/apache/sling/jcr/contentloader/it/BundleContentLoader.java
new file mode 100644
index 0000000..6167e62
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/jcr/contentloader/it/BundleContentLoader.java
@@ -0,0 +1,831 @@
+/*
+ * 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.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.StringTokenizer;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import javax.jcr.Item;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.version.VersionManager;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.jcr.contentloader.ContentReader;
+import org.apache.sling.jcr.contentloader.PathEntry;
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>BundleContentLoader</code> loads initial content from the bundle.
+ */
+public class BundleContentLoader extends BaseImportLoader {
+
+    public static final String PARENT_DESCRIPTOR = "ROOT";
+
+    private final Logger log = 
LoggerFactory.getLogger(BundleContentLoader.class);
+
+    private BundleHelper bundleHelper;
+
+    // bundles whose registration failed and should be retried
+    private List<Bundle> delayedBundles;
+
+    private final Predicate<String> pathFilter;
+
+    public BundleContentLoader(BundleHelper bundleHelper, 
ContentReaderWhiteboard contentReaderWhiteboard,
+            BundleContentLoaderConfiguration configuration) {
+        super(contentReaderWhiteboard);
+        this.bundleHelper = bundleHelper;
+        this.delayedBundles = new LinkedList<>();
+
+        List<Pattern> includes = Arrays
+                
.stream(Optional.ofNullable(configuration).map(BundleContentLoaderConfiguration::includedTargets)
+                        .orElse(new String[0]))
+                
.filter(Objects::nonNull).map(Pattern::compile).collect(Collectors.toList());
+        List<Pattern> excludes = Arrays
+                
.stream(Optional.ofNullable(configuration).map(BundleContentLoaderConfiguration::excludedTargets)
+                        .orElse(new String[0]))
+                
.filter(Objects::nonNull).map(Pattern::compile).collect(Collectors.toList());
+        this.pathFilter = path -> {
+            if (configuration == null || path == null) {
+                return true;
+            } else {
+                return includes.stream().anyMatch(p -> 
p.matcher(path).matches())
+                        && excludes.stream().noneMatch(p -> 
p.matcher(path).matches());
+            }
+        };
+        log.debug("Using includes: {} and excludes: {}", includes, excludes);
+    }
+
+    public void dispose() {
+        if (delayedBundles != null) {
+            delayedBundles.clear();
+            delayedBundles = null;
+        }
+        bundleHelper = null;
+    }
+
+    /**
+     * Retry loading bundles that have previously been delayed 
+     * @param metadataSession the JCR Session for reading/writing metadata
+     */
+    public void retryDelayedBundles(final Session metadataSession) {
+        // handle delayed bundles, might help now
+        int currentSize = -1;
+        for (int i = delayedBundles.size(); i > 0 && currentSize != 
delayedBundles.size()
+                && !delayedBundles.isEmpty(); i--) {
+            for (Iterator<Bundle> di = delayedBundles.iterator(); 
di.hasNext();) {
+                Bundle delayed = di.next();
+                if (registerBundleInternal(metadataSession, delayed, true, 
false)) {
+                    di.remove();
+                }
+            }
+            currentSize = delayedBundles.size();
+        }
+    }
+
+    /**
+     * Register a bundle and install its content.
+     *
+     * @param metadataSession the JCR Session for reading/writing metadata
+     * @param bundle the bundle to install
+     */
+    public void registerBundle(final Session metadataSession, final Bundle 
bundle, final boolean isUpdate) {
+
+        // if this is an update, we have to uninstall the old content first
+        if (isUpdate) {
+            this.unregisterBundle(metadataSession, bundle);
+        }
+
+        log.debug("Registering bundle {} for content loading.", 
bundle.getSymbolicName());
+
+        if (registerBundleInternal(metadataSession, bundle, false, isUpdate)) {
+            retryDelayedBundles(metadataSession);
+        } else if (!isUpdate) {
+            // add to delayed bundles - if this is not an update!
+            delayedBundles.add(bundle);
+        }
+    }
+
+    private boolean registerBundleInternal(final Session metadataSession, 
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 {
+            bundleHelper.createRepositoryPath(metadataSession, 
BundleContentLoaderListener.BUNDLE_CONTENT_NODE);
+
+            // check if the content has already been loaded
+            final Map<String, Object> bundleContentInfo = 
bundleHelper.getBundleContentInfo(metadataSession, bundle,
+                    true);
+
+            // if we don't get an info, someone else is currently loading
+            if (bundleContentInfo == null) {
+                return false;
+            }
+
+            boolean success = false;
+            List<String> createdNodes = null;
+            try {
+                final boolean contentAlreadyLoaded = ((Boolean) 
bundleContentInfo
+                        
.get(BundleContentLoaderListener.PROPERTY_CONTENT_LOADED)).booleanValue();
+                boolean isBundleUpdated = false;
+                Calendar lastLoadedAt = (Calendar) bundleContentInfo
+                        
.get(BundleContentLoaderListener.PROPERTY_CONTENT_LOADED_AT);
+                if (lastLoadedAt != null && lastLoadedAt.getTimeInMillis() < 
bundle.getLastModified()) {
+                    isBundleUpdated = true;
+                }
+                if (!isUpdate && !isBundleUpdated && contentAlreadyLoaded) {
+                    log.info("Content of bundle already loaded {}.", 
bundle.getSymbolicName());
+                } else {
+                    createdNodes = installContent(metadataSession, bundle, 
pathIter,
+                            contentAlreadyLoaded && !isBundleUpdated);
+                    if (isRetry) {
+                        // log success of retry
+                        log.info("Retrying to load initial content for bundle 
{} succeeded.", bundle.getSymbolicName());
+                    }
+                }
+
+                success = true;
+                return true;
+            } finally {
+                bundleHelper.unlockBundleContentInfo(metadataSession, bundle, 
success, createdNodes);
+            }
+
+        } catch (ContentReaderUnavailableException crue) {
+            // if we are retrying we already logged this message once, so we
+            // won't log it again
+            if (!isRetry) {
+                log.warn("Cannot load initial content for bundle {} : {}", 
bundle.getSymbolicName(), crue.getMessage());
+            }
+        } 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 session the session to read/write the metadata
+     * @param bundle The bundle.
+     */
+    public void unregisterBundle(final Session session, final Bundle bundle) {
+
+        if (delayedBundles.contains(bundle)) {
+            delayedBundles.remove(bundle);
+        } else {
+            try {
+                bundleHelper.createRepositoryPath(session, 
BundleContentLoaderListener.BUNDLE_CONTENT_NODE);
+
+                final Map<String, Object> bundleContentInfo = 
bundleHelper.getBundleContentInfo(session, bundle, false);
+
+                // if we don't get an info, someone else is currently loading 
or unloading
+                // or the bundle is already uninstalled
+                if (bundleContentInfo == null) {
+                    return;
+                }
+
+                try {
+                    uninstallContent(session, bundle,
+                            (String[]) 
bundleContentInfo.get(BundleContentLoaderListener.PROPERTY_UNINSTALL_PATHS));
+                    bundleHelper.contentIsUninstalled(session, bundle);
+                } finally {
+                    bundleHelper.unlockBundleContentInfo(session, bundle, 
false, null);
+                }
+            } catch (RepositoryException re) {
+                log.error("Cannot remove initial content for bundle " + 
bundle.getSymbolicName() + " : "
+                        + re.getMessage(), re);
+            }
+        }
+    }
+
+    // ---------- internal 
-----------------------------------------------------
+
+    /**
+     * Install the content from the bundle.
+     *
+     * @return If the content should be removed on uninstall, a list of top 
nodes
+     */
+    private List<String> installContent(final Session defaultSession, final 
Bundle bundle,
+            final Iterator<PathEntry> pathIter, final boolean 
contentAlreadyLoaded) throws RepositoryException, 
ContentReaderUnavailableException {
+
+        final List<String> createdNodes = new ArrayList<>();
+        final Map<String, Session> createdSessions = new HashMap<>();
+
+        log.debug("Installing initial content from bundle {}", 
bundle.getSymbolicName());
+        final DefaultContentCreator contentCreator = new 
DefaultContentCreator(this.bundleHelper);
+        try {
+            while (pathIter.hasNext()) {
+                final PathEntry pathEntry = pathIter.next();
+
+                if (!pathFilter.test(pathEntry.getTarget())) {
+                    log.debug("Path {} excluded by configuration", 
pathEntry.getPath());
+                    continue;
+                }
+
+                if (pathEntry.isOverwrite() && (pathEntry.getTarget() == null 
|| "/".equals(pathEntry.getTarget()))) {
+                    log.error("Path {} tries to overwrite on the repository 
root level which is not allowed, only use overwrite with a dedicated path 
directive having any value but '/'", pathEntry.getPath());
+                    continue;
+                }
+                if (!contentAlreadyLoaded || pathEntry.isOverwrite()) {
+                    String workspace = pathEntry.getWorkspace();
+                    final Session targetSession;
+                    if (workspace != null) {
+                        if (createdSessions.containsKey(workspace)) {
+                            targetSession = createdSessions.get(workspace);
+                        } else {
+                            targetSession = createSession(workspace);
+                            createdSessions.put(workspace, targetSession);
+                        }
+                    } else {
+                        targetSession = defaultSession;
+                    }
+
+                    final Node targetNode = getTargetNode(targetSession, 
pathEntry.getTarget(), pathEntry.isOverwrite());
+
+                    if (targetNode != null) {
+                        installFromPath(bundle, pathEntry.getPath(), 
pathEntry, targetNode,
+                                pathEntry.isUninstall() ? createdNodes : null, 
contentCreator);
+                    }
+                }
+            }
+
+            // now optimize created nodes list
+            Collections.sort(createdNodes);
+            if (createdNodes.size() > 1) {
+                final Iterator<String> i = createdNodes.iterator();
+                String previous = i.next() + '/';
+                while (i.hasNext()) {
+                    final String current = i.next();
+                    if (current.startsWith(previous)) {
+                        i.remove();
+                    } else {
+                        previous = current + '/';
+                    }
+                }
+            }
+
+            // persist modifications now
+            defaultSession.refresh(true);
+            defaultSession.save();
+
+            for (Session session : createdSessions.values()) {
+                session.refresh(true);
+                session.save();
+            }
+
+            // finally check in versionable nodes
+            for (final Node versionable : contentCreator.getVersionables()) {
+                VersionManager versionManager = 
versionable.getSession().getWorkspace().getVersionManager();
+                versionManager.checkin(versionable.getPath());
+            }
+        } finally {
+            try {
+                if (defaultSession.hasPendingChanges()) {
+                    defaultSession.refresh(false);
+                }
+                for (Session session : createdSessions.values()) {
+                    if (session.hasPendingChanges()) {
+                        session.refresh(false);
+                    }
+                }
+            } catch (RepositoryException re) {
+                log.warn("Failure to rollback partial initial content for 
bundle {}", bundle.getSymbolicName(), re);
+            }
+            contentCreator.clear();
+            for (Session session : createdSessions.values()) {
+                session.logout();
+            }
+        }
+        log.debug("Done installing initial content from bundle {}", 
bundle.getSymbolicName());
+
+        return createdNodes;
+    }
+
+    /**
+     * Handle content installation for a single path.
+     *
+     * @param bundle        The bundle containing the content.
+     * @param path          The path
+     * @param configuration
+     * @param parent        The parent node.
+     * @param createdNodes  An optional list to store all new nodes. This list 
is
+     *                      used for an uninstall
+     * @throws RepositoryException
+     */
+    private void installFromPath(final Bundle bundle, final String path, final 
PathEntry configuration,
+            final Node parent, final List<String> createdNodes, final 
DefaultContentCreator contentCreator)
+            throws RepositoryException, ContentReaderUnavailableException {
+
+        // init content creator
+        contentCreator.init(configuration, getContentReaders(), createdNodes, 
null);
+
+        final Map<String, Node> processedEntries = new HashMap<>();
+
+        Enumeration<String> entries = bundle.getEntryPaths(path);
+        if (entries == null) {
+            // check for single content
+            final URL u = bundle.getEntry(path);
+            if (u == null) {
+                log.info("install: No initial content entries at {} in bundle 
{}", path, bundle.getSymbolicName());
+                return;
+            }
+            // we have a single file content -> this should replace the target 
node fully, i.e. parent is one level above
+            handleFile(path, bundle, processedEntries, configuration, 
parent.getParent(), createdNodes, contentCreator);
+            return;
+        }
+
+        // potential parent node import/extension
+        URL parentNodeDescriptor = importParentNode(bundle, path, parent, 
contentCreator);
+        if (parentNodeDescriptor != null) {
+            processedEntries.put(parentNodeDescriptor.toString(), parent);
+        }
+
+        while (entries.hasMoreElements()) {
+            final String entry = entries.nextElement();
+            log.debug("Processing initial content entry {} in bundle {}", 
entry, bundle.getSymbolicName());
+            if (entry.endsWith("/")) {
+
+                // dir, check for node descriptor, else create dir
+                final String base = entry.substring(0, entry.length() - 1);
+
+                URL nodeDescriptor = null;
+                for (String ext : contentCreator.getContentReaders().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)
+                final String name = getName(base);
+                Node node = null;
+                if (nodeDescriptor != null) {
+                    node = processedEntries.get(nodeDescriptor.toString());
+                    if (node == null) {
+                        node = createNode(parent, name, nodeDescriptor, 
contentCreator, configuration);
+                        processedEntries.put(nodeDescriptor.toString(), node);
+                    }
+                } else {
+                    node = createFolder(parent, name, 
configuration.isOverwrite());
+                }
+
+                // walk down the line
+                if (node != null) {
+                    installFromPath(bundle, entry, configuration, node, 
createdNodes, contentCreator);
+                }
+
+            } else {
+                // file => create file
+                handleFile(entry, bundle, processedEntries, configuration, 
parent, createdNodes, contentCreator);
+            }
+        }
+    }
+
+    /**
+     * Handle a file entry.
+     *
+     * @param entry
+     * @param bundle
+     * @param processedEntries
+     * @param configuration
+     * @param parent
+     * @param createdNodes
+     * @throws RepositoryException
+     */
+    private void handleFile(final String entry, final Bundle bundle, final 
Map<String, Node> processedEntries,
+            final PathEntry configuration, final Node parent, final 
List<String> createdNodes,
+            final DefaultContentCreator contentCreator) throws 
RepositoryException, ContentReaderUnavailableException {
+
+        final URL file = bundle.getEntry(entry);
+        final String name = getName(entry);
+        try {
+            if (processedEntries.containsKey(file.toString())) {
+                // this is a consumed node descriptor
+                return;
+            }
+
+            // check for node descriptor
+            URL nodeDescriptor = null;
+            for (String ext : contentCreator.getContentReaders().keySet()) {
+                nodeDescriptor = bundle.getEntry(entry + ext);
+                if (nodeDescriptor != null) {
+                    break;
+                }
+            }
+
+            // install if it is a descriptor
+            boolean foundReader = getContentReader(entry, configuration) != 
null;
+
+            Node node = null;
+            if (foundReader) {
+                node = createNode(parent, name, file, contentCreator, 
configuration);
+                if (node != null) {
+                    log.debug("Created node as {} {}", node.getPath(), name);
+                    processedEntries.put(file.toString(), node);
+                } else {
+                    log.warn("No node created for file {} {}", file, name);
+                }
+            } else {
+                // if we require a ContentReader for this entry but didn't 
find one
+                //   then throw an exception to stop processing this bundle 
and put 
+                //   it into the delayedBundles list to retry later
+                if (configuration.isImportProviderRequired(name)) {
+                    throw new 
ContentReaderUnavailableException(String.format("Unable to locate a required 
content reader for entry %s", entry));
+                } else {
+                    log.debug("Can't find content reader for entry {} at {}", 
entry, name);
+                }
+            }
+
+            // otherwise just place as file
+            if (node == null) {
+                try {
+                    createFile(configuration, parent, file, createdNodes, 
contentCreator);
+                    node = parent.getNode(name);
+                } catch (IOException ioe) {
+                    log.warn("Cannot create file node for {}", file, ioe);
+                }
+            }
+            // if we have a descriptor, which has not been processed yet,
+            // process it
+            if (nodeDescriptor != null && 
!processedEntries.containsKey(nodeDescriptor.toString())) {
+                try {
+                    contentCreator.setIgnoreOverwriteFlag(true);
+                    node = createNode(parent, name, nodeDescriptor, 
contentCreator, configuration);
+                    processedEntries.put(nodeDescriptor.toString(), node);
+                } finally {
+                    contentCreator.setIgnoreOverwriteFlag(false);
+                }
+            }
+        } catch (RepositoryException e) {
+            log.error("jakarta.json process file {} from {}", file, name, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 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 contentCreator the content creator
+     * @param configuration  the configuration for the node that needs to be 
created
+     * @return
+     * @throws RepositoryException
+     */
+    private Node createNode(Node parent, String name, URL resourceUrl, final 
DefaultContentCreator contentCreator,
+            PathEntry configuration) throws RepositoryException {
+
+        final String resourcePath = resourceUrl.getPath().toLowerCase();
+        InputStream contentStream = null;
+        try {
+            // special treatment for system view imports
+            if (resourcePath.endsWith(EXT_JCR_XML)) {
+                contentStream = resourceUrl.openStream();
+                return importJcrXml(parent, name, contentStream, false);
+            }
+
+            // get the node reader for this resource
+            final ContentReader nodeReader = getContentReader(resourcePath, 
configuration);
+
+            // cannot find out the type
+            if (nodeReader == null) {
+                return null;
+            }
+
+            final String contentReaderExtension = 
getContentReaderExtension(name);
+            contentCreator.prepareParsing(parent, toPlainName(name, 
contentReaderExtension));
+            nodeReader.parse(resourceUrl, contentCreator);
+
+            return contentCreator.getCreatedRootNode();
+        } catch (RepositoryException re) {
+            throw re;
+        } catch (Exception t) {
+            throw new RepositoryException(t.getMessage(), t);
+        } finally {
+            IOUtils.closeQuietly(contentStream);
+        }
+    }
+
+    /**
+     * 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, "sling:Folder");
+    }
+
+    /**
+     * Create a file from the given url.
+     *
+     * @param configuration
+     * @param parent
+     * @param source
+     * @param createdNodes
+     * @param contentCreator
+     * @throws IOException
+     * @throws RepositoryException
+     */
+    private void createFile(PathEntry configuration, Node parent, URL source, 
List<String> createdNodes,
+            final DefaultContentCreator contentCreator) throws IOException, 
RepositoryException {
+
+        final String srcPath = source.getPath();
+        int pos = srcPath.lastIndexOf('/');
+        final String name = getName(source.getPath());
+        final String path;
+        if (pos == -1) {
+            path = name;
+        } else {
+            path = srcPath.substring(0, pos + 1) + name;
+        }
+
+        contentCreator.init(configuration, getContentReaders(), createdNodes, 
null);
+        contentCreator.prepareParsing(parent, name);
+        final URLConnection conn = source.openConnection();
+        final long lastModified = Math.min(conn.getLastModified(), 
configuration.getLastModified());
+        final String type = conn.getContentType();
+        final InputStream data = conn.getInputStream();
+        contentCreator.createFileAndResourceNode(path, data, type, 
lastModified);
+        contentCreator.finishNode();
+        contentCreator.finishNode();
+    }
+
+    /**
+     * Gets and decodes 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.net.URLDecoder</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 + " because 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 getTargetNode(Session session, String path, boolean 
overwrite) throws RepositoryException {
+
+        // not specified path directive
+        if (path == null) {
+            return session.getRootNode();
+        }
+
+        if (!path.startsWith("/")) {
+            // make relative path absolute
+            path = "/" + path;
+        }
+
+        if (!session.itemExists(path)) {
+            Node currentNode = session.getRootNode();
+            final StringTokenizer st = new StringTokenizer(path.substring(1), 
"/");
+            while (st.hasMoreTokens()) {
+                final String name = st.nextToken();
+                if (!currentNode.hasNode(name)) {
+                    currentNode.addNode(name, "sling:Folder");
+                }
+                currentNode = currentNode.getNode(name);
+            }
+            return currentNode;
+        } else {
+            Item item = session.getItem(path);
+            if (!item.isNode()) {
+                log.warn("Cannot use item as target path {} as this is an 
existing property", path);
+                return null;
+            }
+            Node targetNode = session.getNode(path);
+            // overwrite target node itself?
+            if (overwrite) {
+                targetNode = createFolder(targetNode.getParent(), 
targetNode.getName(), true);
+            }
+            return targetNode;
+        }
+    }
+
+    private void uninstallContent(final Session defaultSession, final Bundle 
bundle, final String[] uninstallPaths) {
+
+        final Map<String, Session> createdSessions = new HashMap<>();
+
+        try {
+            log.debug("Uninstalling initial content from bundle {}", 
bundle.getSymbolicName());
+            if (uninstallPaths != null && uninstallPaths.length > 0) {
+                for (String path : uninstallPaths) {
+                    if (!pathFilter.test(path)) {
+                        log.debug("Path {} excluded by configuration", path);
+                        continue;
+                    }
+                    final Session targetSession;
+
+                    final int wsSepPos = path.indexOf(":/");
+                    if (wsSepPos != -1) {
+                        final String workspaceName = path.substring(0, 
wsSepPos);
+                        path = path.substring(wsSepPos + 1);
+                        if 
(workspaceName.equals(defaultSession.getWorkspace().getName())) {
+                            targetSession = defaultSession;
+                        } else if (createdSessions.containsKey(workspaceName)) 
{
+                            targetSession = createdSessions.get(workspaceName);
+                        } else {
+                            targetSession = createSession(workspaceName);
+                            createdSessions.put(workspaceName, targetSession);
+                        }
+                    } else {
+                        targetSession = defaultSession;
+                    }
+
+                    if (targetSession.itemExists(path)) {
+                        targetSession.getItem(path).remove();
+                    }
+                }
+
+                // persist modifications now
+                defaultSession.save();
+
+                for (Session session : createdSessions.values()) {
+                    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 (defaultSession.hasPendingChanges()) {
+                    defaultSession.refresh(false);
+                }
+                for (Session session : createdSessions.values()) {
+                    if (session.hasPendingChanges()) {
+                        session.refresh(false);
+                    }
+                }
+            } catch (RepositoryException re) {
+                log.warn("Failure to rollback uninstalling initial content for 
bundle {}", bundle.getSymbolicName(),
+                        re);
+            }
+
+            for (Session session : createdSessions.values()) {
+                session.logout();
+            }
+        }
+    }
+
+    protected static final class Descriptor {
+
+        public URL url;
+
+        private ContentReader contentReader;
+
+    }
+
+    /**
+     * Return the parent node descriptor (ROOT).
+     */
+    private Descriptor getParentNodeDescriptor(final Bundle bundle, final 
String path,
+            final DefaultContentCreator contentCreator) {
+
+        for (Map.Entry<String, ContentReader> entry : 
contentCreator.getContentReaders().entrySet()) {
+            if (entry.getValue() != null) {
+                final StringBuilder filePath = new StringBuilder(path);
+                if (!path.endsWith("/")) {
+                    filePath.append("/");
+                }
+                filePath.append(PARENT_DESCRIPTOR);
+                // add file extension, e.g. .jcr.xml, .xml, .zip (see 
BaseImportLoader)
+                filePath.append(entry.getKey());
+                URL url = bundle.getEntry(filePath.toString());
+                if (url != null) {
+                    final Descriptor descriptor = new Descriptor();
+                    descriptor.url = url;
+                    descriptor.contentReader = entry.getValue();
+                    return descriptor;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Imports mixin nodes and properties (and optionally child nodes) of the 
parent
+     * node.
+     */
+    private URL importParentNode(Bundle bundle, String path, Node parent, 
final DefaultContentCreator contentCreator)
+            throws RepositoryException {
+
+        final Descriptor descriptor = getParentNodeDescriptor(bundle, path, 
contentCreator);
+        // no parent descriptor (ROOT) found
+        if (descriptor == null) {
+            return null;
+        }
+
+        try {
+            contentCreator.prepareParsing(parent, null);
+            descriptor.contentReader.parse(descriptor.url, contentCreator);
+            return descriptor.url;
+        } catch (RepositoryException re) {
+            throw re;
+        } catch (Exception t) {
+            throw new RepositoryException(t.getMessage(), t);
+        }
+    }
+
+    private Session createSession(String workspace) throws RepositoryException 
{
+        try {
+            return bundleHelper.getSession(workspace);
+        } catch (NoSuchWorkspaceException e) {
+            Session temp = bundleHelper.getSession();
+            temp.getWorkspace().createWorkspace(workspace);
+            temp.logout();
+            return bundleHelper.getSession(workspace);
+        }
+    }
+
+}
diff --git 
a/src/test/java/org/apache/sling/jcr/contentloader/it/ContentloaderTestSupport.java
 
b/src/test/java/org/apache/sling/jcr/contentloader/it/ContentloaderTestSupport.java
index 99155d3..6a3bee2 100644
--- 
a/src/test/java/org/apache/sling/jcr/contentloader/it/ContentloaderTestSupport.java
+++ 
b/src/test/java/org/apache/sling/jcr/contentloader/it/ContentloaderTestSupport.java
@@ -109,6 +109,7 @@ public abstract class ContentloaderTestSupport extends 
TestSupport {
         final Option contentloader = 
mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.jcr.contentloader").version(SlingOptions.versionResolver.getVersion("org.apache.sling",
 "org.apache.sling.jcr.contentloader"));
         return composite(
             super.baseConfiguration(),
+            
mavenBundle().groupId("org.glassfish").artifactId("jakarta.json").version("2.0.1"),
             quickstart(),
             // SLING-9735 - add server user for the o.a.s.jcr.contentloader 
bundle
             
factoryConfiguration("org.apache.sling.jcr.repoinit.RepositoryInitializer")


Reply via email to