This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-resource.git
The following commit(s) were added to refs/heads/master by this push:
new 5d46719 SLING-11061 URIProvider for resources backed by JCR
BinaryDownload (#19)
5d46719 is described below
commit 5d467196e3510fa34b5b8c1f8002e328e7c6e34e
Author: Konrad Windszus <[email protected]>
AuthorDate: Tue Jan 18 09:56:49 2022 +0100
SLING-11061 URIProvider for resources backed by JCR BinaryDownload (#19)
SLING-11063 fix test after updating to Oak 1.10
---
pom.xml | 4 +-
.../sling/jcr/resource/internal/NodeUtil.java | 44 +++++++
.../helper/jcr/BinaryDownloadUriProvider.java | 145 +++++++++++++++++++++
.../internal/helper/jcr/JcrNodeResource.java | 51 ++------
.../resource/internal/JcrResourceListenerTest.java | 7 +-
.../helper/jcr/BinaryDownloadUriProviderTest.java | 125 ++++++++++++++++++
6 files changed, 331 insertions(+), 45 deletions(-)
diff --git a/pom.xml b/pom.xml
index 222acee..0e02e0b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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/NodeUtil.java
b/src/main/java/org/apache/sling/jcr/resource/internal/NodeUtil.java
index fa2d348..7f605f5 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,13 +18,25 @@
*/
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. */
@@ -63,4 +75,36 @@ public abstract class NodeUtil {
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..28edfa0
--- /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 with external 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 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 an store providing direct
downloads");
+ }
+ 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 java.util.Map;
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.api.JcrResourceConstants;
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 @@ class JcrNodeResource extends JcrItemResource<Node> { //
this should be package
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 @@ class JcrNodeResource extends JcrItemResource<Node> { //
this should be package
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/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 @@ public class JcrResourceListenerTest {
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 @@ public class JcrResourceListenerTest {
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());
+ }
+}