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 f88f2810e5..97fd92350f 100644 --- a/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/content/ContentServiceImpl.java @@ -45,6 +45,7 @@ import org.alfresco.repo.policy.ClassPolicyDelegate; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.InvalidTypeException; @@ -649,6 +650,23 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa return directAccessUrl; } + /** + * {@inheritDoc} + */ + @Override + @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."); + } + + return store.getObjectStorageProperties(contentData.getContentUrl()); + } + protected String getFileName(NodeRef nodeRef) { String fileName = null; @@ -672,4 +690,4 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa } return validFor; } -} \ No newline at end of file +} 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 ae899c6d53..6c2c72d9d3 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 @@ -28,9 +28,13 @@ package org.alfresco.service.cmr.repository; import org.alfresco.api.AlfrescoPublicApi; import org.alfresco.service.Auditable; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.dictionary.InvalidTypeException; import org.alfresco.service.namespace.QName; +import java.util.Collections; +import java.util.Map; + /** * Provides methods for accessing and transforming content. *

@@ -194,4 +198,22 @@ public interface ContentService */ @Auditable(parameters = {"nodeRef", "validFor"}) DirectAccessUrl requestContentDirectUrl(NodeRef nodeRef, boolean attachment, Long validFor); + + /** + * Gets a key-value (String-String) collection of storage headers/properties with their respective values for a specific node reference. + * 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 nodeRef a reference to a node having a content property + * @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 + @Experimental + default Map getStorageProperties(NodeRef nodeRef, QName propertyQName) + { + return Collections.emptyMap(); + } } 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 23d4349a9f..a819baee45 100644 --- a/repository/src/main/resources/alfresco/public-services-security-context.xml +++ b/repository/src/main/resources/alfresco/public-services-security-context.xml @@ -497,6 +497,7 @@ org.alfresco.service.cmr.repository.ContentService.getTempWriter=ACL_ALLOW 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.*=ACL_DENY 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 aa2f9276eb..b5440bc5f1 100644 --- a/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/ContentServiceImplUnitTest.java @@ -25,8 +25,10 @@ */ package org.alfresco.repo.content; +import static org.junit.Assert.assertEquals; 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; @@ -41,6 +43,7 @@ import static org.mockito.MockitoAnnotations.openMocks; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.directurl.DirectAccessUrlDisabledException; import org.alfresco.repo.content.directurl.SystemWideDirectUrlConfig; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.DirectAccessUrl; import org.alfresco.service.cmr.repository.NodeRef; @@ -50,6 +53,9 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.util.Collections; +import java.util.Map; + /** * Unit tests for content service implementation. * @@ -64,6 +70,13 @@ public class ContentServiceImplUnitTest private static final Long SYS_MAX_EXPIRY_TIME_IN_SECS = 300L; private static final NodeRef NODE_REF = new NodeRef("content://Node/Ref"); + public static final String SOME_CONTENT_URL = "someContentUrl"; + private static final NodeRef NODE_REF_2 = new NodeRef("content://Node/Ref2");; + + 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-header-2"; + private static final String VALUE_2 = "value2"; @InjectMocks private ContentServiceImpl contentService; @@ -77,12 +90,15 @@ public class ContentServiceImplUnitTest @Mock private ContentData mockContentData; + @Mock + private DictionaryService mockDictionaryService; + @Before public void setup() { openMocks(this); when(mockNodeService.getProperty(NODE_REF, ContentModel.PROP_CONTENT)).thenReturn(mockContentData); - when(mockContentData.getContentUrl()).thenReturn("someContentUrl"); + when(mockContentData.getContentUrl()).thenReturn(SOME_CONTENT_URL); when(mockContentData.getMimetype()).thenReturn("someMimetype"); when(mockNodeService.getProperty(NODE_REF, ContentModel.PROP_NAME)).thenReturn("someFilename"); } @@ -148,6 +164,49 @@ public class ContentServiceImplUnitTest verify(mockContentStore, times(1)).requestContentDirectUrl(anyString(), eq(true), anyString(), anyString(), anyLong()); } + @Test + public void shouldReturnStoragePropertiesWhenTheyExist() + { + final Map storageObjectPropsMap = Map.of(X_AMZ_HEADER_1, VALUE_1, X_AMZ_HEADER_2, VALUE_2); + when(mockContentStore.getObjectStorageProperties(SOME_CONTENT_URL)).thenReturn(storageObjectPropsMap); + + final Map objectStorageProperties = contentService.getStorageProperties(NODE_REF, ContentModel.PROP_CONTENT); + assertFalse(objectStorageProperties.isEmpty()); + assertEquals(storageObjectPropsMap, objectStorageProperties); + } + + @Test + public void shouldReturnEmptyStoragePropertiesWhenTheyDontExist() + { + when(mockContentStore.getObjectStorageProperties(SOME_CONTENT_URL)).thenReturn(Collections.emptyMap()); + + final Map objectStorageProperties = contentService.getStorageProperties(NODE_REF, ContentModel.PROP_CONTENT); + assertTrue(objectStorageProperties.isEmpty()); + } + + @Test + public void getStoragePropertiesThrowsExceptionWhenNoContentFound() + { + final String dummyContentProperty = "dummy"; + when(mockNodeService.getProperty(NODE_REF_2, ContentModel.PROP_CONTENT)).thenReturn(dummyContentProperty); + when(mockDictionaryService.getProperty(ContentModel.PROP_CONTENT)).thenReturn(null); + + assertThrows(IllegalArgumentException.class, () -> { + contentService.getStorageProperties(NODE_REF_2, ContentModel.PROP_CONTENT); + }); + } + + @Test + public void getStoragePropertiesThrowsExceptionWhenNoContentUrlFound() + { + final ContentData contentWithoutUrl = new ContentData(null, null, 0, null); + when(mockNodeService.getProperty(NODE_REF_2, ContentModel.PROP_CONTENT)).thenReturn(contentWithoutUrl); + + assertThrows(IllegalArgumentException.class, () -> { + contentService.getStorageProperties(NODE_REF_2, ContentModel.PROP_CONTENT); + }); + } + /* Helper method to set system-wide direct access url configuration settings */ private void setupSystemWideDirectAccessConfig(Boolean isEnabled) {