From 4f1397eeee65cd260b3c27f18ee0193c87fdac02 Mon Sep 17 00:00:00 2001
From: mpichura <41297682+mpichura@users.noreply.github.com>
Date: Wed, 8 Dec 2021 16:33:20 +0100
Subject: [PATCH] ACS-2200: Java API for archive/archive-restore content (#825)
* ACS-2200: Java API for archive/archive-restore content + unit tests.
* Bump restapi from 1.64 to 1.65 (#795)
* Bump utility from 3.0.45 to 3.0.47 (#794)
* ACS-2200: Applying review comments.
* ACS-2200: Applying review comments.
* ACS-2200: Adding new unit test to suite.
* ACS-2200: Adding optional archive params to archive operation.
* Bump restapi from 1.64 to 1.65 (#795)
* Bump utility from 3.0.45 to 3.0.47 (#794)
* ACS-2200: Applying review comments.
* ACS-2200: Java API for archive/archive-restore content + unit tests.
---
.../repo/content/ContentRestoreParams.java | 49 ++++
.../alfresco/repo/content/ContentStore.java | 38 +++
.../content/AbstractRoutingContentStore.java | 75 +++++-
.../repo/content/ContentServiceImpl.java | 44 +++-
.../content/caching/CachingContentStore.java | 24 ++
.../replication/AggregatingContentStore.java | 179 +++++++++++---
.../AbstractTenantRoutingContentStore.java | 24 ++
.../cmr/repository/ContentService.java | 45 +++-
.../public-services-security-context.xml | 2 +
.../java/org/alfresco/AllUnitTestsSuite.java | 1 +
.../content/ContentServiceImplUnitTest.java | 71 +++++-
.../caching/CachingContentStoreTest.java | 54 +++++
.../AggregatingContentStoreTest.java | 48 ----
.../AggregatingContentStoreUnitTest.java | 219 ++++++++++++++++++
14 files changed, 773 insertions(+), 100 deletions(-)
create mode 100644 data-model/src/main/java/org/alfresco/repo/content/ContentRestoreParams.java
create mode 100644 repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreUnitTest.java
diff --git a/data-model/src/main/java/org/alfresco/repo/content/ContentRestoreParams.java b/data-model/src/main/java/org/alfresco/repo/content/ContentRestoreParams.java
new file mode 100644
index 0000000000..b9e63b5e42
--- /dev/null
+++ b/data-model/src/main/java/org/alfresco/repo/content/ContentRestoreParams.java
@@ -0,0 +1,49 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2021 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.repo.content;
+
+import org.alfresco.service.Experimental;
+
+/**
+ * Enumeration with values for archive-restore parameter keys.
+ * Values of this enum should be used as keys when requesting for content restore from archive.
+ * Subject to expand/change.
+ *
+ * @author mpichura
+ */
+@Experimental
+public enum ContentRestoreParams
+{
+ /**
+ * Restore expiry in days. Corresponding value should be integer.
+ */
+ EXPIRY_DAYS,
+ /**
+ * Priority for restore from archive. Corresponding value should one of Standard/High
+ */
+ RESTORE_PRIORITY
+}
diff --git a/data-model/src/main/java/org/alfresco/repo/content/ContentStore.java b/data-model/src/main/java/org/alfresco/repo/content/ContentStore.java
index fe01a1d55e..988ae6a4a4 100644
--- a/data-model/src/main/java/org/alfresco/repo/content/ContentStore.java
+++ b/data-model/src/main/java/org/alfresco/repo/content/ContentStore.java
@@ -34,6 +34,7 @@ import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.DirectAccessUrl;
+import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
@@ -344,4 +345,41 @@ public interface ContentStore
{
return Collections.emptyMap();
}
+
+ /**
+ * Submit a request to send content to archive (offline) state.
+ * If no connector is present or connector is not supporting sending to archive, then {@link UnsupportedOperationException} will be returned.
+ * Specific connector will decide which storage class/tier will be set for content.
+ * This method is experimental and subject to changes.
+ *
+ * @param contentUrl the URL of the content which is to be archived.
+ * @param archiveParams a map of String-Serializable parameters defining Storage Provider specific request parameters (can be empty).
+ * @return true when request successful, false when unsuccessful.
+ * @throws UnsupportedOperationException when store is unable to handle request.
+ */
+ @Experimental
+ default boolean requestSendContentToArchive(String contentUrl, Map archiveParams)
+ {
+ throw new UnsupportedOperationException("Request to archive content is not supported by this content store.");
+ }
+
+ /**
+ * Submit a request to restore content from archive (offline) state.
+ * If no connector is present or connector is not supporting restoring fom archive, then {@link UnsupportedOperationException} will be returned.
+ * One of input parameters of this method is a map (String-Serializable) of Storage Provider specific input needed to perform proper restore.
+ * Keys of this map should be restricted to {@code ContentRestoreParams} enumeration.
+ * For AWS S3 map can indicating expiry days, Glacier restore tier.
+ * For Azure Blob map can indicate rehydrate priority.
+ * This method is experimental and subject to changes.
+ *
+ * @param contentUrl the URL of the content which is to be archived.
+ * @param restoreParams a map of String-Serializable parameters defining Storage Provider specific request parameters (can be empty).
+ * @return true when request successful, false when unsuccessful.
+ * @throws UnsupportedOperationException when store is unable to handle request.
+ */
+ @Experimental
+ default boolean requestRestoreContentFromArchive(String contentUrl, Map restoreParams)
+ {
+ throw new UnsupportedOperationException("Request to restore content from archive is not supported by this content store.");
+ }
}
diff --git a/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java b/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java
index 24b0e722ba..6f13ac2496 100644
--- a/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java
+++ b/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java
@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.content;
+import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -420,23 +421,77 @@ public abstract class AbstractRoutingContentStore implements ContentStore
return deleted;
}
+ /**
+ * {@inheritDoc}
+ */
@Override
@Experimental
- public Map getStorageProperties(String contentUrl) {
+ public Map getStorageProperties(String contentUrl)
+ {
ContentStore contentStore = selectReadStore(contentUrl);
- if (contentStore == null) {
- if (logger.isTraceEnabled()) {
- logger.trace("Storage properties not found for content URL: " + contentUrl);
- }
+ if (contentStore == null)
+ {
+ logNoContentStore(contentUrl);
return Collections.emptyMap();
}
- if (logger.isTraceEnabled()) {
- logger.trace("Getting storage properties from store: \n" +
- " Content URL: " + contentUrl + "\n" +
- " Store: " + contentStore);
- }
+ final String message = "Getting storage properties from store: ";
+ logExecution(contentUrl, contentStore, message);
return contentStore.getStorageProperties(contentUrl);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @Experimental
+ public boolean requestSendContentToArchive(String contentUrl, Map archiveParams)
+ {
+ final ContentStore contentStore = selectReadStore(contentUrl);
+ if (contentStore == null)
+ {
+ logNoContentStore(contentUrl);
+ return ContentStore.super.requestSendContentToArchive(contentUrl, archiveParams);
+ }
+ final String message = "Sending content to archive: ";
+ logExecution(contentUrl, contentStore, message);
+ return contentStore.requestSendContentToArchive(contentUrl, archiveParams);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @Experimental
+ public boolean requestRestoreContentFromArchive(String contentUrl, Map restoreParams)
+ {
+ final ContentStore contentStore = selectReadStore(contentUrl);
+ if (contentStore == null)
+ {
+ logNoContentStore(contentUrl);
+ return ContentStore.super.requestRestoreContentFromArchive(contentUrl, restoreParams);
+ }
+ final String message = "Restoring content from archive: ";
+ logExecution(contentUrl, contentStore, message);
+ return ContentStore.super.requestRestoreContentFromArchive(contentUrl, restoreParams);
+ }
+
+ private void logExecution(final String contentUrl, final ContentStore contentStore, final String message)
+ {
+ if (logger.isTraceEnabled())
+ {
+ logger.trace(message + "\n" +
+ " Content URL: " + contentUrl + "\n" +
+ " Store: " + contentStore);
+ }
+ }
+
+ private void logNoContentStore(String contentUrl)
+ {
+ if (logger.isTraceEnabled())
+ {
+ logger.trace("Content Store not found for content URL: " + contentUrl);
+ }
+ }
}
diff --git a/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java b/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java
index 733089383d..3705cc0923 100644
--- a/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java
+++ b/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java
@@ -104,7 +104,7 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa
private boolean ignoreEmptyContent;
private SystemWideDirectUrlConfig systemWideDirectUrlConfig;
-
+
/** pre-configured allow list of media/mime types, eg. specific types of images & also pdf */
private Set nonAttachContentTypes = Collections.emptySet();
@@ -155,7 +155,7 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa
this.systemWideDirectUrlConfig = systemWideDirectUrlConfig;
}
- public void setNonAttachContentTypes(String nonAttachAllowListStr)
+ public void setNonAttachContentTypes(String nonAttachAllowListStr)
{
if ((nonAttachAllowListStr != null) && (! nonAttachAllowListStr.isEmpty()))
{
@@ -671,16 +671,31 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa
@Experimental
public Map getStorageProperties(NodeRef nodeRef, QName propertyQName)
{
- final ContentData contentData = getContentData(nodeRef, propertyQName);
-
- if (contentData == null || contentData.getContentUrl() == null)
- {
- throw new IllegalArgumentException("The supplied nodeRef " + nodeRef + " and property name: " + propertyQName + " has no content.");
- }
-
+ final ContentData contentData = getContentDataOrThrowError(nodeRef, propertyQName);
return store.getStorageProperties(contentData.getContentUrl());
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean requestSendContentToArchive(NodeRef nodeRef, QName propertyQName,
+ Map archiveParams)
+ {
+ final ContentData contentData = getContentDataOrThrowError(nodeRef, propertyQName);
+ return store.requestSendContentToArchive(contentData.getContentUrl(), archiveParams);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean requestRestoreContentFromArchive(NodeRef nodeRef, QName propertyQName, Map restoreParams)
+ {
+ final ContentData contentData = getContentDataOrThrowError(nodeRef, propertyQName);
+ return store.requestRestoreContentFromArchive(contentData.getContentUrl(), restoreParams);
+ }
+
protected String getFileName(NodeRef nodeRef)
{
String fileName = null;
@@ -721,4 +736,15 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa
}
return attachment;
}
+
+ private ContentData getContentDataOrThrowError(NodeRef nodeRef, QName propertyQName)
+ {
+ final ContentData contentData = getContentData(nodeRef, propertyQName);
+
+ if (contentData == null || contentData.getContentUrl() == null)
+ {
+ throw new IllegalArgumentException("The supplied nodeRef " + nodeRef + " and property name: " + propertyQName + " has no content.");
+ }
+ return contentData;
+ }
}
diff --git a/repository/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java b/repository/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java
index b1329574e4..5fe239850e 100644
--- a/repository/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java
+++ b/repository/src/main/java/org/alfresco/repo/content/caching/CachingContentStore.java
@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.content.caching;
+import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
@@ -382,6 +383,9 @@ public class CachingContentStore implements ContentStore, ApplicationEventPublis
}
}
+ /**
+ * {@inheritDoc}
+ */
@Override
@Experimental
public Map getStorageProperties(final String contentUrl)
@@ -389,6 +393,26 @@ public class CachingContentStore implements ContentStore, ApplicationEventPublis
return backingStore.getStorageProperties(contentUrl);
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @Experimental
+ public boolean requestSendContentToArchive(String contentUrl, Map archiveParams)
+ {
+ return backingStore.requestSendContentToArchive(contentUrl, archiveParams);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @Experimental
+ public boolean requestRestoreContentFromArchive(String contentUrl, Map restoreParams)
+ {
+ return backingStore.requestRestoreContentFromArchive(contentUrl, restoreParams);
+ }
+
/**
* Get a ReentrantReadWriteLock for a given URL. The lock is from a pool rather than
* per URL, so some contention is expected.
diff --git a/repository/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java b/repository/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java
index 4c98583b3c..1e2665be20 100644
--- a/repository/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java
+++ b/repository/src/main/java/org/alfresco/repo/content/replication/AggregatingContentStore.java
@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.content.replication;
+import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -70,6 +71,8 @@ public class AggregatingContentStore extends AbstractContentStore
{
private static final Log logger = LogFactory.getLog(AggregatingContentStore.class);
public static final String REPLICATING_CONTENT_STORE_NOT_INITIALISED = "ReplicatingContentStore not initialised";
+ public static final String SECONDARY_STORE_COULD_NOT_HANDLE_CONTENT_URL = "Secondary store %s could not handle content URL: %s";
+ public static final String PRIMARY_STORE_COULD_NOT_HANDLE_CONTENT_URL = "Primary store could not handle content URL: %s";
private ContentStore primaryStore;
private List secondaryStores;
@@ -136,11 +139,8 @@ public class AggregatingContentStore extends AbstractContentStore
*/
public ContentReader getReader(String contentUrl) throws ContentIOException
{
- if (primaryStore == null)
- {
- throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED);
- }
-
+ checkPrimaryStore();
+
// get a read lock so that we are sure that no replication is underway
readLock.lock();
try
@@ -175,10 +175,7 @@ public class AggregatingContentStore extends AbstractContentStore
public boolean exists(String contentUrl)
{
- if (primaryStore == null)
- {
- throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED);
- }
+ checkPrimaryStore();
// get a read lock so that we are sure that no replication is underway
readLock.lock();
@@ -323,10 +320,7 @@ public class AggregatingContentStore extends AbstractContentStore
public DirectAccessUrl requestContentDirectUrl(String contentUrl, boolean attachment, String fileName, String mimetype, Long validFor)
{
- if (primaryStore == null)
- {
- throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED);
- }
+ checkPrimaryStore();
// get a read lock so that we are sure that no replication is underway
readLock.lock();
@@ -405,38 +399,47 @@ public class AggregatingContentStore extends AbstractContentStore
}
}
+ /**
+ * {@inheritDoc}
+ */
@Override
@Experimental
public Map getStorageProperties(String contentUrl)
{
- if (primaryStore == null) {
- throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED);
- }
+ checkPrimaryStore();
// get a read lock so that we are sure that no replication is underway
readLock.lock();
- try {
+ try
+ {
Optional