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 43a62881e8..b754afb79b 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 @@ -26,6 +26,7 @@ package org.alfresco.repo.content; import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.ContentAccessor; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; @@ -33,6 +34,9 @@ import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.DirectAccessUrl; +import java.util.Collections; +import java.util.Map; + /** * Provides low-level retrieval of content @@ -324,4 +328,20 @@ public interface ContentStore throw new UnsupportedOperationException( "Retrieving direct access URLs is not supported by this content store."); } + + /** + * Gets a key-value (String-String) collection of storage headers/properties with their respective values. + * A particular Cloud Connector will fill in that data with Cloud Storage Provider generic data. + * Map may be also filled in with entries consisting of pre-defined Alfresco keys of {@code ObjectStorageProps} and their values. + * If empty Map is returned - no connector is present or connector is not supporting retrieval of the properties + * or cannot determine the properties. + * + * @param contentUrl the URL of the content for which the storage properties are to be retrieved. + * @return Returns a key-value (String-String) collection of storage headers/properties with their respective values. + */ + @Experimental + default Map getObjectStorageProperties(String contentUrl) + { + return Collections.emptyMap(); + } } diff --git a/data-model/src/main/java/org/alfresco/repo/content/ObjectStorageProps.java b/data-model/src/main/java/org/alfresco/repo/content/ObjectStorageProps.java new file mode 100644 index 0000000000..feaa140d3b --- /dev/null +++ b/data-model/src/main/java/org/alfresco/repo/content/ObjectStorageProps.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Data model classes + * %% + * 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 "header" values for Alfresco derived Storage Properties + * Values of this enum should be used when adding Alfresco derived key-value pairs in Storage Properties map. + * Subject to expand/change. + * + * @author mpichura + */ +@Experimental +public enum ObjectStorageProps { + /** + * Object's content is archived and not immediately accessible. + */ + X_ALF_ARCHIVED("x-alf-archived"), + /** + * Object's content retrieval from archive is in progress + */ + X_ALF_ARCHIVE_RESTORE_IN_PROGRESS("x-alf-archive-restore-in-progress"), + /** + * Expiry date and time of object's content retrieved from archive. + * Use YYYYMMDDThhmmssZ (ISO-8601) datetime format when using this value as key in Storage Properties map. + */ + X_ALF_ARCHIVE_RESTORE_EXPIRY("x-alf-archive-restore-expiry"); + + ObjectStorageProps(String value) { + this.value = value; + } + + private final String value; + + public String getValue() { + return value; + } + +} 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 2bb42921c7..942f8e4d52 100644 --- a/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java +++ b/repository/src/main/java/org/alfresco/repo/content/AbstractRoutingContentStore.java @@ -1,44 +1,47 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 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% - */ +/* + * #%L + * Alfresco Repository + * %% + * 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 java.util.List; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; -import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.repo.cache.SimpleCache; -import org.alfresco.service.cmr.repository.ContentIOException; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.util.GUID; -import org.alfresco.util.Pair; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * A store providing support for content store implementations that provide @@ -416,4 +419,24 @@ public abstract class AbstractRoutingContentStore implements ContentStore } return deleted; } + + @Override + @Experimental + public Map getObjectStorageProperties(String contentUrl) { + ContentStore contentStore = selectReadStore(contentUrl); + + if (contentStore == null) { + if (logger.isDebugEnabled()) { + logger.debug("Storage properties not found for content URL: " + contentUrl); + } + return Collections.emptyMap(); + } + + if (logger.isDebugEnabled()) { + logger.debug("Getting storage properties from store: \n" + + " Content URL: " + contentUrl + "\n" + + " Store: " + contentStore); + } + return contentStore.getObjectStorageProperties(contentUrl); + } } 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 8297fb86f8..8901041663 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.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; @@ -35,12 +36,12 @@ import org.alfresco.repo.content.caching.quota.QuotaManagerStrategy; import org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategy; import org.alfresco.repo.content.filestore.FileContentStore; import org.alfresco.repo.content.filestore.SpoofedTextContentReader; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.DirectAccessUrl; -import org.alfresco.service.cmr.repository.NodeRef; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanNameAware; @@ -67,7 +68,7 @@ public class CachingContentStore implements ContentStore, ApplicationEventPublis private final static Log log = LogFactory.getLog(CachingContentStore.class); // NUM_LOCKS absolutely must be a power of 2 for the use of locks to be evenly balanced private final static int numLocks = 256; - private final static ReentrantReadWriteLock[] locks; + private final static ReentrantReadWriteLock[] locks; private ContentStore backingStore; private ContentCache cache; private QuotaManagerStrategy quota = new UnlimitedQuotaStrategy(); @@ -381,6 +382,13 @@ public class CachingContentStore implements ContentStore, ApplicationEventPublis } } + @Override + @Experimental + public Map getObjectStorageProperties(final String contentUrl) + { + return backingStore.getObjectStorageProperties(contentUrl); + } + /** * 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 3528c74c1c..0c48588391 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,7 +25,10 @@ */ package org.alfresco.repo.content.replication; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -36,11 +39,11 @@ import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.UnsupportedContentUrlException; import org.alfresco.repo.content.caching.CachingContentStore; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.DirectAccessUrl; -import org.alfresco.service.cmr.repository.NodeRef; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -64,9 +67,10 @@ import org.apache.commons.logging.LogFactory; * @see CachingContentStore */ 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"; + private ContentStore primaryStore; private List secondaryStores; @@ -134,7 +138,7 @@ public class AggregatingContentStore extends AbstractContentStore { if (primaryStore == null) { - throw new AlfrescoRuntimeException("ReplicatingContentStore not initialised"); + throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED); } // get a read lock so that we are sure that no replication is underway @@ -173,7 +177,7 @@ public class AggregatingContentStore extends AbstractContentStore { if (primaryStore == null) { - throw new AlfrescoRuntimeException("ReplicatingContentStore not initialised"); + throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED); } // get a read lock so that we are sure that no replication is underway @@ -243,7 +247,7 @@ public class AggregatingContentStore extends AbstractContentStore { // get the writer ContentWriter writer = primaryStore.getWriter(ctx); - + return writer; } @@ -321,7 +325,7 @@ public class AggregatingContentStore extends AbstractContentStore { if (primaryStore == null) { - throw new AlfrescoRuntimeException("ReplicatingContentStore not initialised"); + throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED); } // get a read lock so that we are sure that no replication is underway @@ -400,4 +404,48 @@ public class AggregatingContentStore extends AbstractContentStore readLock.unlock(); } } + + @Override + @Experimental + public Map getObjectStorageProperties(String contentUrl) + { + if (primaryStore == null) { + throw new AlfrescoRuntimeException(REPLICATING_CONTENT_STORE_NOT_INITIALISED); + } + + // get a read lock so that we are sure that no replication is underway + readLock.lock(); + try { + Optional> objectStoragePropertiesMap = Optional.empty(); + // Check the primary store + try { + objectStoragePropertiesMap = Optional.of(primaryStore.getObjectStorageProperties(contentUrl)); + } catch (UnsupportedContentUrlException e) { + if (logger.isTraceEnabled()) { + logger.trace("Primary store could not handle content URL: " + contentUrl); + } + } + + if (objectStoragePropertiesMap.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.getObjectStorageProperties(contentUrl)); + } catch (UnsupportedContentUrlException e) { + if (logger.isTraceEnabled()) { + logger.trace("Secondary store " + store + " could not handle content URL: " + contentUrl); + } + } + + if (objectStoragePropertiesMap.isPresent()) { + return objectStoragePropertiesMap.get(); + } + } + throw new UnsupportedContentUrlException(this, contentUrl); + } + return objectStoragePropertiesMap.orElse(Collections.emptyMap()); + + } finally { + readLock.unlock(); + } + } } 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 8f92a4e633..07037f4915 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 @@ -46,6 +46,7 @@ import static org.mockito.Mockito.when; import java.io.IOException; import java.util.Locale; +import java.util.Map; import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentStore; @@ -521,4 +522,22 @@ public class CachingContentStoreTest when(backingStore.requestContentDirectUrl(anyString(), eq(true), anyString(), anyString(), anyLong())).thenReturn(new DirectAccessUrl()); cachingStore.requestContentDirectUrl("url", true,"someFile", "someMimeType", 30L); } + + @Test + public void shouldReturnSomeStorageProperties() + { + final Map propertiesMap = Map.of("x-amz-header1", "value1", "x-amz-header2", "value2"); + final String contentUrl = "url"; + when(backingStore.getObjectStorageProperties(contentUrl)).thenReturn(propertiesMap); + final Map storageProperties = cachingStore.getObjectStorageProperties(contentUrl); + assertFalse(storageProperties.isEmpty()); + assertEquals(propertiesMap, storageProperties); + } + + @Test + public void shouldReturnEmptyStorageProperties() + { + Map storageProperties = cachingStore.getObjectStorageProperties("url"); + assertTrue(storageProperties.isEmpty()); + } } 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 2d8f2d15bd..77299026f2 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,7 +27,9 @@ 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; @@ -58,6 +60,8 @@ 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. @@ -72,7 +76,11 @@ import static org.mockito.Mockito.when; public class AggregatingContentStoreTest extends AbstractWritableContentStoreTest { private static final String SOME_CONTENT = "The No. 1 Ladies' Detective Agency"; - + public static final String X_AMZ_HEADER_1 = "x-amz-header1"; + public static final String VALUE_1 = "value1"; + public static final String X_AMZ_HEADER_2 = "x-amz-header2"; + public static final String VALUE_2 = "value2"; + private AggregatingContentStore aggregatingStore; private ContentStore primaryStore; private List secondaryStores; @@ -307,4 +315,49 @@ public class AggregatingContentStoreTest extends AbstractWritableContentStoreTes directAccessUrl = aggStore.requestContentDirectUrl("urlSecSupported", true, "anyfilename", "anyMimetype", 30L); 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.getObjectStorageProperties(contentUrl)).thenReturn(primaryStorePropertiesMap); + + final Map storageProperties = aggregatingStore.getObjectStorageProperties(contentUrl); + + assertFalse(storageProperties.isEmpty()); + assertEquals(primaryStorePropertiesMap, storageProperties); + verify(secondaryStoreMock, times(0)).getObjectStorageProperties(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.getObjectStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + when(secondaryStoreMock.getObjectStorageProperties(contentUrl)).thenReturn(secondaryStorePropertiesMap); + + final Map storageProperties = aggregatingStore.getObjectStorageProperties(contentUrl); + + assertFalse(storageProperties.isEmpty()); + assertEquals(secondaryStorePropertiesMap, storageProperties); + verify(secondaryStoreMock, times(1)).getObjectStorageProperties(contentUrl); + verify(primaryStoreMock, times(1)).getObjectStorageProperties(contentUrl); + } + + @Test + public void shouldReturnEmptyStorageProperties() + { + final String contentUrl = "url"; + when(primaryStoreMock.getObjectStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + when(secondaryStoreMock.getObjectStorageProperties(contentUrl)).thenReturn(Collections.emptyMap()); + + final Map storageProperties = aggregatingStore.getObjectStorageProperties(contentUrl); + + assertTrue(storageProperties.isEmpty()); + verify(secondaryStoreMock, times(1)).getObjectStorageProperties(contentUrl); + verify(primaryStoreMock, times(1)).getObjectStorageProperties(contentUrl); + } + }