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> objectStoragePropertiesMap = Optional.empty(); // Check the primary store - try { + try + { objectStoragePropertiesMap = Optional.of(primaryStore.getStorageProperties(contentUrl)); - } catch (UnsupportedContentUrlException e) { - if (logger.isTraceEnabled()) { - logger.trace("Primary store could not handle content URL: " + contentUrl); - } + } + catch (UnsupportedContentUrlException e) + { + final String message = String.format(PRIMARY_STORE_COULD_NOT_HANDLE_CONTENT_URL, contentUrl); + logger.trace(message); } - if (objectStoragePropertiesMap.isEmpty()) {// the content is not in the primary store so we have to go looking for it - for (ContentStore store : secondaryStores) { - try { + if (objectStoragePropertiesMap.isEmpty() || + objectStoragePropertiesMap.get().isEmpty()) {// the content is not in the primary store so we have to go looking for it + for (ContentStore store : secondaryStores) + { + try + { objectStoragePropertiesMap = Optional.of(store.getStorageProperties(contentUrl)); - } catch (UnsupportedContentUrlException e) { - if (logger.isTraceEnabled()) { - logger.trace("Secondary store " + store + " could not handle content URL: " + contentUrl); - } + } + catch (UnsupportedContentUrlException e) + { + final String message = String.format(SECONDARY_STORE_COULD_NOT_HANDLE_CONTENT_URL, store, contentUrl); + logger.trace(message); } - if (objectStoragePropertiesMap.isPresent()) { + if (objectStoragePropertiesMap.isPresent()) + { return objectStoragePropertiesMap.get(); } } @@ -444,8 +447,122 @@ public class AggregatingContentStore extends AbstractContentStore } return objectStoragePropertiesMap.orElse(Collections.emptyMap()); - } finally { + } + finally + { readLock.unlock(); } } + + /** + * {@inheritDoc} + */ + @Experimental + @Override + public boolean requestSendContentToArchive(final String contentUrl, Map archiveParams) + { + return callContentArchiveRequest(contentUrl, archiveParams, false); + } + + /** + * {@inheritDoc} + */ + @Experimental + @Override + public boolean requestRestoreContentFromArchive(final String contentUrl, final Map restoreParams) + { + return callContentArchiveRequest(contentUrl, restoreParams, true); + } + + private boolean callContentArchiveRequest(final String contentUrl, final Map requestParams, final boolean restore) + { + checkPrimaryStore(); + // get a read lock so that we are sure that no replication is underway + readLock.lock(); + boolean archiveRequestSucceeded = false; + boolean primaryContentUrlUnsupported = false; + boolean secondaryContentUrlUnsupported = false; + try + { + // Check the primary store + try + { + archiveRequestSucceeded = archiveRequestResult(contentUrl, requestParams, restore, primaryStore); + } + catch (UnsupportedOperationException e) + { + final String message = String.format("Primary store does not handle this operation for content URL: %s", contentUrl); + logger.trace(message); + } + catch (UnsupportedContentUrlException e) { + final String message = String.format(PRIMARY_STORE_COULD_NOT_HANDLE_CONTENT_URL, contentUrl); + logger.trace(message); + primaryContentUrlUnsupported = true; + } + + if (archiveRequestSucceeded) + { + return true; + } + else + { // the content is not in the primary store so we have to go looking for it + for (ContentStore store : secondaryStores) + { + try + { + archiveRequestSucceeded = archiveRequestResult(contentUrl, requestParams, restore, store); + } catch (UnsupportedOperationException e) + { + final String message = + String.format("Secondary store %s does not handle this operation for content URL: %s", store, + contentUrl); + logger.trace(message); + } + catch (UnsupportedContentUrlException e) + { + secondaryContentUrlUnsupported = true; + final String message = String.format(SECONDARY_STORE_COULD_NOT_HANDLE_CONTENT_URL, store, contentUrl); + logger.trace(message); + } + } + } + if (archiveRequestSucceeded) + { + return true; + } + else if (primaryContentUrlUnsupported || secondaryContentUrlUnsupported) + { + return callSuperMethod(contentUrl, requestParams, restore); + } + + return callSuperMethod(contentUrl, requestParams, restore); + } + finally + { + readLock.unlock(); + } + } + + private boolean callSuperMethod(String contentUrl, Map requestParams, boolean restore) + { + return restore ? + super.requestRestoreContentFromArchive(contentUrl, requestParams) : + super.requestSendContentToArchive(contentUrl, requestParams); + } + + private boolean archiveRequestResult(String contentUrl, Map requestParams, boolean restore, + ContentStore store) + { + return restore ? + store.requestRestoreContentFromArchive(contentUrl, requestParams) : + store.requestSendContentToArchive(contentUrl, requestParams); + } + + private void checkPrimaryStore() + { + if (primaryStore == null) + { + throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED); + } + } } diff --git a/repository/src/main/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java b/repository/src/main/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java index a2d2a70718..607c29268e 100644 --- a/repository/src/main/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java +++ b/repository/src/main/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java @@ -25,6 +25,7 @@ */ package org.alfresco.repo.tenant; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -272,6 +273,9 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC } } + /** + * {@inheritDoc} + */ @Experimental @Override public Map getStorageProperties(String contentUrl) @@ -279,5 +283,25 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC return getTenantContentStore().getStorageProperties(contentUrl); } + /** + * {@inheritDoc} + */ + @Override + @Experimental + public boolean requestSendContentToArchive(String contentUrl, Map archiveParams) + { + return getTenantContentStore().requestSendContentToArchive(contentUrl, archiveParams); + } + + /** + * {@inheritDoc} + */ + @Override + @Experimental + public boolean requestRestoreContentFromArchive(String contentUrl, Map restoreParams) + { + return getTenantContentStore().requestRestoreContentFromArchive(contentUrl, restoreParams); + } + protected abstract ContentStore initContentStore(ApplicationContext ctx, String contentRoot); } diff --git a/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java b/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java index 830770b13a..4fdc8e21f2 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/repository/ContentService.java @@ -33,6 +33,7 @@ import org.alfresco.service.Experimental; import org.alfresco.service.cmr.dictionary.InvalidTypeException; import org.alfresco.service.namespace.QName; +import java.io.Serializable; import java.util.Collections; import java.util.Map; @@ -258,10 +259,52 @@ public interface ContentService * @param propertyQName the name of the property, which must be of type content * @return Returns a key-value (String-String) collection of storage headers/properties with their respective values for a given {@link NodeRef}. */ - @Auditable + @Auditable(parameters = {"nodeRef", "propertyQName"}) @Experimental default Map getStorageProperties(NodeRef nodeRef, QName propertyQName) { 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 nodeRef a reference to a node having a content property + * @param propertyQName the name of the property, which must be of type content + * @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 method not implemented + */ + @Auditable(parameters = {"nodeRef", "propertyQName"}) + @Experimental + default boolean requestSendContentToArchive(NodeRef nodeRef, QName propertyQName, + Map archiveParams) + { + throw new UnsupportedOperationException("Request to archive content is not supported by content service."); + } + + /** + * 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 nodeRef a reference to a node having a content property + * @param propertyQName the name of the property, which must be of type content + * @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 method not implemented + */ + @Auditable(parameters = {"nodeRef", "propertyQName", "restoreParams"}) + @Experimental + default boolean requestRestoreContentFromArchive(NodeRef nodeRef, QName propertyQName, Map restoreParams) + { + throw new UnsupportedOperationException("Request to restore content from archive is not supported by content service."); + } } diff --git a/repository/src/main/resources/alfresco/public-services-security-context.xml b/repository/src/main/resources/alfresco/public-services-security-context.xml index a819baee45..a1887929ff 100644 --- a/repository/src/main/resources/alfresco/public-services-security-context.xml +++ b/repository/src/main/resources/alfresco/public-services-security-context.xml @@ -498,6 +498,8 @@ org.alfresco.service.cmr.repository.ContentService.requestContentDirectUrl=ACL_NODE.0.sys:base.ReadContent org.alfresco.service.cmr.repository.ContentService.isContentDirectUrlEnabled=ACL_ALLOW org.alfresco.service.cmr.repository.ContentService.getStorageProperties=ACL_NODE.0.sys:base.ReadContent + org.alfresco.service.cmr.repository.ContentService.requestSendContentToArchive=ACL_NODE.0.sys:base.WriteContent + org.alfresco.service.cmr.repository.ContentService.requestRestoreContentFromArchive=ACL_NODE.0.sys:base.WriteContent org.alfresco.service.cmr.repository.ContentService.*=ACL_DENY diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 365ab1741b..168e763b88 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -187,6 +187,7 @@ import org.junit.runners.Suite; org.alfresco.repo.content.filestore.FileIOTest.class, org.alfresco.repo.content.filestore.SpoofedTextContentReaderTest.class, org.alfresco.repo.content.ContentDataTest.class, + org.alfresco.repo.content.replication.AggregatingContentStoreUnitTest.class, org.alfresco.service.cmr.repository.TransformationOptionLimitsTest.class, org.alfresco.service.cmr.repository.TransformationOptionPairTest.class, org.alfresco.repo.content.transform.TransformerConfigTestSuite.class, diff --git a/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java b/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java index 658f9a25a3..3450885101 100644 --- a/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java @@ -30,7 +30,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -54,6 +53,7 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.io.Serializable; import java.util.Collections; import java.util.Map; @@ -205,6 +205,75 @@ public class ContentServiceImplUnitTest }); } + @Test + public void shouldRequestSendContentToArchiveSucceed() + { + final boolean expectedResult = true; + final Map archiveParams = Collections.emptyMap(); + when(mockContentStore.requestSendContentToArchive(SOME_CONTENT_URL, archiveParams)).thenReturn(expectedResult); + boolean sendContentToArchive = contentService.requestSendContentToArchive(NODE_REF, ContentModel.PROP_CONTENT, archiveParams); + assertEquals(expectedResult, sendContentToArchive); + } + + @Test + public void requestSendContentToArchiveThrowsExceptionWhenNotImplemented() + { + final Map archiveParams = Collections.emptyMap(); + when(mockContentStore.requestSendContentToArchive(SOME_CONTENT_URL, archiveParams)).thenCallRealMethod(); + assertThrows(UnsupportedOperationException.class, () -> { + contentService.requestSendContentToArchive(NODE_REF, ContentModel.PROP_CONTENT, archiveParams); + }); + } + + @Test + public void requestSendContentToArchiveThrowsExceptionWhenContentNotFound() + { + final String dummyContentProperty = "dummy"; + final Map archiveParams = Collections.emptyMap(); + when(mockNodeService.getProperty(NODE_REF_2, ContentModel.PROP_CONTENT)).thenReturn(dummyContentProperty); + when(mockDictionaryService.getProperty(ContentModel.PROP_CONTENT)).thenReturn(null); + + assertThrows(IllegalArgumentException.class, () -> { + contentService.requestSendContentToArchive(NODE_REF_2, ContentModel.PROP_CONTENT, archiveParams); + }); + } + + @Test + public void requestRestoreContentFromArchiveThrowsExceptionWhenContentNotFound() + { + final String dummyContentProperty = "dummy"; + when(mockNodeService.getProperty(NODE_REF_2, ContentModel.PROP_CONTENT)).thenReturn(dummyContentProperty); + when(mockDictionaryService.getProperty(ContentModel.PROP_CONTENT)).thenReturn(null); + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + + assertThrows(IllegalArgumentException.class, () -> { + contentService.requestRestoreContentFromArchive(NODE_REF_2, ContentModel.PROP_CONTENT, restoreParams); + }); + } + + @Test + public void shouldRequestRestoreContentFromArchiveSucceed() + { + final boolean expectedResult = true; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + when(mockContentStore.requestRestoreContentFromArchive(SOME_CONTENT_URL, restoreParams)).thenReturn(expectedResult); + + boolean restoreContentFromArchive = + contentService.requestRestoreContentFromArchive(NODE_REF, ContentModel.PROP_CONTENT, restoreParams); + + assertEquals(expectedResult, restoreContentFromArchive); + } + + @Test + public void requestRestoreContentFromArchiveThrowsExceptionWhenNotImplemented() + { + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + when(mockContentStore.requestRestoreContentFromArchive(SOME_CONTENT_URL, restoreParams)).thenCallRealMethod(); + assertThrows(UnsupportedOperationException.class, () -> { + contentService.requestRestoreContentFromArchive(NODE_REF, ContentModel.PROP_CONTENT, restoreParams); + }); + } + /* Helper method to set system-wide direct access url configuration settings */ private void setupSystemWideDirectAccessConfig(Boolean isEnabled) { diff --git a/repository/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java b/repository/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java index b61db5d123..51e58cc7c4 100644 --- a/repository/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/caching/CachingContentStoreTest.java @@ -29,6 +29,7 @@ package org.alfresco.repo.content.caching; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -45,10 +46,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; +import java.io.Serializable; +import java.util.Collections; import java.util.Locale; import java.util.Map; import org.alfresco.repo.content.ContentContext; +import org.alfresco.repo.content.ContentRestoreParams; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.caching.quota.QuotaManagerStrategy; import org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategy; @@ -540,4 +544,54 @@ public class CachingContentStoreTest Map storageProperties = cachingStore.getStorageProperties("url"); assertTrue(storageProperties.isEmpty()); } + + @Test + public void shouldCompleteArchiveContentRequest() + { + final boolean expectedResult = true; + final String contentUrl = "url"; + final Map archiveParams = Collections.emptyMap(); + when(backingStore.requestSendContentToArchive(contentUrl, archiveParams)).thenReturn(expectedResult); + + final boolean sendContentToArchive = cachingStore.requestSendContentToArchive(contentUrl, archiveParams); + + assertEquals(expectedResult, sendContentToArchive); + } + + @Test + public void shouldThrowExceptionOnArchiveContentRequest() + { + final String contentUrl = "url"; + final Map archiveParams = Collections.emptyMap(); + when(backingStore.requestSendContentToArchive(contentUrl, archiveParams)).thenCallRealMethod(); + + assertThrows(UnsupportedOperationException.class, () -> { + cachingStore.requestSendContentToArchive(contentUrl, archiveParams); + }); + } + + @Test + public void shouldCompleteRestoreContentFromArchiveRequest() + { + final String contentUrl = "url"; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + final boolean expectedResult = true; + when(backingStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenReturn(expectedResult); + + final boolean sendContentToArchive = cachingStore.requestRestoreContentFromArchive(contentUrl, restoreParams); + + assertEquals(expectedResult, sendContentToArchive); + } + + @Test + public void shouldThrowExceptionOnRestoreContentFromArchiveRequest() + { + final String contentUrl = "url"; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + when(backingStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenCallRealMethod(); + + assertThrows(UnsupportedOperationException.class, () -> { + cachingStore.requestRestoreContentFromArchive(contentUrl, restoreParams); + }); + } } diff --git a/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java b/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java index 3fdb1b3245..1fdbbaec94 100644 --- a/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreTest.java @@ -27,9 +27,7 @@ package org.alfresco.repo.content.replication; import java.io.File; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; import org.alfresco.repo.content.AbstractWritableContentStoreTest; import org.alfresco.repo.content.ContentContext; @@ -60,8 +58,6 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests read and write functionality for the aggregating store. @@ -316,48 +312,4 @@ public class AggregatingContentStoreTest extends AbstractWritableContentStoreTes assertNotNull(directAccessUrl); } - @Test - public void shouldReturnStoragePropertiesFromPrimaryStore() - { - final String contentUrl = "url"; - final Map primaryStorePropertiesMap = Map.of(X_AMZ_HEADER_1, VALUE_1, X_AMZ_HEADER_2, VALUE_2);; - when(primaryStoreMock.getStorageProperties(contentUrl)).thenReturn(primaryStorePropertiesMap); - - final Map storageProperties = aggregatingStore.getStorageProperties(contentUrl); - - assertFalse(storageProperties.isEmpty()); - assertEquals(primaryStorePropertiesMap, storageProperties); - verify(secondaryStoreMock, times(0)).getStorageProperties(contentUrl); - } - - @Test - public void shouldReturnStoragePropertiesFromSecondaryStore() - { - final String contentUrl = "url"; - final Map secondaryStorePropertiesMap = Map.of(X_AMZ_HEADER_1, VALUE_1, X_AMZ_HEADER_2, VALUE_2);; - when(primaryStoreMock.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); - when(secondaryStoreMock.getStorageProperties(contentUrl)).thenReturn(secondaryStorePropertiesMap); - - final Map storageProperties = aggregatingStore.getStorageProperties(contentUrl); - - assertFalse(storageProperties.isEmpty()); - assertEquals(secondaryStorePropertiesMap, storageProperties); - verify(secondaryStoreMock, times(1)).getStorageProperties(contentUrl); - verify(primaryStoreMock, times(1)).getStorageProperties(contentUrl); - } - - @Test - public void shouldReturnEmptyStorageProperties() - { - final String contentUrl = "url"; - when(primaryStoreMock.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); - when(secondaryStoreMock.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); - - final Map storageProperties = aggregatingStore.getStorageProperties(contentUrl); - - assertTrue(storageProperties.isEmpty()); - verify(secondaryStoreMock, times(1)).getStorageProperties(contentUrl); - verify(primaryStoreMock, times(1)).getStorageProperties(contentUrl); - } - } diff --git a/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreUnitTest.java b/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreUnitTest.java new file mode 100644 index 0000000000..e40e03a71e --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/content/replication/AggregatingContentStoreUnitTest.java @@ -0,0 +1,219 @@ +/* + * #%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.replication; + +import org.alfresco.repo.content.ContentRestoreParams; +import org.alfresco.repo.content.ContentStore; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests class for {@code AggregatingContentStore} + * + * Currently does not cover all methods. + * + * @author mpichura + */ +@RunWith(MockitoJUnitRunner.class) +public class AggregatingContentStoreUnitTest +{ + private static final String X_AMZ_HEADER_1 = "x-amz-header1"; + private static final String VALUE_1 = "value1"; + private static final String X_AMZ_HEADER_2 = "x-amz-header2"; + private static final String VALUE_2 = "value2"; + + private List secondaryStores; + @Mock + ContentStore primaryStore; + @Mock + ContentStore secondaryStore; + + @InjectMocks + private AggregatingContentStore objectUnderTest; + + @Before + public void setUp() { + secondaryStores = List.of(secondaryStore); + objectUnderTest.setSecondaryStores(secondaryStores); + } + + + @Test + public void shouldReturnStoragePropertiesFromPrimaryStore() + { + final String contentUrl = "url"; + final Map primaryStorePropertiesMap = Map.of(X_AMZ_HEADER_1, VALUE_1, X_AMZ_HEADER_2, VALUE_2);; + when(primaryStore.getStorageProperties(contentUrl)).thenReturn(primaryStorePropertiesMap); + + final Map storageProperties = objectUnderTest.getStorageProperties(contentUrl); + + assertFalse(storageProperties.isEmpty()); + assertEquals(primaryStorePropertiesMap, storageProperties); + verify(secondaryStore, times(0)).getStorageProperties(contentUrl); + } + + @Test + public void shouldReturnStoragePropertiesFromSecondaryStore() + { + final String contentUrl = "url"; + final Map secondaryStorePropertiesMap = Map.of(X_AMZ_HEADER_1, VALUE_1, X_AMZ_HEADER_2, VALUE_2);; + when(primaryStore.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + when(secondaryStore.getStorageProperties(contentUrl)).thenReturn(secondaryStorePropertiesMap); + + final Map storageProperties = objectUnderTest.getStorageProperties(contentUrl); + + assertFalse(storageProperties.isEmpty()); + assertEquals(secondaryStorePropertiesMap, storageProperties); + verify(secondaryStore, times(1)).getStorageProperties(contentUrl); + verify(primaryStore, times(1)).getStorageProperties(contentUrl); + } + + @Test + public void shouldReturnEmptyStorageProperties() + { + final String contentUrl = "url"; + when(primaryStore.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + when(secondaryStore.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + + final Map storageProperties = objectUnderTest.getStorageProperties(contentUrl); + + assertTrue(storageProperties.isEmpty()); + verify(primaryStore, times(1)).getStorageProperties(contentUrl); + } + + @Test + public void shouldRequestContentArchiveThroughPrimaryStore() + { + final String contentUrl = "url"; + final boolean expectedResult = true; + final Map archiveParams = Collections.emptyMap(); + + when(primaryStore.requestSendContentToArchive(contentUrl, archiveParams)).thenReturn(expectedResult); + + boolean sendContentToArchive = objectUnderTest.requestSendContentToArchive(contentUrl, archiveParams); + + assertEquals(expectedResult, sendContentToArchive); + verify(secondaryStore, never()).requestSendContentToArchive(contentUrl,archiveParams); + } + + @Test + public void shouldRequestContentArchiveThroughSecondaryStore() + { + final String contentUrl = "url"; + final boolean expectedResult = true; + final Map archiveParams = Collections.emptyMap(); + + when(primaryStore.requestSendContentToArchive(contentUrl, archiveParams)).thenThrow(UnsupportedOperationException.class); + when(secondaryStore.requestSendContentToArchive(contentUrl, archiveParams)).thenReturn(expectedResult); + + boolean sendContentToArchive = objectUnderTest.requestSendContentToArchive(contentUrl, archiveParams); + + assertEquals(expectedResult, sendContentToArchive); + verify(primaryStore, times(1)).requestSendContentToArchive(contentUrl, archiveParams); + verify(secondaryStore, times(1)).requestSendContentToArchive(contentUrl, archiveParams); + } + + @Test + public void shouldThrowExceptionWhenRequestContentArchiveNotImplemented() + { + final String contentUrl = "url"; + when(primaryStore.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + when(secondaryStore.getStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + + final Map storageProperties = objectUnderTest.getStorageProperties(contentUrl); + + assertTrue(storageProperties.isEmpty()); + verify(primaryStore, times(1)).getStorageProperties(contentUrl); + } + + @Test + public void shouldRequestRestoreContentFromArchiveThroughPrimaryStore() + { + final String contentUrl = "url"; + final boolean expectedResult = true; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + + when(primaryStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenReturn(expectedResult); + + boolean sendContentToArchive = objectUnderTest.requestRestoreContentFromArchive(contentUrl, restoreParams); + + assertEquals(expectedResult, sendContentToArchive); + verify(secondaryStore, never()).requestRestoreContentFromArchive(contentUrl, restoreParams); + } + + @Test + public void shouldRequestRestoreContentFromArchiveThroughSecondaryStore() + { + final String contentUrl = "url"; + final boolean expectedResult = true; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + + when(primaryStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenThrow(UnsupportedOperationException.class); + when(secondaryStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenReturn(expectedResult); + + boolean sendContentToArchive = objectUnderTest.requestRestoreContentFromArchive(contentUrl, restoreParams); + + assertEquals(expectedResult, sendContentToArchive); + verify(primaryStore, times(1)).requestRestoreContentFromArchive(contentUrl, restoreParams); + verify(secondaryStore, times(1)).requestRestoreContentFromArchive(contentUrl, restoreParams); + } + + @Test + public void shouldThrowExceptionWhenRequestRestoreContentFromArchiveNotImplemented() + { + final String contentUrl = "url"; + final Map restoreParams = Map.of(ContentRestoreParams.RESTORE_PRIORITY.name(), "High"); + when(primaryStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenCallRealMethod(); + when(secondaryStore.requestRestoreContentFromArchive(contentUrl, restoreParams)).thenCallRealMethod(); + + assertThrows(UnsupportedOperationException.class, () -> { + objectUnderTest.requestRestoreContentFromArchive(contentUrl, restoreParams); + }); + + verify(primaryStore, times(1)).requestRestoreContentFromArchive(contentUrl, restoreParams); + verify(secondaryStore, times(1)).requestRestoreContentFromArchive(contentUrl, restoreParams); + } + +}