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")