Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessIT.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessIT.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessIT.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessIT.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,876 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary;
+
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertTrue;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.getBinary;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.httpGet;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.httpPut;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.isFailedHttpPut;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.isSuccessfulHttpPut;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.putBinary;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.storeBinary;
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.storeBinaryAndRetrieve;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Binary;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.JackrabbitValueFactory;
+import org.apache.jackrabbit.api.ReferenceBinary;
+import org.apache.jackrabbit.api.binary.BinaryDownload;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions;
+import org.apache.jackrabbit.api.binary.BinaryUpload;
+import org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStore;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.binary.util.Content;
+import 
org.apache.jackrabbit.oak.plugins.blob.datastore.directaccess.ConfigurableDataRecordAccessProvider;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterables;
+
+/**
+ * Integration test for direct binary GET/PUT via HTTP, that requires a fully 
working data store
+ * (such as S3) for each {@link AbstractBinaryAccessIT#dataStoreFixtures() 
configured fixture}.
+ * The data store in question must support direct GET/PUT access via a URI.
+ * 
+ * Data store must be configured through e.g. aws.properties.
+ *
+ * Run this IT in maven using either:
+ *
+ *   single test:
+ *     mvn clean test -Dtest=BinaryAccessIT
+ * 
+ *   as part of all integration tests:
+ *     mvn -PintegrationTesting clean install
+ */
+@RunWith(Parameterized.class)
+public class BinaryAccessIT extends AbstractBinaryAccessIT {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String FILE_PATH = "/file";
+    private static final int REGULAR_WRITE_EXPIRY = 60*5; // seconds
+    private static final int REGULAR_READ_EXPIRY = 60*5; // seconds
+
+    public BinaryAccessIT(NodeStoreFixture fixture) {
+        // reuse NodeStore (and DataStore) across all tests in this class
+        super(fixture, true);
+    }
+
+    private JackrabbitValueFactory uploadProvider;
+    private JackrabbitValueFactory anonymousUploadProvider;
+
+    @Before
+    public void cleanRepoContents() throws RepositoryException {
+        Session anonymousSession = getAnonymousSession();
+        uploadProvider = (JackrabbitValueFactory) 
getAdminSession().getValueFactory();
+        anonymousUploadProvider = (JackrabbitValueFactory) 
anonymousSession.getValueFactory();
+
+        if (getAdminSession().nodeExists(FILE_PATH)) {
+            getAdminSession().getNode(FILE_PATH).remove();
+            getAdminSession().save();
+        }
+    }
+
+    // F1 - basic test
+    @Test
+    public void testUpload() throws Exception {
+        // enable writable URI feature
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        Content content = Content.createRandom(256);
+
+        assertTrue(getAdminSession().getValueFactory() instanceof 
JackrabbitValueFactory);
+
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        assertNotNull(upload);
+
+        // very small test binary
+        assertTrue(content.size() < upload.getMaxPartSize());
+
+        URI uri = upload.getUploadURIs().iterator().next();
+        assertNotNull(uri);
+
+        log.info("- uploading binary via PUT to {}", uri.toString());
+        int code = httpPut(uri, content.size(), content.getStream());
+
+        assertTrue("PUT to pre-signed URI failed",
+                isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+
+        Binary writeBinary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        putBinary(getAdminSession(), FILE_PATH, writeBinary);
+
+        Binary readBinary = getBinary(getAdminSession(), FILE_PATH);
+        content.assertEqualsWith(readBinary.getStream());
+    }
+
+    // F3 - Multi-part upload
+    @Test
+    public void testMultiPartUpload() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        assertTrue(getAdminSession().getValueFactory() instanceof 
JackrabbitValueFactory);
+
+        // 25MB is a good size to ensure chunking is done
+        Content content = Content.createRandom(1024 * 1024 * 25);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 50);
+        assertNotNull(upload);
+
+        List<URI> uris = new ArrayList<>();
+        Iterables.addAll(uris, upload.getUploadURIs());
+
+        // this follows the upload algorithm from BinaryUpload
+        if (content.size() / upload.getMaxPartSize() > uris.size()) {
+            fail("exact binary size was provided but implementation failed to 
provide enough upload URIs");
+        }
+        if (content.size() < upload.getMinPartSize()) {
+            // single upload
+            content.httpPUT(uris.get(0));
+        } else {
+            // multipart upload
+            final long basePartSize = (long) Math.ceil(content.size() / 
(double) uris.size());
+            
+            long offset = 0;
+            for (URI uri : uris) {
+                final long partSize = Math.min(basePartSize, content.size() - 
offset);
+
+                int code = content.httpPUT(uri, offset, partSize);
+                assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+
+                offset += partSize;
+                // fail safe check, shouldn't be necessary
+                if (offset >= content.size()) {
+                    break;
+                }
+            }
+        }
+
+        Binary writeBinary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        putBinary(getAdminSession(), FILE_PATH, writeBinary);
+
+        Binary readBinary = getBinary(getAdminSession(), FILE_PATH);
+        content.assertEqualsWith(readBinary.getStream());
+    }
+
+    // F8 - test reading getBinary().toInputStream() once uploaded
+    @Test
+    public void testStreamBinaryThroughJCRAfterURIWrite() throws Exception {
+        // enable writable URI feature
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        // 1. add binary and upload
+        Content content = Content.createRandom(256);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 10);
+        int code = content.httpPUT(upload.getUploadURIs().iterator().next());
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+
+        Binary binaryWrite = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        storeBinary(getAdminSession(), FILE_PATH, binaryWrite);
+
+        // 2. stream through JCR and validate it's the same
+        Session session = createAdminSession();
+        try {
+            Binary binaryRead = getBinary(session, FILE_PATH);
+            content.assertEqualsWith(binaryRead.getStream());
+        } finally {
+            session.logout();
+        }
+    }
+
+    // F10 - GET Binary when created via repo
+    @Test
+    public void testGetBinary() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        // Must be larger than the minimum file size, to keep it from being 
inlined in the node store.
+        Content content = Content.createRandom(1024*20);
+
+        // make sure to test getting a fresh Binary
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+        assertTrue(binary instanceof BinaryDownload);
+
+        URI downloadURI = ((BinaryDownload) 
binary).getURI(BinaryDownloadOptions.DEFAULT);
+        assertNotNull("HTTP download URI is null", downloadURI);
+        content.assertEqualsWith(httpGet(downloadURI));
+
+        // different way to retrieve binary
+        // TODO: also test multivalue binary prop
+        binary = getAdminSession().getNode(FILE_PATH)
+            .getNode(JcrConstants.JCR_CONTENT)
+            .getProperty(JcrConstants.JCR_DATA).getValue().getBinary();
+        downloadURI = ((BinaryDownload) 
binary).getURI(BinaryDownloadOptions.DEFAULT);
+        assertNotNull("HTTP download URI is null", downloadURI);
+    }
+
+    // F9 - GET Binary for binary after write using direct PUT
+    @Test
+    public void testGetBinaryAfterPut() throws Exception {
+        // enable writable and readable URI feature
+        ConfigurableDataRecordAccessProvider provider = 
getConfigurableHttpDataRecordProvider();
+        provider.setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        provider.setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        // 1. add binary and upload
+        Content content = Content.createRandom(1024*20);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 10);
+        int code = content.httpPUT(upload.getUploadURIs().iterator().next());
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+        Binary writeBinary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        storeBinary(getAdminSession(), FILE_PATH, writeBinary);
+
+        // 2. read binary, get the URI
+        Binary binary = getBinary(getAdminSession(), FILE_PATH);
+        URI downloadURI = 
((BinaryDownload)(binary)).getURI(BinaryDownloadOptions.DEFAULT);
+
+        // 3. GET on URI and verify contents are the same
+        content.assertEqualsWith(httpGet(downloadURI));
+    }
+
+    @Test
+    public void testGetSmallBinaryReturnsNull() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        // Must be smaller than the minimum file size, (inlined binary)
+        Content content = Content.createRandom(256);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        URI downloadURI = 
((BinaryDownload)(binary)).getURI(BinaryDownloadOptions.DEFAULT);
+        assertNull(downloadURI);
+    }
+
+    @Test
+    public void testGetBinaryWithSpecificMediaType() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedMediaType = "image/png";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withMediaType(expectedMediaType)
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String mediaType = conn.getHeaderField("Content-Type");
+        assertNotNull(mediaType);
+        assertEquals(expectedMediaType, mediaType);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryWithSpecificMediaTypeAndEncoding() throws 
Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedMediaType = "text/plain";
+        String expectedCharacterEncoding = "utf-8";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withMediaType(expectedMediaType)
+                .withCharacterEncoding(expectedCharacterEncoding)
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String mediaType = conn.getHeaderField("Content-Type");
+        assertNotNull(mediaType);
+        assertEquals(String.format("%s; charset=%s", expectedMediaType, 
expectedCharacterEncoding),
+                mediaType);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryWithCharacterEncodingOnly() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedCharacterEncoding = "utf-8";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withCharacterEncoding(expectedCharacterEncoding)
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String mediaType = conn.getHeaderField("Content-Type");
+        // application/octet-stream is the default Content-Type if none is
+        // set in the signed URI
+        assertEquals("application/octet-stream", mediaType);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryWithSpecificFileName() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedName = "beautiful landscape.png";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withFileName(expectedName)
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        assertNotNull(contentDisposition);
+        String encodedName = new 
String(expectedName.getBytes(StandardCharsets.UTF_8));
+        assertEquals(
+                String.format("inline; filename=\"%s\"; filename*=UTF-8''%s",
+                        expectedName, encodedName),
+                contentDisposition
+        );
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryWithSpecificFileNameAndDispositionType() throws 
Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedName = "beautiful landscape.png";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withFileName(expectedName)
+                .withDispositionTypeAttachment()
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        assertNotNull(contentDisposition);
+        String encodedName = new 
String(expectedName.getBytes(StandardCharsets.UTF_8));
+        assertEquals(
+                String.format("attachment; filename=\"%s\"; 
filename*=UTF-8''%s",
+                        expectedName, encodedName),
+                contentDisposition
+        );
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryWithDispositionType() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withDispositionTypeInline()
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        // Should be no header since filename was not set and disposition type
+        // is "inline"
+        assertNull(contentDisposition);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+
+        downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withDispositionTypeAttachment()
+                .build();
+        downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        conn = (HttpURLConnection) downloadURI.toURL().openConnection();
+        contentDisposition = conn.getHeaderField("Content-Disposition");
+        // Content-Disposition should exist now because "attachment" was set
+        assertNotNull(contentDisposition);
+        assertEquals("attachment", contentDisposition);
+    }
+
+    @Test
+    public void testGetBinarySetsAllHeaders() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        String expectedMediaType = "image/png";
+        String expectedCharacterEncoding = "utf-8";
+        String expectedName = "beautiful landscape.png";
+        BinaryDownloadOptions downloadOptions = BinaryDownloadOptions
+                .builder()
+                .withMediaType(expectedMediaType)
+                .withCharacterEncoding(expectedCharacterEncoding)
+                .withFileName(expectedName)
+                .withDispositionTypeAttachment()
+                .build();
+        URI downloadURI = ((BinaryDownload)(binary)).getURI(downloadOptions);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String mediaType = conn.getHeaderField("Content-Type");
+        assertNotNull(mediaType);
+        assertEquals(
+                String.format("%s; charset=%s", expectedMediaType, 
expectedCharacterEncoding),
+                mediaType);
+
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        assertNotNull(contentDisposition);
+        String encodedName = new 
String(expectedName.getBytes(StandardCharsets.UTF_8));
+        assertEquals(
+                String.format("attachment; filename=\"%s\"; 
filename*=UTF-8''%s",
+                        expectedName, encodedName),
+                contentDisposition
+        );
+
+        String cacheControl = conn.getHeaderField("Cache-Control");
+        assertNotNull(cacheControl);
+        assertEquals(String.format("private, max-age=%d, immutable", 
REGULAR_READ_EXPIRY), cacheControl);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    @Test
+    public void testGetBinaryDefaultOptions() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        URI downloadURI = 
((BinaryDownload)(binary)).getURI(BinaryDownloadOptions.DEFAULT);
+
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        String mediaType = conn.getHeaderField("Content-Type");
+        assertNotNull(mediaType);
+        assertEquals("application/octet-stream", mediaType);
+
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        assertNull(contentDisposition);
+
+        String cacheControl = conn.getHeaderField("Cache-Control");
+        assertNotNull(cacheControl);
+        assertEquals(String.format("private, max-age=%d, immutable", 
REGULAR_READ_EXPIRY), cacheControl);
+
+        // Verify response content
+        assertEquals(200, conn.getResponseCode());
+        content.assertEqualsWith(conn.getInputStream());
+    }
+
+    // A6 - Client MUST only get permission to add a blob referenced in a JCR 
binary property
+    //      where the user has JCR set_property permission.
+    @Test
+    @Ignore("OAK-7602")  // michid FIXME OAK-7602
+    public void testUnprivilegedSessionCannotUploadBinary() throws Exception {
+        // enable writable URI feature
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        try {
+            anonymousUploadProvider.initiateBinaryUpload(1024*20, 10);
+            fail();
+        }
+        catch (AccessDeniedException e) { }
+    }
+
+    // A2 - disable write URIs entirely
+    @Test
+    public void testDisableDirectHttpUpload() throws Exception {
+        // disable in data store config by setting expiry to zero
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(0);
+
+        Content content = Content.createRandom(256);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 10);
+
+        assertNotNull(upload);
+        assertFalse(upload.getUploadURIs().iterator().hasNext());
+    }
+
+    // A2 - disable get URIs entirely
+    @Test
+    public void testDisableDirectHttpDownload() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectDownloadURIExpirySeconds(0);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        URI downloadURI = 
((BinaryDownload)(binary)).getURI(BinaryDownloadOptions.DEFAULT);
+        assertNull(downloadURI);
+    }
+
+    // A2/A3 - configure short expiry time, wait, ensure upload fails after 
expired
+    @Test
+    public void testPutURIExpires() throws Exception {
+        // short timeout
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(1);
+
+        Content content = Content.createRandom(1024*20);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 10);
+
+        // wait to pass timeout: 2 seconds
+        Thread.sleep(2 * 1000);
+
+        // ensure PUT fails with 403 or anything 400+
+        assertTrue(content.httpPUT(upload.getUploadURIs().iterator().next()) 
>= HttpURLConnection.HTTP_BAD_REQUEST);
+    }
+
+    // F2 - transfer accelerator (S3 only feature)
+    @Test
+    public void testTransferAcceleration() throws Exception {
+        ConfigurableDataRecordAccessProvider provider = 
getConfigurableHttpDataRecordProvider();
+
+        // This test is S3 specific
+        if (provider instanceof S3DataStore) {
+
+            provider.setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+            provider.setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+            provider.setBinaryTransferAccelerationEnabled(true);
+
+            BinaryUpload upload = uploadProvider.initiateBinaryUpload(1024 * 
20, 1);
+            URI uri = upload.getUploadURIs().iterator().next();
+            assertNotNull(uri);
+
+            log.info("accelerated URI: {}", uri.toString());
+            assertTrue(uri.getHost().endsWith(".s3-accelerate.amazonaws.com"));
+
+            provider.setBinaryTransferAccelerationEnabled(false);
+            upload = uploadProvider.initiateBinaryUpload(1024*20, 1);
+            uri = upload.getUploadURIs().iterator().next();
+            assertNotNull(uri);
+
+            log.info("non-accelerated URI: {}", uri.toString());
+            
assertFalse(uri.getHost().endsWith(".s3-accelerate.amazonaws.com"));
+        }
+    }
+
+    // A1 - get put URI, change it and try uploading it
+    @Test
+    public void testModifiedPutURIFails() throws Exception {
+        // enable writable URI feature
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        Content content = Content.createRandom(1024*20);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        URI uri = upload.getUploadURIs().iterator().next();
+        URI changedURI = new URI(
+                String.format("%s://%s/%sX?%s",  // NOTE the injected "X" in 
the URI filename
+                uri.getScheme(),
+                uri.getHost(),
+                uri.getPath(),
+                uri.getQuery())
+        );
+        int code = content.httpPUT(changedURI);
+        assertTrue(isFailedHttpPut(code));
+    }
+
+    // A1 - get put URI, upload, then try reading from the same URI
+    @Test
+    public void testCannotReadFromPutURI() throws Exception {
+        // enable writable URI and readable URI feature
+        ConfigurableDataRecordAccessProvider provider = 
getConfigurableHttpDataRecordProvider();
+        provider.setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        provider.setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        URI uri = upload.getUploadURIs().iterator().next();
+        int code = content.httpPUT(uri);
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+
+        HttpURLConnection conn = (HttpURLConnection) 
uri.toURL().openConnection();
+        code = conn.getResponseCode();
+        assertTrue(isFailedHttpPut(code));
+    }
+
+    // A1 - add binary via JCR, then get put URI, and modify to try to upload 
over first binary
+    @Test
+    public void testCannotModifyExistingBinaryViaPutURI() throws Exception {
+        // enable writable URI and readable URI feature
+        ConfigurableDataRecordAccessProvider provider = 
getConfigurableHttpDataRecordProvider();
+        provider.setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        provider.setDirectDownloadURIExpirySeconds(REGULAR_READ_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        URI downloadURI = 
((BinaryDownload)(binary)).getURI(BinaryDownloadOptions.DEFAULT);
+        assertNotNull(downloadURI);
+
+        Content moreContent = Content.createRandom(1024*20);
+        uploadProvider.initiateBinaryUpload(moreContent.size(), 1);
+        HttpURLConnection conn = (HttpURLConnection) 
downloadURI.toURL().openConnection();
+        conn.setRequestMethod("PUT");
+        conn.setDoOutput(true);
+        IOUtils.copy(moreContent.getStream(), conn.getOutputStream());
+
+        int code = conn.getResponseCode();
+        assertTrue(isFailedHttpPut(code));
+
+        content.assertEqualsWith(httpGet(downloadURI));
+    }
+
+    // D1 - immutable after initial upload
+    @Test
+    public void testUploadedBinaryIsImmutable() throws Exception {
+        // enable writable URI feature
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        // 1. upload and store first binary
+        Content content = Content.createRandom(1024*20);
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        assertNotNull(upload);
+        int code = content.httpPUT(upload.getUploadURIs().iterator().next());
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+        Binary uploadedBinary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        storeBinary(getAdminSession(), FILE_PATH, uploadedBinary);
+
+        Binary binary1 = getBinary(getAdminSession(), FILE_PATH);
+
+        // 2. upload different binary content, but store at the same JCR 
location
+        Content moreContent = Content.createRandom(1024*21);
+        upload = uploadProvider.initiateBinaryUpload(moreContent.size(), 1);
+        assertNotNull(upload);
+        code = moreContent.httpPUT(upload.getUploadURIs().iterator().next());
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+        uploadedBinary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        storeBinary(getAdminSession(), FILE_PATH, uploadedBinary);
+
+        Binary binary2 = getBinary(getAdminSession(), FILE_PATH);
+
+        // 3. verify they have different references
+        assertTrue(binary1 instanceof ReferenceBinary);
+        assertTrue(binary2 instanceof ReferenceBinary);
+        assertNotEquals(((ReferenceBinary) binary1).getReference(), 
((ReferenceBinary) binary2).getReference());
+    }
+
+    // D2 - unique identifiers
+    @Test
+    public void testUploadPathsAreUnique() throws Exception {
+        // Every upload destination should be unique with no regard to file 
content
+        // The content is not read by the Oak code so no deduplication can be 
performed
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+
+        BinaryUpload upload1 = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        assertNotNull(upload1);
+        BinaryUpload upload2 = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        assertNotNull(upload2);
+
+        assertNotEquals(upload1.getUploadURIs().iterator().next().toString(),
+                upload2.getUploadURIs().iterator().next().toString());
+    }
+
+    // D3 - do not delete directly => copy nt:file node, delete one, ensure 
binary still there
+    @Test
+    public void testBinaryNotDeletedWithNode() throws Exception {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        Content content = Content.createRandom(1024*20);
+
+        BinaryUpload upload = 
uploadProvider.initiateBinaryUpload(content.size(), 1);
+        int code = content.httpPUT(upload.getUploadURIs().iterator().next());
+        assertTrue(isSuccessfulHttpPut(code, 
getConfigurableHttpDataRecordProvider()));
+        Binary binary = 
uploadProvider.completeBinaryUpload(upload.getUploadToken());
+        storeBinary(getAdminSession(), FILE_PATH+"2", binary);
+
+        storeBinary(getAdminSession(), FILE_PATH, binary);
+
+        getAdminSession().getNode(FILE_PATH+"2").remove();
+        getAdminSession().save();
+
+        Binary savedBinary = getBinary(getAdminSession(), FILE_PATH);
+        assertNotNull(savedBinary);
+        assertTrue(binary instanceof ReferenceBinary);
+        assertTrue(savedBinary instanceof ReferenceBinary);
+        assertEquals(((ReferenceBinary) binary).getReference(),
+                ((ReferenceBinary) savedBinary).getReference());
+    }
+
+    // D5 - blob ref not persisted in NodeStore until binary uploaded and 
immutable
+    // NOTE: not test needed for this, as the API guarantees the Binary is 
only returned after
+    //       the blob was persisted in completeBinaryUpload() and client code 
is responsible
+    //       for writing it to the JCR
+
+    // more tests
+
+    @Test
+    public void testInitiateHttpUploadWithZeroSizeFails() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(0, 1);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testInitiateHttpUploadWithZeroURIsFails() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(1024 * 20, 0);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testInitiateHttpUploadWithUnsupportedNegativeNumberURIsFails()
+        throws RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(1024 * 20, -2);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testInitiateHttpUploadWithUnlimitedURIs() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        BinaryUpload upload = uploadProvider.initiateBinaryUpload(1024 * 1024 
* 1024, -1);
+        assertNotNull(upload);
+        assertTrue(Iterables.size(upload.getUploadURIs()) > 50);
+        // 50 is our default expected client max -
+        // this is to make sure we will give as many as needed
+        // if the client doesn't specify their own limit
+    }
+
+    @Test
+    public void testInitiateHttpUploadLargeSinglePut() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        BinaryUpload upload = uploadProvider.initiateBinaryUpload(1024 * 1024 
* 100, 1);
+        assertNotNull(upload);
+        assertEquals(1, Iterables.size(upload.getUploadURIs()));
+    }
+
+    @Test
+    public void testInitiateHttpUploadTooLargeForSinglePut() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(1024L * 1024L * 1024L * 10L, 
1);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testInitiateHttpUploadTooLargeForUpload() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(1024L * 1024L * 1024L * 1024L 
* 10L, -1);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testInitiateHttpUploadTooLargeForRequestedNumURIs() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+        try {
+            uploadProvider.initiateBinaryUpload(1024L * 1024L * 1024L * 10L, 
10);
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+
+    @Test
+    public void testCompleteHttpUploadWithInvalidTokenFails() throws 
RepositoryException {
+        getConfigurableHttpDataRecordProvider()
+                .setDirectUploadURIExpirySeconds(REGULAR_WRITE_EXPIRY);
+
+        BinaryUpload upload = uploadProvider.initiateBinaryUpload(256, 10);
+        assertNotNull(upload);
+
+        try {
+            uploadProvider.completeBinaryUpload(upload.getUploadToken() + "X");
+            fail();
+        }
+        catch (IllegalArgumentException e) { }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessIT.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessTest.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessTest.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessTest.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,310 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import javax.jcr.Binary;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.ObservationManager;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.JackrabbitValueFactory;
+import org.apache.jackrabbit.api.binary.BinaryDownload;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions;
+import org.apache.jackrabbit.api.binary.BinaryUpload;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.blob.BlobAccessProvider;
+import org.apache.jackrabbit.oak.api.blob.BlobDownloadOptions;
+import org.apache.jackrabbit.oak.api.blob.BlobUpload;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.datastore.FileDataStoreFixture;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.nodestore.SegmentMemoryNodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils;
+import org.apache.jackrabbit.oak.jcr.binary.util.Content;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.test.LogPrintWriter;
+import org.apache.jackrabbit.test.api.observation.EventResult;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a unit test for the direct binary access JCR API extension.
+ * It uses a mock of the underlying BlobAccessProvider.
+ *
+ * For a full integration test against real binary cloud storage,
+ * see BinaryAccessIt.
+ */
+@RunWith(Parameterized.class)
+public class BinaryAccessTest extends AbstractRepositoryTest {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(BinaryAccessTest.class);
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Iterable<?> dataStoreFixtures() {
+        Collection<NodeStoreFixture> fixtures = new ArrayList<>();
+        fixtures.add(new SegmentMemoryNodeStoreFixture(new 
FileDataStoreFixture()));
+        return fixtures;
+    }
+
+    public BinaryAccessTest(NodeStoreFixture fixture) {
+        super(fixture);
+    }
+
+    private static final String FILE_PATH = "/file";
+
+    private static final long SEGMENT_INLINE_SIZE = 16 * 1024;
+
+    private static final String DOWNLOAD_URL = 
"http://expected.com/dummy/url/for/test/download";;
+
+    private static URI expectedDownloadURI() {
+        return toURI(DOWNLOAD_URL);
+    }
+
+    private static final String UPLOAD_TOKEN = "super-safe-encrypted-token";
+
+    private static final String UPLOAD_URL = 
"http://expected.com/dummy/url/for/test/upload";;
+
+    private static URI expectedUploadURI() {
+        return toURI(UPLOAD_URL);
+    }
+
+    private static URI toURI(String url) {
+        try {
+            return new URI(url);
+        } catch (URISyntaxException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    protected Content blobContent;
+
+    /**
+     * Adjust JCR repository creation to register a mock BlobAccessProvider in 
Whiteboard
+     * so it can be picked up by oak-jcr.
+     */
+    @Override
+    protected Repository createRepository(NodeStore nodeStore) {
+        Oak oak = new Oak(nodeStore);
+        oak.getWhiteboard().register(BlobAccessProvider.class, new 
MockBlobAccessProvider(), Collections.emptyMap());
+        return initJcr(new Jcr(oak)).createRepository();
+    }
+
+    private class MockBlobAccessProvider implements BlobAccessProvider {
+
+        @Override
+        public @Nullable BlobUpload initiateBlobUpload(long maxSize, int 
maxURIs) throws IllegalArgumentException {
+            return new BlobUpload() {
+                @Override
+                public @NotNull String getUploadToken() {
+                    return UPLOAD_TOKEN;
+                }
+
+                @Override
+                public long getMinPartSize() {
+                    return 0;
+                }
+
+                @Override
+                public long getMaxPartSize() {
+                    return 10 * 1024 * 1024;
+                }
+
+                @Override
+                public @NotNull Collection<URI> getUploadURIs() {
+                    Collection<URI> uris = new ArrayList<>();
+                    uris.add(expectedUploadURI());
+                    return uris;
+                }
+            };
+        }
+
+        @Override
+        public @Nullable Blob completeBlobUpload(@NotNull String uploadToken) 
throws IllegalArgumentException {
+            if (!UPLOAD_TOKEN.equals(uploadToken)) {
+                return null;
+            }
+
+            // this returns the binary content set on the "blobContent" member
+            // as a simple way to mock some binary "storage"
+            return new Blob() {
+
+                @Override
+                public @NotNull InputStream getNewStream() {
+                    return blobContent.getStream();
+                }
+
+                @Override
+                public long length() {
+                    return blobContent.size();
+                }
+
+                @Override
+                public String getReference() {
+                    return "super-secure-key#" + getContentIdentity();
+                }
+
+                @Override
+                public String getContentIdentity() {
+                    return DigestUtils.md5Hex(blobContent.toString());
+                }
+            };
+        }
+
+        @Override
+        public @Nullable URI getDownloadURI(@NotNull Blob blob,
+                                            @NotNull BlobDownloadOptions 
blobDownloadOptions) {
+            return expectedDownloadURI();
+        }
+    }
+
+    @Test
+    public void testBinaryDownload() throws RepositoryException {
+        Content content = Content.createRandom(SEGMENT_INLINE_SIZE * 2);
+        Binary binary = 
BinaryAccessTestUtils.storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        assertTrue(binary instanceof BinaryDownload);
+
+        BinaryDownload binaryDownload = (BinaryDownload) binary;
+        URI uri = binaryDownload.getURI(BinaryDownloadOptions.DEFAULT);
+
+        // we only need test that the we get a URI back (from our mock) to 
validate oak-jcr's inner workings
+        assertNotNull(uri);
+        assertEquals(expectedDownloadURI(), uri);
+    }
+
+    @Test
+    public void testBinaryUpload() throws RepositoryException, IOException {
+        Content content = Content.createRandom(SEGMENT_INLINE_SIZE * 2);
+
+        ValueFactory vf = getAdminSession().getValueFactory();
+        assertTrue(vf instanceof JackrabbitValueFactory);
+
+        JackrabbitValueFactory valueFactory = (JackrabbitValueFactory) vf;
+
+        // 1. test initiate
+        BinaryUpload binaryUpload = 
valueFactory.initiateBinaryUpload(content.size(), 1);
+
+        assertNotNull(binaryUpload);
+        assertEquals(UPLOAD_TOKEN, binaryUpload.getUploadToken());
+
+        // 2. simulate an "upload"
+        blobContent = content;
+
+        // 3. test complete
+        Binary binary = 
valueFactory.completeBinaryUpload(binaryUpload.getUploadToken());
+
+        assertNotNull(binary);
+        assertEquals(content.size(), binary.getSize());
+
+        // 4. test that we can use this binary in JCR
+        BinaryAccessTestUtils.storeBinary(getAdminSession(), FILE_PATH, 
binary);
+
+        binary = BinaryAccessTestUtils.getBinary(getAdminSession(), FILE_PATH);
+        content.assertEqualsWith(binary.getStream());
+    }
+
+    @Test
+    public void testEvent() throws Exception {
+        BinaryAccessTestUtils.storeBinaryAndRetrieve(getAdminSession(), 
FILE_PATH, Content.createRandom(0));
+
+        ObservationManager obsMgr = 
getAdminSession().getWorkspace().getObservationManager();
+        EventResult result = new EventResult(new LogPrintWriter(LOG));
+        obsMgr.addEventListener(result, Event.PROPERTY_CHANGED, FILE_PATH, 
true, null, null, false);
+
+        Content content = Content.createRandom(SEGMENT_INLINE_SIZE * 2);
+        BinaryAccessTestUtils.storeBinaryAndRetrieve(getAdminSession(), 
FILE_PATH, content);
+
+        Event[] events = result.getEvents(TimeUnit.SECONDS.toMillis(5));
+        assertEquals(1, events.length);
+
+        assertEquals(Event.PROPERTY_CHANGED, events[0].getType());
+        Value afterValue = (Value) events[0].getInfo().get("afterValue");
+        assertNotNull(afterValue);
+        Binary binary = afterValue.getBinary();
+        content.assertEqualsWith(binary.getStream());
+
+        assertTrue(binary instanceof BinaryDownload);
+
+        BinaryDownload binaryDownload = (BinaryDownload) binary;
+        URI uri = binaryDownload.getURI(BinaryDownloadOptions.DEFAULT);
+
+        assertNotNull(uri);
+        assertEquals(expectedDownloadURI(), uri);
+    }
+
+    @Test
+    public void testAuthorizableProperty() throws Exception {
+        assertTrue(getAdminSession() instanceof JackrabbitSession);
+        JackrabbitSession session = (JackrabbitSession) getAdminSession();
+        UserManager userMgr = session.getUserManager();
+        ValueFactory vf = session.getValueFactory();
+
+        Content content = Content.createRandom(SEGMENT_INLINE_SIZE * 2);
+        Binary binary = 
BinaryAccessTestUtils.storeBinaryAndRetrieve(getAdminSession(), FILE_PATH, 
content);
+
+        Authorizable auth = userMgr.getAuthorizable(session.getUserID());
+        assertNotNull(auth);
+
+        auth.setProperty("avatar", vf.createValue(binary));
+        if (!userMgr.isAutoSave()) {
+            session.save();
+        }
+
+        Value[] values = auth.getProperty("avatar");
+        assertNotNull(values);
+        assertEquals(1, values.length);
+        binary = values[0].getBinary();
+
+        content.assertEqualsWith(binary.getStream());
+
+        assertTrue(binary instanceof BinaryDownload);
+
+        BinaryDownload binaryDownload = (BinaryDownload) binary;
+        URI uri = binaryDownload.getURI(BinaryDownloadOptions.DEFAULT);
+
+        assertNotNull(uri);
+        assertEquals(expectedDownloadURI(), uri);
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessUnsupportedIT.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessUnsupportedIT.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessUnsupportedIT.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessUnsupportedIT.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,123 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary;
+
+import static 
org.apache.jackrabbit.oak.jcr.binary.util.BinaryAccessTestUtils.storeBinaryAndRetrieve;
+import static org.junit.Assert.assertNull;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.jcr.Binary;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.api.JackrabbitValueFactory;
+import org.apache.jackrabbit.api.binary.BinaryDownload;
+import org.apache.jackrabbit.api.binary.BinaryDownloadOptions;
+import org.apache.jackrabbit.api.binary.BinaryUpload;
+import org.apache.jackrabbit.oak.api.blob.BlobAccessProvider;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.datastore.FileDataStoreFixture;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.nodestore.DocumentMemoryNodeStoreFixture;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.nodestore.SegmentMemoryNodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.binary.util.Content;
+import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+/**
+ * Test binary upload / download capabilities of the {@link 
JackrabbitValueFactory} interface when the underlying
+ * implementation does not support these features.  If the underlying doesn't 
support these features the implementation
+ * will return null when the methods are called and clients are expected to 
check for null to determine if the
+ * features are supported.
+ */
+public class BinaryAccessUnsupportedIT extends AbstractRepositoryTest {
+    private JackrabbitValueFactory uploadProvider;
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Iterable<?> dataStoreFixtures() {
+        Collection<NodeStoreFixture> fixtures = Lists.newArrayList();
+
+        // Create a fixture using FileDataStore.  FileDataStore doesn't 
support the direct access features so
+        // it should be a valid real-world example of how the API should 
behave when the implementation doesn't
+        // have the feature support.
+        FileDataStoreFixture fds = new FileDataStoreFixture();
+        fixtures.add(new SegmentMemoryNodeStoreFixture(fds));
+        fixtures.add(new DocumentMemoryNodeStoreFixture(fds));
+
+        return fixtures;
+    }
+
+    public BinaryAccessUnsupportedIT(NodeStoreFixture fixture) {
+        super(fixture);
+    }
+
+    @Before
+    public void setup() throws RepositoryException {
+        uploadProvider = (JackrabbitValueFactory) 
getAdminSession().getValueFactory();
+    }
+
+    @Override
+    protected Repository createRepository(NodeStore nodeStore) {
+        Whiteboard wb = new DefaultWhiteboard();
+
+        BlobStore blobStore = getNodeStoreComponent(BlobStore.class);
+        if (blobStore != null && blobStore instanceof BlobAccessProvider) {
+            wb.register(BlobAccessProvider.class, (BlobAccessProvider) 
blobStore,
+                    Collections.emptyMap());
+
+        }
+
+        return initJcr(new Jcr(nodeStore).with(wb)).createRepository();
+    }
+
+    @Test
+    public void testInitiateUploadUnsupportedReturnsNull() throws Exception {
+        BinaryUpload upload = uploadProvider.initiateBinaryUpload(1024*20, 10);
+        assertNull(upload);
+    }
+
+    @Test
+    public void testCompleteUploadUnsupportedReturnsNull() throws Exception {
+        Binary binary = uploadProvider.completeBinaryUpload("fake_token");
+        assertNull(binary);
+    }
+
+    @Test
+    public void testGetDownloadURIUnsupportedReturnsNull() throws Exception {
+        Content content = Content.createRandom(1024*20);
+        Binary binary = storeBinaryAndRetrieve(getAdminSession(), "/my_path", 
content);
+
+        // the returned binary could not be implementing BinaryDownload...
+        if (binary instanceof BinaryDownload) {
+            // ...or implement it but return null on getURI()
+            URI downloadURI = ((BinaryDownload) 
binary).getURI(BinaryDownloadOptions.DEFAULT);
+            assertNull(downloadURI);
+        }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/BinaryAccessUnsupportedIT.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/AzureDataStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/AzureDataStoreFixture.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/AzureDataStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/AzureDataStoreFixture.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,143 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary.fixtures.datastore;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+
+import org.apache.jackrabbit.core.data.DataStore;
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzureConstants;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.AzureDataStore;
+import org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.Utils;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.binary.fixtures.nodestore.FixtureUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.microsoft.azure.storage.StorageException;
+import com.microsoft.azure.storage.blob.CloudBlobContainer;
+
+/**
+ * Fixture for AzureDataStore based on an azure.properties config file. It 
creates
+ * a new temporary Azure Blob Container for each DataStore created.
+ *
+ * Note: when using this, it's highly recommended to reuse the NodeStores 
across multiple tests (using
+ * {@link 
org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest#AbstractRepositoryTest(NodeStoreFixture,
 boolean) AbstractRepositoryTest(fixture, true)})
+ * otherwise it will be slower and can lead to out of memory issues if there 
are many tests.
+ *
+ * <p>
+ * Test buckets are named "direct-binary-test-...". If some did not get 
cleaned up, you can
+ * list them using the aws cli with this command:
+ * <pre>
+ *     az storage container list --output table | grep direct-binary-test-
+ * </pre>
+ *
+ * And after checking, delete them all in one go with this command:
+ * <pre>
+ *     az storage container list --output table | grep direct-binary-test- | 
cut -d " " -f 1 | xargs -n 1 -I {} sh -c 'az storage container delete -n {}'
+ * </pre>
+ */
+public class AzureDataStoreFixture implements DataStoreFixture {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Nullable
+    private final Properties azProps;
+    private Map<DataStore, CloudBlobContainer> containers = new HashMap<>();
+
+    public AzureDataStoreFixture() {
+        azProps = FixtureUtils.loadDataStoreProperties("azure.config", 
"azure.properties", ".azure");
+    }
+
+    @Override
+    public boolean isAvailable() {
+        if (azProps == null) {
+            log.warn("Skipping Azure DataStore fixture because no AZ 
properties file was found given by " +
+                "'azure.config' system property or named 'azure.properties' or 
'~/.azure/azure.properties'.");
+            return false;
+        }
+        return true;
+    }
+
+    @NotNull
+    @Override
+    public DataStore createDataStore() {
+        if (!isAvailable() || azProps == null) {
+            throw new AssertionError("createDataStore() called but this 
fixture is not available");
+        }
+
+        // Create a temporary container that will be removed at test completion
+        String containerName = "direct-binary-test-" + 
UUID.randomUUID().toString();
+
+        log.info("Creating Azure test blob container {}", containerName);
+
+        String connectionString = 
Utils.getConnectionStringFromProperties(azProps);
+        try {
+            CloudBlobContainer container = 
Utils.getBlobContainer(connectionString, containerName);
+            container.createIfNotExists();
+
+            // create new properties since azProps is shared for all created 
DataStores
+            Properties clonedAzProps = new Properties(azProps);
+            
clonedAzProps.setProperty(AzureConstants.AZURE_BLOB_CONTAINER_NAME, 
container.getName());
+
+            // setup Oak DS
+            AzureDataStore dataStore = new AzureDataStore();
+            dataStore.setProperties(clonedAzProps);
+            dataStore.setStagingSplitPercentage(0);
+
+            containers.put(dataStore, container);
+            return dataStore;
+
+        } catch (DataStoreException | StorageException e) {
+            throw new AssertionError("Azure DataStore fixture fails because of 
issue with Azure config or connection", e);
+        }
+    }
+
+    @Override
+    public void dispose(DataStore dataStore) {
+        if (dataStore == null) {
+            return;
+        }
+
+        try {
+            dataStore.close();
+        } catch (DataStoreException e) {
+            log.warn("Issue while disposing DataStore", e);
+        }
+
+        CloudBlobContainer container = containers.get(dataStore);
+        if (container != null) {
+            log.info("Removing Azure test blob container {}", 
container.getName());
+            try {
+                // For Azure, you can just delete the container and all
+                // blobs it in will also be deleted
+                container.delete();
+            } catch (StorageException e) {
+                log.warn("Unable to delete Azure Blob container {}", 
container.getName());
+            }
+
+            containers.remove(dataStore);
+        }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/AzureDataStoreFixture.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/DataStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/DataStoreFixture.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/DataStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/DataStoreFixture.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,51 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary.fixtures.datastore;
+
+import org.apache.jackrabbit.core.data.DataStore;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *  DataStore fixture for parametrized tests. To be used inside 
NodeStoreFixture implementations.
+ */
+public interface DataStoreFixture {
+
+    /**
+     * Create a new DataStore instance. This might include setting up a 
temporary folder,
+     * bucket, container... of the underlying storage solution. Return null if 
this
+     * DataStore is not available because of missing configuration or setup 
issues
+     * (implementation shall log such warnings/errors).
+     *
+     * Calling DataStore.init() is left to the client.
+     */
+    @NotNull
+    DataStore createDataStore();
+
+    /**
+     * Dispose a DataStore. This can include removing the temporary test 
folder, bucket etc.
+     */
+    void dispose(DataStore dataStore);
+
+    /**
+     * Return whether this fixture is available, for example if the necessary 
configuration is present.
+     */
+    boolean isAvailable();
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/DataStoreFixture.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/FileDataStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/FileDataStoreFixture.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/FileDataStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/FileDataStoreFixture.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,42 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary.fixtures.datastore;
+
+import org.apache.jackrabbit.core.data.DataStore;
+import org.apache.jackrabbit.core.data.FileDataStore;
+import org.jetbrains.annotations.NotNull;
+
+public class FileDataStoreFixture implements DataStoreFixture {
+
+    @NotNull
+    @Override
+    public DataStore createDataStore() {
+        return new FileDataStore();
+    }
+
+    @Override
+    public void dispose(DataStore dataStore) {
+        // nothing to do, using directory inside repository that gets cleaned 
up by NodeStoreFixture dispose
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/FileDataStoreFixture.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/S3DataStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/S3DataStoreFixture.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/S3DataStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/S3DataStoreFixture.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.jcr.binary.fixtures.datastore;
+
+import java.util.Properties;
+import java.util.UUID;
+
+import org.apache.jackrabbit.core.data.DataStore;
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.oak.blob.cloud.s3.S3Backend;
+import org.apache.jackrabbit.oak.blob.cloud.s3.S3Constants;
+import org.apache.jackrabbit.oak.blob.cloud.s3.S3DataStore;
+import org.apache.jackrabbit.oak.blob.cloud.s3.Utils;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.binary.fixtures.nodestore.FixtureUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.BucketAccelerateConfiguration;
+import com.amazonaws.services.s3.model.BucketAccelerateStatus;
+import com.amazonaws.services.s3.model.CreateBucketRequest;
+import com.amazonaws.services.s3.model.ObjectListing;
+import com.amazonaws.services.s3.model.S3ObjectSummary;
+import com.amazonaws.services.s3.model.SetBucketAccelerateConfigurationRequest;
+
+/**
+ * Fixture for S3DataStore based on an aws.properties config file. It creates
+ * a new temporary Azure Blob Container for each DataStore created.
+ *
+ * <p>
+ * Note: when using this, it's highly recommended to reuse the NodeStores 
across multiple tests (using
+ * {@link 
org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest#AbstractRepositoryTest(NodeStoreFixture,
 boolean) AbstractRepositoryTest(fixture, true)})
+ * otherwise it will be slower and can lead to out of memory issues if there 
are many tests.
+ *
+ * <p>
+ * Test buckets are named "direct-binary-test-...". If some did not get 
cleaned up, you can
+ * list them using the aws cli with this command:
+ * <pre>
+ *     aws s3 ls | grep direct-binary-test-
+ * </pre>
+ *
+ * And after checking, delete them all in one go with this command:
+ * <pre>
+ *     aws s3 ls | grep direct-binary-test- | cut -f 3 -d " " | xargs -n 1 -I 
{} sh -c 'aws s3 rb s3://{} || exit 1'
+ * </pre>
+ */
+public class S3DataStoreFixture implements DataStoreFixture {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Nullable
+    private final Properties s3Props;
+
+    public S3DataStoreFixture() {
+        s3Props = FixtureUtils.loadDataStoreProperties("s3.config", 
"aws.properties", ".aws");
+    }
+
+    @Override
+    public boolean isAvailable() {
+        if (s3Props == null) {
+            log.warn("Skipping S3 DataStore fixture because no S3 properties 
file was found given by " +
+                "'s3.config' system property or named 'aws.properties' or 
'~/.aws/aws.properties'.");
+            return false;
+        }
+        return true;
+    }
+
+    @NotNull
+    @Override
+    public DataStore createDataStore() {
+        if (s3Props == null) {
+            throw new AssertionError("createDataStore() called but this 
fixture is not available");
+        }
+
+        AmazonS3 s3Client = Utils.openService(s3Props);
+
+        // Create a temporary bucket that will be removed at test completion
+        String bucketName = "direct-binary-test-" + 
UUID.randomUUID().toString();
+
+        log.info("Creating S3 test bucket {}", bucketName);
+        CreateBucketRequest createBucket = new CreateBucketRequest(bucketName);
+        s3Client.createBucket(createBucket);
+
+        log.info("Enabling S3 acceleration for bucket {}", bucketName);
+        s3Client.setBucketAccelerateConfiguration(
+            new SetBucketAccelerateConfigurationRequest(bucketName,
+                new BucketAccelerateConfiguration(
+                    BucketAccelerateStatus.Enabled
+                )
+            )
+        );
+
+        s3Client.shutdown();
+
+        // create new properties since azProps is shared for all created 
DataStores
+        Properties clonedS3Props = new Properties(s3Props);
+        clonedS3Props.setProperty(S3Constants.S3_BUCKET, 
createBucket.getBucketName());
+
+        // setup Oak DS
+        S3DataStore dataStore = new S3DataStore();
+        dataStore.setProperties(clonedS3Props);
+        dataStore.setStagingSplitPercentage(0);
+
+        log.info("s3props: " + s3Props.toString());
+
+        return dataStore;
+    }
+
+    @Override
+    public void dispose(DataStore dataStore) {
+        if (dataStore != null && dataStore instanceof S3DataStore) {
+            try {
+                dataStore.close();
+            } catch (DataStoreException e) {
+                log.warn("Issue while disposing DataStore", e);
+            }
+
+            S3DataStore s3DataStore = (S3DataStore) dataStore;
+            String bucketName = ((S3Backend) 
s3DataStore.getBackend()).getBucket();
+
+            if (s3Props == null) {
+                // should be impossible if we created the client successfully 
in createDataStore()
+                log.warn("Could not cleanup and remove S3 bucket {}", 
bucketName);
+                return;
+            }
+            
+            AmazonS3 s3Client = Utils.openService(s3Props);
+
+            // For S3, you have to empty the bucket before removing the bucket 
itself
+            log.info("Emptying S3 test bucket {}", bucketName);
+            
+            ObjectListing listing = s3Client.listObjects(bucketName);
+            while (true) {
+                for (S3ObjectSummary summary : listing.getObjectSummaries()) {
+                    s3Client.deleteObject(bucketName, summary.getKey());
+                }
+                if (! listing.isTruncated()) {
+                    break;
+                }
+                listing = s3Client.listNextBatchOfObjects(listing);
+            }
+            
+            log.info("Removing S3 test bucket {}", bucketName);
+            s3Client.deleteBucket(bucketName);
+
+            s3Client.shutdown();
+        }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/datastore/S3DataStoreFixture.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/DocumentMemoryNodeStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/DocumentMemoryNodeStoreFixture.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/DocumentMemoryNodeStoreFixture.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/DocumentMemoryNodeStoreFixture.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,132 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary.fixtures.nodestore;
+
+import java.io.File;
+import java.io.IOException;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.jackrabbit.core.data.DataStore;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.datastore.DataStoreFixture;
+import org.apache.jackrabbit.oak.jcr.util.ComponentHolder;
+import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+/**
+ * Creates a repository with
+ * - DocumentNodeStore, storing data in-memory
+ * - an optional DataStore provided by DataStoreFixture
+ */
+public class DocumentMemoryNodeStoreFixture extends NodeStoreFixture 
implements ComponentHolder {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final DataStoreFixture dataStoreFixture;
+
+    private final Table<NodeStore, String, Object> components = 
HashBasedTable.create();
+
+    public DocumentMemoryNodeStoreFixture(@Nullable DataStoreFixture 
dataStoreFixture) {
+        this.dataStoreFixture = dataStoreFixture;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        // if a DataStore is configured, it must be available for our 
NodeStore to be available
+        return dataStoreFixture == null || dataStoreFixture.isAvailable();
+    }
+
+    @Override
+    public NodeStore createNodeStore() {
+        try {
+            log.info("Creating NodeStore using " + toString());
+
+            DocumentNodeStoreBuilder<?> documentNodeStoreBuilder = 
DocumentNodeStoreBuilder.newDocumentNodeStoreBuilder();
+
+            File dataStoreFolder = null;
+            BlobStore blobStore = null;
+            DataStore dataStore = null;
+            if (dataStoreFixture != null) {
+                dataStore = dataStoreFixture.createDataStore();
+
+                // init with a new folder inside a temporary one
+                dataStoreFolder = FixtureUtils.createTempFolder();
+                dataStore.init(dataStoreFolder.getAbsolutePath());
+
+                blobStore = new DataStoreBlobStore(dataStore);
+                documentNodeStoreBuilder.setBlobStore(blobStore);
+            }
+
+            NodeStore nodeStore = documentNodeStoreBuilder.build();
+
+            // track all main components
+            if (dataStore != null) {
+                components.put(nodeStore, DataStore.class.getName(), 
dataStore);
+                components.put(nodeStore, DataStore.class.getName() + 
":folder", dataStoreFolder);
+            }
+            if (blobStore != null) {
+                components.put(nodeStore, BlobStore.class.getName(), 
blobStore);
+            }
+
+            return nodeStore;
+        } catch (IOException | RepositoryException e) {
+            throw new AssertionError("Cannot create test repo fixture " + 
toString(), e);
+        }
+    }
+
+    @Override
+    public void dispose(NodeStore nodeStore) {
+        try {
+            if (nodeStore instanceof DocumentNodeStore) {
+                ((DocumentNodeStore)nodeStore).dispose();
+            }
+
+            DataStore dataStore = (DataStore) components.get(nodeStore, 
DataStore.class.getName());
+            if (dataStore != null && dataStoreFixture != null) {
+                dataStoreFixture.dispose(dataStore);
+
+                File dataStoreFolder = (File) components.get(nodeStore, 
DataStore.class.getName() + ":folder");
+                FileUtils.deleteQuietly(dataStoreFolder);
+            }
+        } finally {
+            components.row(nodeStore).clear();
+        }
+    }
+
+    @Override
+    public String toString() {
+        // for nice Junit parameterized test labels
+        return FixtureUtils.getFixtureLabel(this, dataStoreFixture);
+    }
+
+    @Override
+    public <T> T get(NodeStore nodeStore, String componentName) {
+        return (T) components.get(nodeStore, componentName);
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/DocumentMemoryNodeStoreFixture.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/FixtureUtils.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/FixtureUtils.java?rev=1838617&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/FixtureUtils.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/FixtureUtils.java
 Wed Aug 22 08:51:41 2018
@@ -0,0 +1,81 @@
+/*
+ * 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.jackrabbit.oak.jcr.binary.fixtures.nodestore;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import 
org.apache.jackrabbit.oak.jcr.binary.fixtures.datastore.DataStoreFixture;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Utility methods for NodeStoreFixtures and DataStoreFixtures.
+ */
+public abstract class FixtureUtils {
+
+    /** Return a nice label for jUnit Parameterized tests for fixtures */
+    public static String getFixtureLabel(NodeStoreFixture fixture,
+                                  DataStoreFixture dataStoreFixture) {
+        
+        String nodeStoreName = fixture.getClass().getSimpleName();
+        String name = StringUtils.removeEnd(nodeStoreName, "Fixture");
+        if (dataStoreFixture != null) {
+            String dataStoreName = dataStoreFixture.getClass().getSimpleName();
+            return name + "_" + StringUtils.removeEnd(dataStoreName, 
"Fixture");
+        }
+        return name;
+
+    }
+
+    /** Create a temporary folder inside the maven build folder "target". */
+    public static File createTempFolder() throws IOException {
+        // create temp folder inside maven's "target" folder
+        File parent = new File("target");
+        File tempFolder = File.createTempFile("junit", "", parent);
+        tempFolder.delete();
+        tempFolder.mkdir();
+        return tempFolder;
+    }
+
+    /**
+     * Load data store *.properties from path in system property, local file 
or file inside user home directory.
+     * Returns null if no file was found.
+     */
+    @Nullable
+    public static Properties loadDataStoreProperties(String systemProperty,
+                                                     String defaultFileName,
+                                                     String homeFolderName) {
+        Properties props = new Properties();
+        try {
+            File file = new File(System.getProperty(systemProperty, 
defaultFileName));
+            if (!file.exists()) {
+                file = Paths.get(System.getProperty("user.home"), 
homeFolderName, defaultFileName).toFile();
+            }
+            props.load(new FileReader(file));
+        } catch (IOException e) {
+            return null;
+        }
+        return props;
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/binary/fixtures/nodestore/FixtureUtils.java
------------------------------------------------------------------------------
    svn:eol-style = native


Reply via email to