Merge branch 'master' into SLING-11229-SLING-11230
diff --git a/pom.xml b/pom.xml
index 222acee..8aeb840 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
</parent>
<artifactId>org.apache.sling.jcr.resource</artifactId>
- <version>3.1.1-SNAPSHOT</version>
+ <version>3.2.1-SNAPSHOT</version>
<name>Apache Sling JCR Resource</name>
<description>
@@ -44,8 +44,8 @@
<properties>
<site.jira.version.id>12314286</site.jira.version.id>
<site.javadoc.exclude>**.internal.**</site.javadoc.exclude>
- <oak.version>1.5.15</oak.version>
- <jackrabbit.version>2.13.4</jackrabbit.version>
+ <oak.version>1.10.0</oak.version><!-- first version compatible with Jackrabbit API 2.18 (https://issues.apache.org/jira/browse/OAK-7943) -->
+ <jackrabbit.version>2.18.0</jackrabbit.version><!-- required for direct binary access, https://issues.apache.org/jira/browse/JCR-4335 -->
<project.build.outputTimestamp>1</project.build.outputTimestamp>
</properties>
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/JcrModifiableValueMap.java b/src/main/java/org/apache/sling/jcr/resource/internal/JcrModifiableValueMap.java
index 445edb1..d28afdd 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/JcrModifiableValueMap.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/JcrModifiableValueMap.java
@@ -25,6 +25,7 @@
import javax.jcr.RepositoryException;
import javax.jcr.Value;
+import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.jcr.resource.internal.helper.JcrPropertyMapCacheEntry;
@@ -62,7 +63,7 @@
final JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(value, this.node);
this.cache.put(key, entry);
final String name = escapeKeyName(key);
- if ( NodeUtil.MIXIN_TYPES.equals(name) ) {
+ if ( JcrConstants.JCR_MIXINTYPES.equals(name) ) {
NodeUtil.handleMixinTypes(node, entry.convertToType(String[].class, node, this.helper.getDynamicClassLoader()));
} else if ( "jcr:primaryType".equals(name) ) {
node.setPrimaryType(entry.convertToType(String.class, node, this.helper.getDynamicClassLoader()));
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/NodeUtil.java b/src/main/java/org/apache/sling/jcr/resource/internal/NodeUtil.java
index fa2d348..df852a1 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/NodeUtil.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/NodeUtil.java
@@ -18,21 +18,27 @@
*/
package org.apache.sling.jcr.resource.internal;
+import static javax.jcr.Property.JCR_CONTENT;
+import static javax.jcr.Property.JCR_DATA;
+import static javax.jcr.Property.JCR_FROZEN_PRIMARY_TYPE;
+import static javax.jcr.nodetype.NodeType.NT_FILE;
+import static javax.jcr.nodetype.NodeType.NT_FROZEN_NODE;
+import static javax.jcr.nodetype.NodeType.NT_LINKED_FILE;
+
import java.util.HashSet;
import java.util.Set;
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
+import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
+import org.jetbrains.annotations.NotNull;
+
public abstract class NodeUtil {
- /** Property for the mixin node types. */
- public static final String MIXIN_TYPES = "jcr:mixinTypes";
-
- /** Property for the node type. */
- public static final String NODE_TYPE = "jcr:primaryType";
-
/**
* Update the mixin node types
*
@@ -63,4 +69,36 @@
node.addMixin(name);
}
}
+
+ /**
+ * Returns the primary property of the given node. For {@code nt:file} nodes this is a property of the child node {@code jcr:content}.
+ * In case the node has a {@code jcr:data} property it is returned, otherwise the node's primary item as specified by its node type recursively until a property is found .
+ *
+ * @param node the node for which to return the primary property
+ * @return the primary property of the given node
+ * @throws ItemNotFoundException in case the given node does neither have a {@code jcr:data} property nor a primary property given through its node type
+ * @throws RepositoryException in case some exception occurs
+ */
+ public static @NotNull Property getPrimaryProperty(@NotNull Node node) throws RepositoryException {
+ // find the content node: for nt:file it is jcr:content
+ // otherwise it is the node of this resource
+ Node content = (node.isNodeType(NT_FILE) ||
+ (node.isNodeType(NT_FROZEN_NODE) &&
+ node.getProperty(JCR_FROZEN_PRIMARY_TYPE).getString().equals(NT_FILE)))
+ ? node.getNode(JCR_CONTENT)
+ : node.isNodeType(NT_LINKED_FILE) ? node.getProperty(JCR_CONTENT).getNode() : node;
+ Property data;
+ // if the node has a jcr:data property, use that property
+ if (content.hasProperty(JCR_DATA)) {
+ data = content.getProperty(JCR_DATA);
+ } else {
+ // otherwise try to follow default item trail
+ Item item = content.getPrimaryItem();
+ while (item.isNode()) {
+ item = ((Node) item).getPrimaryItem();
+ }
+ data = (Property) item;
+ }
+ return data;
+ }
}
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProvider.java
new file mode 100644
index 0000000..3aa5abb
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProvider.java
@@ -0,0 +1,145 @@
+/*
+ * 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.resource.internal.helper.jcr;
+
+import java.net.URI;
+
+import javax.jcr.Binary;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.ValueFormatException;
+
+import org.apache.jackrabbit.api.binary.BinaryDownload;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions.BinaryDownloadOptionsBuilder;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.external.URIProvider;
+import org.apache.sling.jcr.resource.internal.NodeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.propertytypes.ServiceRanking;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Provides URIs for direct binary read-access based on the Jackrabbit API {@link BinaryDownload}.
+ *
+ * @see <a href="https://jackrabbit.apache.org/oak/docs/features/direct-binary-access.html">Oak Direct Binary Access</a>
+ *
+ */
+@Component(service = URIProvider.class, configurationPolicy = ConfigurationPolicy.REQUIRE)
+@ServiceRanking(-100)
+@Designate(ocd = BinaryDownloadUriProvider.Configuration.class)
+public class BinaryDownloadUriProvider implements URIProvider {
+
+ enum ContentDisposition {
+ INLINE,
+ ATTACHMENT
+ }
+
+ @ObjectClassDefinition(
+ name = "Apache Sling Binary Download URI Provider",
+ description = "Provides URIs for resources containing a primary JCR binary property backed by a blob store allowing direct HTTP access")
+ public static @interface Configuration {
+ @AttributeDefinition(
+ name = "Content-Disposition",
+ description = "The content-disposition header to send when the binary is delivered via HTTP")
+ ContentDisposition contentDisposition();
+ }
+
+ private final boolean isContentDispositionAttachment;
+
+ @Activate
+ public BinaryDownloadUriProvider(Configuration configuration) {
+ this(configuration.contentDisposition() == ContentDisposition.ATTACHMENT);
+ }
+
+ BinaryDownloadUriProvider(boolean isContentDispositionAttachment) {
+ this.isContentDispositionAttachment = isContentDispositionAttachment;
+ }
+
+ @Override
+ public @NotNull URI toURI(@NotNull Resource resource, @NotNull Scope scope, @NotNull Operation operation) {
+ if (!isRelevantScopeAndOperation(scope, operation)) {
+ throw new IllegalArgumentException("This provider only provides URIs for 'READ' operations in scope 'PUBLIC' or 'EXTERNAL', but not for scope '" + scope + "' and operation '" + operation + "'");
+ }
+ Node node = resource.adaptTo(Node.class);
+ if (node == null) {
+ throw new IllegalArgumentException("This provider only provides URIs for node-based resources");
+ }
+ try {
+ // get main property (probably containing binary data)
+ Property primaryProperty = getPrimaryProperty(node);
+ try {
+ return getUriFromProperty(resource, node, primaryProperty);
+ } catch (RepositoryException e) {
+ throw new IllegalArgumentException("Error getting URI for property '" + primaryProperty.getPath() + "'", e);
+ }
+ } catch (ItemNotFoundException e) {
+ throw new IllegalArgumentException("Node does not have a primary property", e);
+ } catch (RepositoryException e) {
+ throw new IllegalArgumentException("Error accessing primary property", e);
+ }
+ }
+
+ protected @NotNull Property getPrimaryProperty(@NotNull Node node) throws RepositoryException {
+ return NodeUtil.getPrimaryProperty(node);
+ }
+
+ private boolean isRelevantScopeAndOperation(@NotNull Scope scope, @NotNull Operation operation) {
+ return ((Scope.PUBLIC.equals(scope) || Scope.EXTERNAL.equals(scope)) && Operation.READ.equals(operation));
+ }
+
+ private @NotNull URI getUriFromProperty(@NotNull Resource resource, @NotNull Node node, @NotNull Property binaryProperty) throws ValueFormatException, RepositoryException {
+ Binary binary = binaryProperty.getBinary();
+ if (!(binary instanceof BinaryDownload)) {
+ binary.dispose();
+ throw new IllegalArgumentException("The property " + binaryProperty.getPath() + " is not backed by a blob store allowing direct HTTP access");
+ }
+ BinaryDownload binaryDownload = BinaryDownload.class.cast(binary);
+ try {
+ String encoding = resource.getResourceMetadata().getCharacterEncoding();
+ String fileName = node.getName();
+ String mediaType = resource.getResourceMetadata().getContentType();
+ BinaryDownloadOptionsBuilder optionsBuilder = BinaryDownloadOptions.builder().withFileName(fileName);
+ if (encoding != null) {
+ optionsBuilder.withCharacterEncoding(encoding);
+ }
+ if (mediaType != null) {
+ optionsBuilder.withMediaType(mediaType);
+ }
+ if (isContentDispositionAttachment) {
+ optionsBuilder.withDispositionTypeAttachment();
+ } else {
+ optionsBuilder.withDispositionTypeInline();
+ }
+ URI uri = binaryDownload.getURI(optionsBuilder.build());
+ if (uri == null) {
+ throw new IllegalArgumentException("Cannot provide url for downloading the binary property at '" + binaryProperty.getPath() + "'");
+ }
+ return uri;
+ } finally {
+ binaryDownload.dispose();
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrNodeResource.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrNodeResource.java
index 4ff50af..2e21900 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrNodeResource.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrNodeResource.java
@@ -16,13 +16,6 @@
*/
package org.apache.sling.jcr.resource.internal.helper.jcr;
-import static javax.jcr.Property.JCR_CONTENT;
-import static javax.jcr.Property.JCR_DATA;
-import static javax.jcr.nodetype.NodeType.NT_FILE;
-import static javax.jcr.nodetype.NodeType.NT_LINKED_FILE;
-import static javax.jcr.nodetype.NodeType.NT_FROZEN_NODE;
-import static javax.jcr.Property.JCR_FROZEN_PRIMARY_TYPE;
-
import java.io.InputStream;
import java.net.URI;
import java.security.AccessControlException;
@@ -32,8 +25,10 @@
import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
+import javax.jcr.ValueFormatException;
import org.apache.sling.adapter.annotations.Adaptable;
import org.apache.sling.adapter.annotations.Adapter;
@@ -47,6 +42,8 @@
import org.apache.sling.jcr.resource.internal.HelperData;
import org.apache.sling.jcr.resource.internal.JcrModifiableValueMap;
import org.apache.sling.jcr.resource.internal.JcrValueMap;
+import org.apache.sling.jcr.resource.internal.NodeUtil;
+import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -181,34 +178,15 @@
final Node node = getNode();
if (node != null) {
try {
- // find the content node: for nt:file it is jcr:content
- // otherwise it is the node of this resource
- Node content = (node.isNodeType(NT_FILE) ||
- (node.isNodeType(NT_FROZEN_NODE) &&
- node.getProperty(JCR_FROZEN_PRIMARY_TYPE).getString().equals(NT_FILE)))
- ? node.getNode(JCR_CONTENT)
- : node.isNodeType(NT_LINKED_FILE) ? node.getProperty(JCR_CONTENT).getNode() : node;
-
Property data;
-
- // if the node has a jcr:data property, use that property
- if (content.hasProperty(JCR_DATA)) {
- data = content.getProperty(JCR_DATA);
- } else {
- // otherwise try to follow default item trail
- try {
- Item item = content.getPrimaryItem();
- while (item.isNode()) {
- item = ((Node) item).getPrimaryItem();
- }
- data = (Property) item;
-
- } catch (ItemNotFoundException infe) {
- // we don't actually care, but log for completeness
- LOGGER.debug("getInputStream: No primary items for {}", toString(), infe);
- data = null;
- }
+ try {
+ data = NodeUtil.getPrimaryProperty(node);
+ } catch (ItemNotFoundException infe) {
+ // we don't actually care, but log for completeness
+ LOGGER.debug("getInputStream: No primary items for {}", toString(), infe);
+ data = null;
}
+
URI uri = convertToPublicURI();
if ( uri != null ) {
return new JcrExternalizableInputStream(data, uri);
@@ -235,12 +213,9 @@
private URI convertToPublicURI() {
for (URIProvider up : helper.getURIProviders()) {
try {
- URI uri = up.toURI(this, URIProvider.Scope.EXTERNAL, URIProvider.Operation.READ);
- if ( uri != null ) {
- return uri;
- }
+ return up.toURI(this, URIProvider.Scope.EXTERNAL, URIProvider.Operation.READ);
} catch (IllegalArgumentException e) {
- LOGGER.debug(up.getClass().toString()+" declined toURI ", e);
+ LOGGER.debug("{} declined toURI for resource '{}'", up.getClass(), getPath(), e);
}
}
return null;
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
index 16e39db..d7809d6 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
@@ -22,12 +22,14 @@
import java.io.IOException;
import java.security.Principal;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import javax.jcr.Item;
@@ -36,6 +38,7 @@
import javax.jcr.RepositoryException;
import javax.jcr.Session;
+import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
@@ -94,13 +97,13 @@
/** Logger */
private final Logger logger = LoggerFactory.getLogger(JcrResourceProvider.class);
- private static final String REPOSITORY_REFERNENCE_NAME = "repository";
+ private static final String REPOSITORY_REFERENCE_NAME = "repository";
- private static final Set<String> IGNORED_PROPERTIES = new HashSet<String>();
+ private static final Set<String> IGNORED_PROPERTIES = new HashSet<>();
static {
- IGNORED_PROPERTIES.add(NodeUtil.MIXIN_TYPES);
- IGNORED_PROPERTIES.add(NodeUtil.NODE_TYPE);
- IGNORED_PROPERTIES.add("jcr:created");
+ IGNORED_PROPERTIES.add(JcrConstants.JCR_MIXINTYPES);
+ IGNORED_PROPERTIES.add(JcrConstants.JCR_PRIMARYTYPE);
+ IGNORED_PROPERTIES.add(JcrConstants.JCR_CREATED);
IGNORED_PROPERTIES.add("jcr:createdBy");
}
@@ -113,8 +116,8 @@
@AttributeDefinition(name = "Default Query Limit", description = "The default query limit for queries using the findResources methods")
long default_query_limit() default 10000L;
}
-
- @Reference(name = REPOSITORY_REFERNENCE_NAME, service = SlingRepository.class)
+
+ @Reference(name = REPOSITORY_REFERENCE_NAME, service = SlingRepository.class)
private ServiceReference<SlingRepository> repositoryReference;
/** The JCR listener base configuration. */
@@ -123,21 +126,25 @@
/** The JCR observation listeners. */
private final Map<ObserverConfiguration, Closeable> listeners = new HashMap<>();
- private final Map<URIProvider, URIProvider> providers = new ConcurrentHashMap<URIProvider, URIProvider>();
+ /**
+ * Map of bound URIProviders sorted by service ranking in descending order (highest ranking first).
+ * Key = service reference, value = service implementation
+ */
+ private final SortedMap<ServiceReference<URIProvider>, URIProvider> providers = Collections.synchronizedSortedMap(new TreeMap<>(Collections.reverseOrder()));
private volatile SlingRepository repository;
private volatile JcrProviderStateFactory stateFactory;
private Config config;
+
+ private final AtomicReference<DynamicClassLoaderManager> classLoaderManagerReference = new AtomicReference<>();
- private final AtomicReference<DynamicClassLoaderManager> classLoaderManagerReference = new AtomicReference<DynamicClassLoaderManager>();
-
- private AtomicReference<URIProvider[]> uriProviderReference = new AtomicReference<URIProvider[]>();
+ private AtomicReference<URIProvider[]> uriProviderReference = new AtomicReference<>();
@Activate
protected void activate(final ComponentContext context, final Config config) throws RepositoryException {
- SlingRepository repository = context.locateService(REPOSITORY_REFERNENCE_NAME,
+ SlingRepository repository = context.locateService(REPOSITORY_REFERENCE_NAME,
this.repositoryReference);
if (repository == null) {
// concurrent unregistration of SlingRepository service
@@ -179,13 +186,13 @@
bind = "bindUriProvider",
unbind = "unbindUriProvider"
)
- private void bindUriProvider(URIProvider uriProvider) {
-
- providers.put(uriProvider, uriProvider);
+ private void bindUriProvider(ServiceReference<URIProvider> srUriProvider, URIProvider uriProvider) {
+ providers.put(srUriProvider, uriProvider);
updateURIProviders();
}
- private void unbindUriProvider(URIProvider uriProvider) {
- providers.remove(uriProvider);
+
+ private void unbindUriProvider(ServiceReference<URIProvider> srUriProvider) {
+ providers.remove(srUriProvider);
updateURIProviders();
}
@@ -422,7 +429,7 @@
public Resource create(final @NotNull ResolveContext<JcrProviderState> ctx, final String path, final Map<String, Object> properties)
throws PersistenceException {
// check for node type
- final Object nodeObj = (properties != null ? properties.get(NodeUtil.NODE_TYPE) : null);
+ final Object nodeObj = (properties != null ? properties.get(JcrConstants.JCR_PRIMARYTYPE) : null);
// check for sling:resourcetype
final String nodeType;
if ( nodeObj != null ) {
@@ -471,9 +478,9 @@
// create modifiable map
final JcrModifiableValueMap jcrMap = new JcrModifiableValueMap(node, ctx.getProviderState().getHelperData());
// check mixin types first
- final Object value = properties.get(NodeUtil.MIXIN_TYPES);
+ final Object value = properties.get(JcrConstants.JCR_MIXINTYPES);
if ( value != null ) {
- jcrMap.put(NodeUtil.MIXIN_TYPES, value);
+ jcrMap.put(JcrConstants.JCR_MIXINTYPES, value);
}
for(final Map.Entry<String, Object> entry : properties.entrySet()) {
if ( !IGNORED_PROPERTIES.contains(entry.getKey()) ) {
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/JcrResourceListenerTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/JcrResourceListenerTest.java
index 7bf62d9..d491210 100644
--- a/src/test/java/org/apache/sling/jcr/resource/internal/JcrResourceListenerTest.java
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/JcrResourceListenerTest.java
@@ -269,7 +269,7 @@
session.save();
}
System.out.println("Events = " + events);
- assertEquals("Received: " + events, 7, events.size());
+ assertEquals("Received: " + events, 6, events.size());
final Set<String> addPaths = new HashSet<String>();
final Set<String> modifyPaths = new HashSet<String>();
final Set<String> removePaths = new HashSet<String>();
@@ -294,12 +294,9 @@
assertTrue("Modified set should contain /libs/" + rootName, modifyPaths.contains("/libs/" + rootName));
assertTrue("Modified set should contain /apps/" + rootName, modifyPaths.contains("/apps/" + rootName));
- // The OakEventFilter is using withIncludeAncestorsRemove, so we get also "removed"
- // events for all ancestors of /apps and /libs;
- assertEquals("Received: " + removePaths, 3, removePaths.size());
+ assertEquals("Received: " + removePaths, 2, removePaths.size());
assertTrue("Removed set should contain /libs/" + rootName, removePaths.contains("/libs/" + rootName));
assertTrue("Removed set should contain /apps/" + rootName, removePaths.contains("/apps/" + rootName));
- assertTrue("Removed set should contain /" + rootName, removePaths.contains("/" + rootName));
}
}
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProviderTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProviderTest.java
new file mode 100644
index 0000000..00d1ba5
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/BinaryDownloadUriProviderTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.resource.internal.helper.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.ValueFormatException;
+
+import org.apache.jackrabbit.api.binary.BinaryDownload;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.external.URIProvider.Operation;
+import org.apache.sling.api.resource.external.URIProvider.Scope;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BinaryDownloadUriProviderTest {
+
+ @Rule
+ public final SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ private Session session;
+ private BinaryDownloadUriProvider uriProvider;
+ private Resource fileResource;
+
+ @Mock
+ private BinaryDownload binaryDownload;
+
+ @Mock
+ private Property property;
+
+ @Before
+ public void setUp() throws IOException, RepositoryException {
+ uriProvider = new BinaryDownloadUriProvider(false);
+ session = context.resourceResolver().adaptTo(Session.class);
+ try (InputStream input = this.getClass().getResourceAsStream("/SLING-INF/nodetypes/folder.cnd")) {
+ JcrUtils.putFile(session.getRootNode(), "test", "myMimeType", input);
+ }
+ fileResource = context.resourceResolver().getResource("/test");
+ }
+
+ @Test
+ public void testMockedProperty() throws ValueFormatException, RepositoryException, URISyntaxException {
+ uriProvider = new BinaryDownloadUriProvider(false) {
+ @Override
+ protected Property getPrimaryProperty(Node node) throws RepositoryException {
+ return property;
+ }
+ };
+ Mockito.when(property.getBinary()).thenReturn(binaryDownload);
+ URI myUri = new URI("https://example.com/mybinary");
+ Mockito.when(binaryDownload.getURI(Matchers.any(BinaryDownloadOptions.class))).thenReturn(myUri);
+
+ assertEquals(myUri, uriProvider.toURI(fileResource, Scope.EXTERNAL, Operation.READ));
+ ArgumentCaptor<BinaryDownloadOptions> argumentCaptor = ArgumentCaptor.forClass(BinaryDownloadOptions.class);
+ Mockito.verify(binaryDownload).getURI(argumentCaptor.capture());
+ assertEquals("myMimeType", argumentCaptor.getValue().getMediaType());
+ assertEquals("test", argumentCaptor.getValue().getFileName());
+ assertNull(argumentCaptor.getValue().getCharacterEncoding());
+ }
+
+ @Test
+ public void testPropertyWithoutExternallyAccessibleBlobStore() throws URISyntaxException, RepositoryException, IOException {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()-> uriProvider.toURI(fileResource, Scope.EXTERNAL, Operation.READ));
+ assertEquals("Cannot provide url for downloading the binary property at '/test/jcr:content/jcr:data'", e.getMessage());
+ }
+
+ @Test
+ public void testNoPrimaryPropertyUri() {
+ Resource resource = context.create().resource("/content/test1", Collections.singletonMap("jcr:primaryProperty", "nt:folder"));
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()-> uriProvider.toURI(resource, Scope.PUBLIC, Operation.READ));
+ assertEquals("Node does not have a primary property", e.getMessage());
+ }
+
+ @Test
+ public void testUnsupportedScope() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()-> uriProvider.toURI(fileResource, Scope.INTERNAL, Operation.READ));
+ assertEquals("This provider only provides URIs for 'READ' operations in scope 'PUBLIC' or 'EXTERNAL', but not for scope 'INTERNAL' and operation 'READ'", e.getMessage());
+ }
+
+ @Test
+ public void testUnsupportedOperation() {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, ()-> uriProvider.toURI(fileResource, Scope.EXTERNAL, Operation.UPDATE));
+ assertEquals("This provider only provides URIs for 'READ' operations in scope 'PUBLIC' or 'EXTERNAL', but not for scope 'EXTERNAL' and operation 'UPDATE'", e.getMessage());
+ }
+}