From dad9789974ccf0776f199475736d831bf310f7d4 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 4 May 2023 20:26:40 -0700 Subject: [PATCH 01/28] wip --- .../fs/store/DefaultFilesystemStoreImpl.java | 118 +++++++++++------- .../test/java/it/store/FilesystemStoreIT.java | 35 ++++-- 2 files changed, 96 insertions(+), 57 deletions(-) diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index d7b7b42cf..7365ee691 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -10,6 +10,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; @@ -171,58 +173,78 @@ public boolean matches(Field field) { }); } + protected String calculateName(String name) { + Pattern p = Pattern.compile("^(.+)(Id|Len|Length|MimeType|Mimetype|ContentType|(? { - BeforeEach(() ->{ + It("should have the updated content", () -> { + FileSystemResourceLoader loader = context.getBean(FileSystemResourceLoader.class); + String contentId = entity.getContentId(); + assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); + String renditionId = entity.getRenditionId(); + assertThat(new File(loader.getFilesystemRoot(), renditionId).exists(), is(true)); + store.setContent(entity, new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes())); - store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes())); + store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes())); entity = repo.save(entity); - }); - It("should have the updated content", () -> { - //content + //content boolean matches = false; try (InputStream content = store.getContent(entity)) { matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); @@ -318,11 +322,22 @@ public class FilesystemStoreIT { } //rendition - matches = false; - try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { - matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); - assertThat(matches, is(true)); - } + matches = false; + try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); + assertThat(new File(loader.getFilesystemRoot(), renditionId).exists(), is(true)); + + assertThat(entity.getContentId(), is(not(contentId))); + assertThat(entity.getRenditionId(), is(not(renditionId))); + + assertThat(new File(loader.getFilesystemRoot(), entity.getContentId()).exists(), is(true)); + assertThat(new File(loader.getFilesystemRoot(), entity.getRenditionId()).exists(), is(true)); + + int i=0; }); }); From 632a0c5368a492753c8b176512649b5871166ee2 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 8 May 2023 21:52:23 -0700 Subject: [PATCH 02/28] feat: add a new setContent method accepting a SetContentParams arg with content length and overwriteExistingContent - implement in filesystem storage module --- .../commons/store/factory/StoreImpl.java | 73 + .../content/commons/store/ContentStore.java | 5 +- .../commons/store/SetContentParams.java | 11 + .../factory/AbstractStoreFactoryBeanTest.java | 6 + .../fs/store/DefaultFilesystemStoreImpl.java | 15 +- .../DefaultFilesystemStoresImplTest.java | 1532 ++++++++--------- .../test/java/it/events/BeforeSetEventIT.java | 4 +- .../test/java/it/store/FilesystemStoreIT.java | 24 + .../ContentStoreContentService.java | 12 + 9 files changed, 908 insertions(+), 774 deletions(-) create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java index fe1e83e69..bc9a3550e 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java @@ -18,6 +18,7 @@ import org.springframework.content.commons.repository.Store; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.store.events.AfterAssociateEvent; import org.springframework.content.commons.store.events.AfterGetContentEvent; @@ -199,6 +200,78 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { return result; } + @Override + public Object setContent(Object property, PropertyPath propertyPath, InputStream content, SetContentParams params) { + Object result = null; + + File contentCopy = null; + TeeInputStream contentCopyStream = null; + try { + contentCopy = Files.createTempFile(copyContentRootPath, "contentCopy", ".tmp").toFile(); + contentCopyStream = new TeeInputStream(content, new FileOutputStream(contentCopy), true); + + org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = null; + BeforeSetContentEvent before = null; + + oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, propertyPath, delegate, contentCopyStream); + publisher.publishEvent(oldBefore); + + ContentStore contentStore = castToContentStore(delegate); + if (contentStore != null) { + before = new BeforeSetContentEvent(property, propertyPath, contentStore, contentCopyStream); + publisher.publishEvent(before); + } + + // inputstream was processed and replaced + if (oldBefore != null && oldBefore.getInputStream() != null && oldBefore.getInputStream().equals(contentCopyStream) == false) { + content = oldBefore.getInputStream(); + } + else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { + content = before.getInputStream(); + } + // content was processed but not replaced + else if (contentCopyStream != null && contentCopyStream.isDirty()) { + while (contentCopyStream.read(new byte[4096]) != -1) { + } + content = new FileInputStream(contentCopy); + } + + try { + result = ((org.springframework.content.commons.store.ContentStore)delegate).setContent(property, propertyPath, content, params); + } + catch (Exception e) { + throw e; + } + + org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); + oldAfter.setResult(result); + publisher.publishEvent(oldAfter); + + if (contentStore != null) { + AfterSetContentEvent after = new AfterSetContentEvent(property, propertyPath, contentStore); + after.setResult(result); + publisher.publishEvent(after); + } + } catch (FileNotFoundException fileNotFoundException) { + fileNotFoundException.printStackTrace(); + } catch (IOException ioException) { + ioException.printStackTrace(); + } finally { + if (contentCopyStream != null) { + IOUtils.closeQuietly(contentCopyStream); + } + if (contentCopy != null) { + try { + Files.deleteIfExists(contentCopy.toPath()); + } catch (IOException e) { + logger.error(String.format("Unable to delete content copy %s", contentCopy.toPath()), e); + } + } + } + + return result; + } + @Override public Object setContent(Object property, Resource resourceContent) { diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java index 30e177bd9..5c3208e43 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java @@ -19,7 +19,10 @@ public interface ContentStore extends AssociativeSt @LockParticipant S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen); - @LockParticipant + @LockParticipant + S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params); + + @LockParticipant S setContent(S entity, Resource resourceContent); @LockParticipant diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java new file mode 100644 index 000000000..bc6b60b4b --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java @@ -0,0 +1,11 @@ +package org.springframework.content.commons.store; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SetContentParams { + private long contentLength = -1; + private boolean overwriteExistingContent; +} diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java index 711991319..26fc50609 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java @@ -15,6 +15,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.Store; import org.springframework.core.io.Resource; @@ -143,6 +144,11 @@ public Object setContent(Object property, PropertyPath propertyPath, InputStream return null; } + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return null; + } + @Override public Object setContent(Object property, PropertyPath propertyPath, Resource resourceContent) { // TODO Auto-generated method stub diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 7365ee691..6136c5f4b 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -28,6 +28,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.Store; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; import org.springframework.content.commons.utils.Condition; @@ -43,7 +44,7 @@ @Transactional(readOnly = true) public class DefaultFilesystemStoreImpl implements Store, AssociativeStore, ContentStore, - org.springframework.content.commons.store.AssociativeStore { + org.springframework.content.commons.store.ContentStore { private static Log logger = LogFactory.getLog(DefaultFilesystemStoreImpl.class); @@ -198,7 +199,7 @@ public S setContent(S entity, InputStream content) { // Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class); -// if (contentId == null || replaceContent == true) { +// if (contentId == null) { // // Serializable newId = UUID.randomUUID().toString(); // @@ -256,6 +257,12 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content) @Transactional @Override public S setContent(S property, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(property, propertyPath, content, SetContentParams.builder().contentLength(contentLen).build()); + } + + @Transactional + @Override + public S setContent(S property, PropertyPath propertyPath, InputStream content, SetContentParams params) { boolean replaceContent = true; @@ -265,7 +272,7 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, } Object contentId = contentProperty.getContentId(property); - if (contentId == null || replaceContent == true) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -305,7 +312,7 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, } try { - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = resource.contentLength(); } diff --git a/spring-content-fs/src/test/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoresImplTest.java b/spring-content-fs/src/test/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoresImplTest.java index 79f1e4961..8328f7189 100644 --- a/spring-content-fs/src/test/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoresImplTest.java +++ b/spring-content-fs/src/test/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoresImplTest.java @@ -1,766 +1,766 @@ -package internal.org.springframework.content.fs.store; - -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.anyObject; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.matches; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.junit.runner.RunWith; -import org.mockito.InOrder; -import org.mockito.Matchers; -import org.mockito.Mockito; -import org.springframework.content.commons.annotations.ContentId; -import org.springframework.content.commons.annotations.ContentLength; -import org.springframework.content.commons.io.DeletableResource; -import org.springframework.content.commons.repository.StoreAccessException; -import org.springframework.content.commons.utils.FileService; -import org.springframework.content.commons.utils.PlacementService; -import org.springframework.content.commons.utils.PlacementServiceImpl; -import org.springframework.content.fs.io.FileSystemResourceLoader; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.WritableResource; - -import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; - -@RunWith(Ginkgo4jRunner.class) -public class DefaultFilesystemStoresImplTest { - private DefaultFilesystemStoreImpl filesystemContentRepoImpl; - private FileSystemResourceLoader loader; - private PlacementService placer; - private ContentProperty entity; - - private Resource resource, inputResource; - private WritableResource writeableResource; - private DeletableResource deletableResource; - private DeletableResource nonExistentResource; - private FileService fileService; - - private InputStream content; - private OutputStream output; - - private File parent; - private File root; - - private String id; - - private InputStream result; - private Exception e; - - { - Describe("DefaultFilesystemContentRepositoryImpl", () -> { - - BeforeEach(() -> { - loader = mock(FileSystemResourceLoader.class); - placer = mock(PlacementService.class); - fileService = mock(FileService.class); - - filesystemContentRepoImpl = spy(new DefaultFilesystemStoreImpl( - loader, null, placer, fileService)); - }); - - Describe("Store", () -> { - Context("#getResource", () -> { - BeforeEach(() -> { - id = "12345-67890"; - - when(placer.convert(eq("12345-67890"), eq(String.class))) - .thenReturn("12345-67890"); - }); - JustBeforeEach(() -> { - resource = filesystemContentRepoImpl.getResource(id); - }); - It("should use the placer service to get a resource path", () -> { - verify(placer).convert(eq("12345-67890"), eq(String.class)); - verify(loader).getResource(eq("12345-67890")); - }); - }); - }); - Describe("AssociativeStore", () -> { - Context("#getResource", () -> { - JustBeforeEach(() -> { - resource = filesystemContentRepoImpl.getResource(entity); - }); - Context("when the entity is not already associated with a resource", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - }); - It("should not return a resource", - () -> { - assertThat(resource, is(nullValue())); - }); - }); - Context("when the entity is already associated with a resource", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - entity.setContentId("12345-67890"); - - when(placer.convert(eq("12345-67890"), - eq(String.class))).thenReturn("/12345/67890"); - }); - It("should use the placer service to get a resource path", () -> { - verify(placer).convert(eq("12345-67890"), - eq(String.class)); - verify(loader) - .getResource(eq("/12345/67890")); - }); - }); - Context("when there is an entity converter", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - - deletableResource = mock(DeletableResource.class); - - when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); - when(placer.convert(eq(entity), eq(String.class))).thenReturn("/abcd/efgh"); - when(loader.getResource("/abcd/efgh")).thenReturn(deletableResource); - }); - It("should not need to convert the id", () -> { - verify(placer, never()).convert(argThat(not(entity)), eq(String.class)); - }); - It("should return the resource", () -> { - assertThat(resource, is(deletableResource)); - }); - }); - Context("when the entity has a String-arg constructor - Issue #57", () ->{ - BeforeEach(() -> { - PlacementService placementService = new PlacementServiceImpl(); - placer = spy(placementService); - - entity = new TestEntity(); - }); - It("should not call the placement service trying to convert the entity to a string", () -> { - verify(placer, never()).convert(eq(entity), eq(String.class)); - }); - }); - }); - Context("#associate", () -> { - BeforeEach(() -> { - id = "12345-67890"; - - entity = new TestEntity(); - - when(placer.convert(eq("12345-67890"), eq(String.class))) - .thenReturn("/12345/67890"); - - deletableResource = mock(DeletableResource.class); - when(loader.getResource(eq("/12345/67890"))) - .thenReturn(deletableResource); - - when(deletableResource.contentLength()).thenReturn(20L); - }); - JustBeforeEach(() -> { - filesystemContentRepoImpl.associate(entity, id); - }); - It("should set the entity's content ID attribute", () -> { - assertThat(entity.getContentId(), is("12345-67890")); - }); - }); - Context("#unassociate", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - entity.setContentId("12345-67890"); - }); - JustBeforeEach(() -> { - filesystemContentRepoImpl.unassociate(entity); - }); - It("should reset the entity's content ID attribute", () -> { - assertThat(entity.getContentId(), is(nullValue())); - }); - }); - }); - Describe("ContentStore", () -> { - BeforeEach(() -> { - writeableResource = mock(WritableResource.class); - }); - - Context("#setContent", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - content = new ByteArrayInputStream( - "Hello content world!".getBytes()); - }); - - JustBeforeEach(() -> { - try { - filesystemContentRepoImpl.setContent(entity, content); - } catch (Exception e) { - this.e = e; - } - }); - - Context("given an entity converter", () -> { - Context("when the content doesn't yet exist", () -> { - BeforeEach(() -> { - when(placer.convert(matches( - "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"), - eq(String.class))) - .thenReturn("12345-67890"); - - when(loader.getResource(eq("12345-67890"))) - .thenReturn(writeableResource); - output = mock(OutputStream.class); - when(writeableResource.getOutputStream()).thenReturn(output); - - File resourceFile = mock(File.class); - parent = mock(File.class); - when(writeableResource.getFile()).thenReturn(resourceFile); - when(resourceFile.getParentFile()).thenReturn(parent); - }); - It("creates a directory for the parent", () -> { - verify(fileService).mkdirs(eq(parent)); - }); - It("should make a new UUID", () -> { - assertThat(entity.getContentId(), is(not(nullValue()))); - }); - It("should create a new resource", () -> { - verify(loader).getResource(eq("12345-67890")); - }); - It("should write to the resource's outputstream", () -> { - verify(writeableResource).getOutputStream(); - verify(output, times(1)).write(Matchers.any(), eq(0), - eq(20)); - }); - }); - Context("when the content already exists", () -> { - BeforeEach(() -> { - when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); - when(placer.convert(eq(entity), eq(String.class))).thenReturn("/abcd/efgh"); - when(loader.getResource(eq("/abcd/efgh"))).thenReturn(writeableResource); - - when(writeableResource.exists()).thenReturn(true); - - output = mock(OutputStream.class); - when(writeableResource.getOutputStream()).thenReturn(output); - - when(writeableResource.contentLength()).thenReturn(20L); - }); - - It("should write to the resource's outputstream", () -> { - verify(output, times(1)).write(Matchers.any(), eq(0), - eq(20)); - verify(output).close(); - }); - - It("should change the content length", () -> { - assertThat(entity.getContentLen(), is(20L)); - }); - }); - }); - - Context("given just the default ID converters", () -> { - BeforeEach(() -> { - when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); - - when(placer.convert(eq("12345-67890"), - eq(String.class))) - .thenReturn("12345-67890"); - - when(placer.convert(matches( - "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"), - eq(String.class))) - .thenReturn("12345-67890"); - - when(loader.getResource(eq("12345-67890"))) - .thenReturn(writeableResource); - output = mock(OutputStream.class); - when(writeableResource.getOutputStream()).thenReturn(output); - - when(writeableResource.contentLength()).thenReturn(20L); - }); - - Context("when the content already exists", () -> { - BeforeEach(() -> { - entity.setContentId("12345-67890"); - when(writeableResource.exists()).thenReturn(true); - }); - - It("should use the placer service to get a resource path", - () -> { - verify(placer, atLeastOnce()).convert(anyObject(), anyObject()); - verify(loader).getResource(eq("12345-67890")); - }); - - It("should change the content length", () -> { - assertThat(entity.getContentLen(), is(20L)); - }); - - It("should write to the resource's outputstream", () -> { - verify(writeableResource).getOutputStream(); - verify(output, times(1)).write(Matchers.any(), eq(0), - eq(20)); - }); - }); - - Context("when the content does not already exist", () -> { - BeforeEach(() -> { - assertThat(entity.getContentId(), is(nullValue())); - - File resourceFile = mock(File.class); - parent = mock(File.class); - - when(writeableResource.getFile()).thenReturn(resourceFile); - when(resourceFile.getParentFile()).thenReturn(parent); - }); - - It("creates a directory for the parent", () -> { - verify(fileService).mkdirs(eq(parent)); - }); - - It("should make a new UUID", () -> { - assertThat(entity.getContentId(), is(not(nullValue()))); - }); - It("should create a new resource", () -> { - verify(loader).getResource(eq("12345-67890")); - }); - It("should write to the resource's outputstream", () -> { - verify(writeableResource).getOutputStream(); - verify(output, times(1)).write(Matchers.any(), eq(0), - eq(20)); - }); - }); - - Context("when getting the resource output stream throws an IOException", () -> { - BeforeEach(() -> { - File resourceFile = mock(File.class); - parent = mock(File.class); - - when(writeableResource.getFile()).thenReturn(resourceFile); - when(resourceFile.getParentFile()).thenReturn(parent); - - when(writeableResource.getOutputStream()).thenThrow(new IOException()); - }); - It("should return a StoreAccessException wrapping the IOException", () -> { - assertThat(e, is(instanceOf(StoreAccessException.class))); - assertThat(e.getCause(), is(instanceOf(IOException.class))); - }); - }); - }); - }); - - Context("#setContent from Resource", () -> { - - BeforeEach(() -> { - entity = new TestEntity(); - content = new ByteArrayInputStream("Hello content world!".getBytes()); - inputResource = new InputStreamResource(content); - }); - - JustBeforeEach(() -> { - try { - filesystemContentRepoImpl.setContent(entity, inputResource); - } catch (Exception e) { - this.e = e; - } - }); - - It("should delegate to setContent from InputStream", () -> { - verify(filesystemContentRepoImpl).setContent(eq(entity), eq(content)); - }); - - Context("when the resource throws an IOException", () -> { - BeforeEach(() -> { - inputResource = mock(Resource.class); - when(inputResource.getInputStream()).thenThrow(new IOException("setContent badness")); - }); - It("should throw a StoreAccessException", () -> { - assertThat(e, is(instanceOf(StoreAccessException.class))); - assertThat(e.getCause().getMessage(), containsString("setContent badness")); - }); - }); - }); - - Context("#getContent", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - content = mock(InputStream.class); - entity.setContentId("abcd-efgh"); - - when(placer.convert(eq(entity), eq(String.class))) - .thenReturn(null); - - when(placer.convert(eq("abcd-efgh"), eq(String.class))) - .thenReturn("abcd-efgh"); - - when(loader.getResource(eq("abcd-efgh"))) - .thenReturn(writeableResource); - when(writeableResource.getInputStream()).thenReturn(content); - }); - - JustBeforeEach(() -> { - try { - result = filesystemContentRepoImpl.getContent(entity); - } catch (Exception e) { - this.e = e; - } - }); - - Context("given an entity converter", () -> { - BeforeEach(() -> { - when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); - when(placer.convert(eq(entity), eq(String.class))) - .thenReturn("/abcd/efgh"); - }); - Context("when the resource does not exists", () -> { - BeforeEach(() -> { - nonExistentResource = mock(DeletableResource.class); - - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(nonExistentResource); - - when(writeableResource.exists()).thenReturn(false); - }); - - It("should not return the content", () -> { - assertThat(result, is(nullValue())); - }); - }); - - Context("when the resource exists", () -> { - BeforeEach(() -> { - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(writeableResource); - - when(writeableResource.exists()).thenReturn(true); - - when(writeableResource.getInputStream()).thenReturn(content); - }); - It("should get content", () -> { - assertThat(result, is(content)); - }); - }); - }); - - Context("given just the default ID converter", () -> { - BeforeEach(() -> { - when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); - }); - Context("when the resource does not exists", () -> { - BeforeEach(() -> { - nonExistentResource = mock(DeletableResource.class); - when(writeableResource.exists()).thenReturn(true); - - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(nonExistentResource); - when(loader.getResource(eq("abcd-efgh"))) - .thenReturn(nonExistentResource); - }); - - It("should not find the content", () -> { - assertThat(result, is(nullValue())); - }); - }); - - Context("when the resource exists", () -> { - BeforeEach(() -> { - when(writeableResource.exists()).thenReturn(true); - }); - - It("should get content", () -> { - assertThat(result, is(content)); - }); - - Context("when getting the resource inputstream throws an IOException", () -> { - BeforeEach(() -> { - when(writeableResource.getInputStream()).thenThrow(new IOException("test-ioexception")); - }); - It("should return a StoreAccessException wrapping the IOException", () -> { - assertThat(result, is(nullValue())); - assertThat(e, is(instanceOf(StoreAccessException.class))); - assertThat(e.getCause().getMessage(), is("test-ioexception")); - }); - }); - }); - - Context("when the resource exists but in the old location", () -> { - BeforeEach(() -> { - nonExistentResource = mock(DeletableResource.class); - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(nonExistentResource); - when(nonExistentResource.exists()).thenReturn(false); - - when(loader.getResource(eq("abcd-efgh"))) - .thenReturn(writeableResource); - when(writeableResource.exists()).thenReturn(true); - }); - It("should check the new location and then the old", () -> { - InOrder inOrder = Mockito.inOrder(loader); - - inOrder.verify(loader).getResource(eq("abcd-efgh")); - inOrder.verifyNoMoreInteractions(); - }); - It("should get content", () -> { - assertThat(result, is(content)); - }); - }); - }); - }); - - Context("#unsetContent", () -> { - BeforeEach(() -> { - entity = new TestEntity(); - entity.setContentId("abcd-efgh"); - entity.setContentLen(100L); - deletableResource = mock(DeletableResource.class); - }); - - JustBeforeEach(() -> { - filesystemContentRepoImpl.unsetContent(entity); - }); - - Context("given an entity converter", () -> { - BeforeEach(() -> { - when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); - when(placer.convert(eq(entity), eq(String.class))) - .thenReturn("/abcd/efgh"); - }); - Context("given the resource does not exist", () -> { - BeforeEach(() -> { - nonExistentResource = mock(DeletableResource.class); - - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(nonExistentResource); - - when(nonExistentResource.exists()).thenReturn(false); - }); - It("should not delete the resource", () -> { - verify(nonExistentResource, never()).delete(); - }); - }); - Context("given the resource exists", () -> { - BeforeEach(() -> { - deletableResource = mock(DeletableResource.class); - - when(loader.getResource(eq("/abcd/efgh"))) - .thenReturn(deletableResource); - - File resourceFile = mock(File.class); - parent = mock(File.class); - when(deletableResource.getFile()).thenReturn(resourceFile); - when(resourceFile.getParentFile()).thenReturn(parent); - when(deletableResource.exists()).thenReturn(true); - - FileSystemResource rootResource = mock(FileSystemResource.class); - when(loader.getRootResource()).thenReturn(rootResource); - root = mock(File.class); - when(rootResource.getFile()).thenReturn(root); - }); - It("should delete the resource", () -> { - verify(deletableResource, times(1)).delete(); - }); - }); - }); - - Context("given just the default ID converter", () -> { - BeforeEach(() -> { - when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); - }); - Context("when the content exists in the new location", () -> { - BeforeEach(() -> { - when(placer.convert(eq("abcd-efgh"), eq(String.class))) - .thenReturn("abcd-efgh"); - - when(loader.getResource(eq("abcd-efgh"))) - .thenReturn(deletableResource); - - File resourceFile = mock(File.class); - parent = mock(File.class); - when(deletableResource.getFile()).thenReturn(resourceFile); - when(resourceFile.getParentFile()).thenReturn(parent); - when(deletableResource.exists()).thenReturn(true); - - FileSystemResource rootResource = mock(FileSystemResource.class); - when(loader.getRootResource()).thenReturn(rootResource); - root = mock(File.class); - when(rootResource.getFile()).thenReturn(root); - }); - - It("should delete the resource", () -> { - verify(deletableResource, times(1)).delete(); - }); - - Context("when the property has a dedicated ContentId field", () -> { - It("should reset the metadata", () -> { - assertThat(entity.getContentId(), is(nullValue())); - assertThat(entity.getContentLen(), is(0L)); - }); - }); - Context("when the property's ContentId field also is the javax persistence Id field", () -> { - BeforeEach(() -> { - entity = new SharedIdContentIdEntity(); - entity.setContentId("abcd-efgh"); - }); - It("should not reset the content id metadata", () -> { - assertThat(entity.getContentId(), is("abcd-efgh")); - assertThat(entity.getContentLen(), is(0L)); - }); - }); - Context("when the property's ContentId field also is the Spring Id field", () -> { - BeforeEach(() -> { - entity = new SharedSpringIdContentIdEntity(); - entity.setContentId("abcd-efgh"); - }); - It("should not reset the content id metadata", () -> { - assertThat(entity.getContentId(), is("abcd-efgh")); - assertThat(entity.getContentLen(), is(0L)); - }); - }); - }); - - Context("when the content doesnt exist", () -> { - BeforeEach(() -> { - when(placer.convert(eq("abcd-efgh"), eq(String.class))) - .thenReturn("abcd-efgh"); - - nonExistentResource = mock(DeletableResource.class); - when(loader.getResource(eq("abcd-efgh"))) - .thenReturn(nonExistentResource); - when(nonExistentResource.exists()).thenReturn(false); - }); - It("should unset the content", () -> { - verify(nonExistentResource, never()).delete(); - assertThat(entity.getContentId(), is(nullValue())); - assertThat(entity.getContentLen(), is(0L)); - }); - }); - }); - }); - }); - }); - } - - public interface ContentProperty { - String getContentId(); - - void setContentId(String contentId); - - long getContentLen(); - - void setContentLen(long contentLen); - } - - public static class TestEntity implements ContentProperty { - @ContentId - private String contentId; - - @ContentLength - private long contentLen; - - public TestEntity() { - this.contentId = null; - } - - public TestEntity(String contentId) { - this.contentId = new String(contentId); - } - - @Override - public String getContentId() { - return this.contentId; - } - - @Override - public void setContentId(String contentId) { - this.contentId = contentId; - } - - @Override - public long getContentLen() { - return contentLen; - } - - @Override - public void setContentLen(long contentLen) { - this.contentLen = contentLen; - } - } - - public static class SharedIdContentIdEntity implements ContentProperty { - - @jakarta.persistence.Id - @ContentId - private String contentId; - - @ContentLength - private long contentLen; - - public SharedIdContentIdEntity() { - this.contentId = null; - } - - @Override - public String getContentId() { - return this.contentId; - } - - @Override - public void setContentId(String contentId) { - this.contentId = contentId; - } - - @Override - public long getContentLen() { - return contentLen; - } - - @Override - public void setContentLen(long contentLen) { - this.contentLen = contentLen; - } - } - - public static class SharedSpringIdContentIdEntity implements ContentProperty { - - @org.springframework.data.annotation.Id - @ContentId - private String contentId; - - @ContentLength - private long contentLen; - - public SharedSpringIdContentIdEntity() { - this.contentId = null; - } - - @Override - public String getContentId() { - return this.contentId; - } - - @Override - public void setContentId(String contentId) { - this.contentId = contentId; - } - - @Override - public long getContentLen() { - return contentLen; - } - - @Override - public void setContentLen(long contentLen) { - this.contentLen = contentLen; - } - } -} +//package internal.org.springframework.content.fs.store; +// +//import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +//import static org.hamcrest.CoreMatchers.containsString; +//import static org.hamcrest.CoreMatchers.instanceOf; +//import static org.hamcrest.CoreMatchers.is; +//import static org.hamcrest.CoreMatchers.not; +//import static org.hamcrest.CoreMatchers.nullValue; +//import static org.hamcrest.MatcherAssert.assertThat; +//import static org.mockito.ArgumentMatchers.anyObject; +//import static org.mockito.ArgumentMatchers.eq; +//import static org.mockito.ArgumentMatchers.matches; +//import static org.mockito.Mockito.atLeastOnce; +//import static org.mockito.Mockito.mock; +//import static org.mockito.Mockito.never; +//import static org.mockito.Mockito.spy; +//import static org.mockito.Mockito.times; +//import static org.mockito.Mockito.verify; +//import static org.mockito.Mockito.when; +//import static org.mockito.hamcrest.MockitoHamcrest.argThat; +// +//import java.io.ByteArrayInputStream; +//import java.io.File; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.OutputStream; +// +//import org.junit.runner.RunWith; +//import org.mockito.InOrder; +//import org.mockito.Matchers; +//import org.mockito.Mockito; +//import org.springframework.content.commons.annotations.ContentId; +//import org.springframework.content.commons.annotations.ContentLength; +//import org.springframework.content.commons.io.DeletableResource; +//import org.springframework.content.commons.repository.StoreAccessException; +//import org.springframework.content.commons.utils.FileService; +//import org.springframework.content.commons.utils.PlacementService; +//import org.springframework.content.commons.utils.PlacementServiceImpl; +//import org.springframework.content.fs.io.FileSystemResourceLoader; +//import org.springframework.core.io.FileSystemResource; +//import org.springframework.core.io.InputStreamResource; +//import org.springframework.core.io.Resource; +//import org.springframework.core.io.WritableResource; +// +//import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +// +//@RunWith(Ginkgo4jRunner.class) +//public class DefaultFilesystemStoresImplTest { +// private DefaultFilesystemStoreImpl filesystemContentRepoImpl; +// private FileSystemResourceLoader loader; +// private PlacementService placer; +// private ContentProperty entity; +// +// private Resource resource, inputResource; +// private WritableResource writeableResource; +// private DeletableResource deletableResource; +// private DeletableResource nonExistentResource; +// private FileService fileService; +// +// private InputStream content; +// private OutputStream output; +// +// private File parent; +// private File root; +// +// private String id; +// +// private InputStream result; +// private Exception e; +// +// { +// Describe("DefaultFilesystemContentRepositoryImpl", () -> { +// +// BeforeEach(() -> { +// loader = mock(FileSystemResourceLoader.class); +// placer = mock(PlacementService.class); +// fileService = mock(FileService.class); +// +// filesystemContentRepoImpl = spy(new DefaultFilesystemStoreImpl( +// loader, null, placer, fileService)); +// }); +// +// Describe("Store", () -> { +// Context("#getResource", () -> { +// BeforeEach(() -> { +// id = "12345-67890"; +// +// when(placer.convert(eq("12345-67890"), eq(String.class))) +// .thenReturn("12345-67890"); +// }); +// JustBeforeEach(() -> { +// resource = filesystemContentRepoImpl.getResource(id); +// }); +// It("should use the placer service to get a resource path", () -> { +// verify(placer).convert(eq("12345-67890"), eq(String.class)); +// verify(loader).getResource(eq("12345-67890")); +// }); +// }); +// }); +// Describe("AssociativeStore", () -> { +// Context("#getResource", () -> { +// JustBeforeEach(() -> { +// resource = filesystemContentRepoImpl.getResource(entity); +// }); +// Context("when the entity is not already associated with a resource", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// }); +// It("should not return a resource", +// () -> { +// assertThat(resource, is(nullValue())); +// }); +// }); +// Context("when the entity is already associated with a resource", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// entity.setContentId("12345-67890"); +// +// when(placer.convert(eq("12345-67890"), +// eq(String.class))).thenReturn("/12345/67890"); +// }); +// It("should use the placer service to get a resource path", () -> { +// verify(placer).convert(eq("12345-67890"), +// eq(String.class)); +// verify(loader) +// .getResource(eq("/12345/67890")); +// }); +// }); +// Context("when there is an entity converter", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// +// deletableResource = mock(DeletableResource.class); +// +// when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); +// when(placer.convert(eq(entity), eq(String.class))).thenReturn("/abcd/efgh"); +// when(loader.getResource("/abcd/efgh")).thenReturn(deletableResource); +// }); +// It("should not need to convert the id", () -> { +// verify(placer, never()).convert(argThat(not(entity)), eq(String.class)); +// }); +// It("should return the resource", () -> { +// assertThat(resource, is(deletableResource)); +// }); +// }); +// Context("when the entity has a String-arg constructor - Issue #57", () ->{ +// BeforeEach(() -> { +// PlacementService placementService = new PlacementServiceImpl(); +// placer = spy(placementService); +// +// entity = new TestEntity(); +// }); +// It("should not call the placement service trying to convert the entity to a string", () -> { +// verify(placer, never()).convert(eq(entity), eq(String.class)); +// }); +// }); +// }); +// Context("#associate", () -> { +// BeforeEach(() -> { +// id = "12345-67890"; +// +// entity = new TestEntity(); +// +// when(placer.convert(eq("12345-67890"), eq(String.class))) +// .thenReturn("/12345/67890"); +// +// deletableResource = mock(DeletableResource.class); +// when(loader.getResource(eq("/12345/67890"))) +// .thenReturn(deletableResource); +// +// when(deletableResource.contentLength()).thenReturn(20L); +// }); +// JustBeforeEach(() -> { +// filesystemContentRepoImpl.associate(entity, id); +// }); +// It("should set the entity's content ID attribute", () -> { +// assertThat(entity.getContentId(), is("12345-67890")); +// }); +// }); +// Context("#unassociate", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// entity.setContentId("12345-67890"); +// }); +// JustBeforeEach(() -> { +// filesystemContentRepoImpl.unassociate(entity); +// }); +// It("should reset the entity's content ID attribute", () -> { +// assertThat(entity.getContentId(), is(nullValue())); +// }); +// }); +// }); +// Describe("ContentStore", () -> { +// BeforeEach(() -> { +// writeableResource = mock(WritableResource.class); +// }); +// +// Context("#setContent", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// content = new ByteArrayInputStream( +// "Hello content world!".getBytes()); +// }); +// +// JustBeforeEach(() -> { +// try { +// filesystemContentRepoImpl.setContent(entity, content); +// } catch (Exception e) { +// this.e = e; +// } +// }); +// +// Context("given an entity converter", () -> { +// Context("when the content doesn't yet exist", () -> { +// BeforeEach(() -> { +// when(placer.convert(matches( +// "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"), +// eq(String.class))) +// .thenReturn("12345-67890"); +// +// when(loader.getResource(eq("12345-67890"))) +// .thenReturn(writeableResource); +// output = mock(OutputStream.class); +// when(writeableResource.getOutputStream()).thenReturn(output); +// +// File resourceFile = mock(File.class); +// parent = mock(File.class); +// when(writeableResource.getFile()).thenReturn(resourceFile); +// when(resourceFile.getParentFile()).thenReturn(parent); +// }); +// It("creates a directory for the parent", () -> { +// verify(fileService).mkdirs(eq(parent)); +// }); +// It("should make a new UUID", () -> { +// assertThat(entity.getContentId(), is(not(nullValue()))); +// }); +// FIt("should create a new resource", () -> { +// verify(loader).getResource(eq("12345-67890")); +// }); +// It("should write to the resource's outputstream", () -> { +// verify(writeableResource).getOutputStream(); +// verify(output, times(1)).write(Matchers.any(), eq(0), +// eq(20)); +// }); +// }); +// Context("when the content already exists", () -> { +// BeforeEach(() -> { +// when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); +// when(placer.convert(eq(entity), eq(String.class))).thenReturn("/abcd/efgh"); +// when(loader.getResource(eq("/abcd/efgh"))).thenReturn(writeableResource); +// +// when(writeableResource.exists()).thenReturn(true); +// +// output = mock(OutputStream.class); +// when(writeableResource.getOutputStream()).thenReturn(output); +// +// when(writeableResource.contentLength()).thenReturn(20L); +// }); +// +// It("should write to the resource's outputstream", () -> { +// verify(output, times(1)).write(Matchers.any(), eq(0), +// eq(20)); +// verify(output).close(); +// }); +// +// It("should change the content length", () -> { +// assertThat(entity.getContentLen(), is(20L)); +// }); +// }); +// }); +// +// Context("given just the default ID converters", () -> { +// BeforeEach(() -> { +// when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); +// +// when(placer.convert(eq("12345-67890"), +// eq(String.class))) +// .thenReturn("12345-67890"); +// +// when(placer.convert(matches( +// "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"), +// eq(String.class))) +// .thenReturn("12345-67890"); +// +// when(loader.getResource(eq("12345-67890"))) +// .thenReturn(writeableResource); +// output = mock(OutputStream.class); +// when(writeableResource.getOutputStream()).thenReturn(output); +// +// when(writeableResource.contentLength()).thenReturn(20L); +// }); +// +// Context("when the content already exists", () -> { +// BeforeEach(() -> { +// entity.setContentId("12345-67890"); +// when(writeableResource.exists()).thenReturn(true); +// }); +// +// It("should use the placer service to get a resource path", +// () -> { +// verify(placer, atLeastOnce()).convert(anyObject(), anyObject()); +// verify(loader).getResource(eq("12345-67890")); +// }); +// +// It("should change the content length", () -> { +// assertThat(entity.getContentLen(), is(20L)); +// }); +// +// It("should write to the resource's outputstream", () -> { +// verify(writeableResource).getOutputStream(); +// verify(output, times(1)).write(Matchers.any(), eq(0), +// eq(20)); +// }); +// }); +// +// Context("when the content does not already exist", () -> { +// BeforeEach(() -> { +// assertThat(entity.getContentId(), is(nullValue())); +// +// File resourceFile = mock(File.class); +// parent = mock(File.class); +// +// when(writeableResource.getFile()).thenReturn(resourceFile); +// when(resourceFile.getParentFile()).thenReturn(parent); +// }); +// +// It("creates a directory for the parent", () -> { +// verify(fileService).mkdirs(eq(parent)); +// }); +// +// It("should make a new UUID", () -> { +// assertThat(entity.getContentId(), is(not(nullValue()))); +// }); +// It("should create a new resource", () -> { +// verify(loader).getResource(eq("12345-67890")); +// }); +// It("should write to the resource's outputstream", () -> { +// verify(writeableResource).getOutputStream(); +// verify(output, times(1)).write(Matchers.any(), eq(0), +// eq(20)); +// }); +// }); +// +// Context("when getting the resource output stream throws an IOException", () -> { +// BeforeEach(() -> { +// File resourceFile = mock(File.class); +// parent = mock(File.class); +// +// when(writeableResource.getFile()).thenReturn(resourceFile); +// when(resourceFile.getParentFile()).thenReturn(parent); +// +// when(writeableResource.getOutputStream()).thenThrow(new IOException()); +// }); +// It("should return a StoreAccessException wrapping the IOException", () -> { +// assertThat(e, is(instanceOf(StoreAccessException.class))); +// assertThat(e.getCause(), is(instanceOf(IOException.class))); +// }); +// }); +// }); +// }); +// +// Context("#setContent from Resource", () -> { +// +// BeforeEach(() -> { +// entity = new TestEntity(); +// content = new ByteArrayInputStream("Hello content world!".getBytes()); +// inputResource = new InputStreamResource(content); +// }); +// +// JustBeforeEach(() -> { +// try { +// filesystemContentRepoImpl.setContent(entity, inputResource); +// } catch (Exception e) { +// this.e = e; +// } +// }); +// +// It("should delegate to setContent from InputStream", () -> { +// verify(filesystemContentRepoImpl).setContent(eq(entity), eq(content)); +// }); +// +// Context("when the resource throws an IOException", () -> { +// BeforeEach(() -> { +// inputResource = mock(Resource.class); +// when(inputResource.getInputStream()).thenThrow(new IOException("setContent badness")); +// }); +// It("should throw a StoreAccessException", () -> { +// assertThat(e, is(instanceOf(StoreAccessException.class))); +// assertThat(e.getCause().getMessage(), containsString("setContent badness")); +// }); +// }); +// }); +// +// Context("#getContent", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// content = mock(InputStream.class); +// entity.setContentId("abcd-efgh"); +// +// when(placer.convert(eq(entity), eq(String.class))) +// .thenReturn(null); +// +// when(placer.convert(eq("abcd-efgh"), eq(String.class))) +// .thenReturn("abcd-efgh"); +// +// when(loader.getResource(eq("abcd-efgh"))) +// .thenReturn(writeableResource); +// when(writeableResource.getInputStream()).thenReturn(content); +// }); +// +// JustBeforeEach(() -> { +// try { +// result = filesystemContentRepoImpl.getContent(entity); +// } catch (Exception e) { +// this.e = e; +// } +// }); +// +// Context("given an entity converter", () -> { +// BeforeEach(() -> { +// when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); +// when(placer.convert(eq(entity), eq(String.class))) +// .thenReturn("/abcd/efgh"); +// }); +// Context("when the resource does not exists", () -> { +// BeforeEach(() -> { +// nonExistentResource = mock(DeletableResource.class); +// +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(nonExistentResource); +// +// when(writeableResource.exists()).thenReturn(false); +// }); +// +// It("should not return the content", () -> { +// assertThat(result, is(nullValue())); +// }); +// }); +// +// Context("when the resource exists", () -> { +// BeforeEach(() -> { +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(writeableResource); +// +// when(writeableResource.exists()).thenReturn(true); +// +// when(writeableResource.getInputStream()).thenReturn(content); +// }); +// It("should get content", () -> { +// assertThat(result, is(content)); +// }); +// }); +// }); +// +// Context("given just the default ID converter", () -> { +// BeforeEach(() -> { +// when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); +// }); +// Context("when the resource does not exists", () -> { +// BeforeEach(() -> { +// nonExistentResource = mock(DeletableResource.class); +// when(writeableResource.exists()).thenReturn(true); +// +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(nonExistentResource); +// when(loader.getResource(eq("abcd-efgh"))) +// .thenReturn(nonExistentResource); +// }); +// +// It("should not find the content", () -> { +// assertThat(result, is(nullValue())); +// }); +// }); +// +// Context("when the resource exists", () -> { +// BeforeEach(() -> { +// when(writeableResource.exists()).thenReturn(true); +// }); +// +// It("should get content", () -> { +// assertThat(result, is(content)); +// }); +// +// Context("when getting the resource inputstream throws an IOException", () -> { +// BeforeEach(() -> { +// when(writeableResource.getInputStream()).thenThrow(new IOException("test-ioexception")); +// }); +// It("should return a StoreAccessException wrapping the IOException", () -> { +// assertThat(result, is(nullValue())); +// assertThat(e, is(instanceOf(StoreAccessException.class))); +// assertThat(e.getCause().getMessage(), is("test-ioexception")); +// }); +// }); +// }); +// +// Context("when the resource exists but in the old location", () -> { +// BeforeEach(() -> { +// nonExistentResource = mock(DeletableResource.class); +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(nonExistentResource); +// when(nonExistentResource.exists()).thenReturn(false); +// +// when(loader.getResource(eq("abcd-efgh"))) +// .thenReturn(writeableResource); +// when(writeableResource.exists()).thenReturn(true); +// }); +// It("should check the new location and then the old", () -> { +// InOrder inOrder = Mockito.inOrder(loader); +// +// inOrder.verify(loader).getResource(eq("abcd-efgh")); +// inOrder.verifyNoMoreInteractions(); +// }); +// It("should get content", () -> { +// assertThat(result, is(content)); +// }); +// }); +// }); +// }); +// +// Context("#unsetContent", () -> { +// BeforeEach(() -> { +// entity = new TestEntity(); +// entity.setContentId("abcd-efgh"); +// entity.setContentLen(100L); +// deletableResource = mock(DeletableResource.class); +// }); +// +// JustBeforeEach(() -> { +// filesystemContentRepoImpl.unsetContent(entity); +// }); +// +// Context("given an entity converter", () -> { +// BeforeEach(() -> { +// when(placer.canConvert(eq(entity.getClass()), eq(String.class))).thenReturn(true); +// when(placer.convert(eq(entity), eq(String.class))) +// .thenReturn("/abcd/efgh"); +// }); +// Context("given the resource does not exist", () -> { +// BeforeEach(() -> { +// nonExistentResource = mock(DeletableResource.class); +// +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(nonExistentResource); +// +// when(nonExistentResource.exists()).thenReturn(false); +// }); +// It("should not delete the resource", () -> { +// verify(nonExistentResource, never()).delete(); +// }); +// }); +// Context("given the resource exists", () -> { +// BeforeEach(() -> { +// deletableResource = mock(DeletableResource.class); +// +// when(loader.getResource(eq("/abcd/efgh"))) +// .thenReturn(deletableResource); +// +// File resourceFile = mock(File.class); +// parent = mock(File.class); +// when(deletableResource.getFile()).thenReturn(resourceFile); +// when(resourceFile.getParentFile()).thenReturn(parent); +// when(deletableResource.exists()).thenReturn(true); +// +// FileSystemResource rootResource = mock(FileSystemResource.class); +// when(loader.getRootResource()).thenReturn(rootResource); +// root = mock(File.class); +// when(rootResource.getFile()).thenReturn(root); +// }); +// It("should delete the resource", () -> { +// verify(deletableResource, times(1)).delete(); +// }); +// }); +// }); +// +// Context("given just the default ID converter", () -> { +// BeforeEach(() -> { +// when(placer.convert(eq(entity), eq(String.class))).thenReturn(null); +// }); +// Context("when the content exists in the new location", () -> { +// BeforeEach(() -> { +// when(placer.convert(eq("abcd-efgh"), eq(String.class))) +// .thenReturn("abcd-efgh"); +// +// when(loader.getResource(eq("abcd-efgh"))) +// .thenReturn(deletableResource); +// +// File resourceFile = mock(File.class); +// parent = mock(File.class); +// when(deletableResource.getFile()).thenReturn(resourceFile); +// when(resourceFile.getParentFile()).thenReturn(parent); +// when(deletableResource.exists()).thenReturn(true); +// +// FileSystemResource rootResource = mock(FileSystemResource.class); +// when(loader.getRootResource()).thenReturn(rootResource); +// root = mock(File.class); +// when(rootResource.getFile()).thenReturn(root); +// }); +// +// It("should delete the resource", () -> { +// verify(deletableResource, times(1)).delete(); +// }); +// +// Context("when the property has a dedicated ContentId field", () -> { +// It("should reset the metadata", () -> { +// assertThat(entity.getContentId(), is(nullValue())); +// assertThat(entity.getContentLen(), is(0L)); +// }); +// }); +// Context("when the property's ContentId field also is the javax persistence Id field", () -> { +// BeforeEach(() -> { +// entity = new SharedIdContentIdEntity(); +// entity.setContentId("abcd-efgh"); +// }); +// It("should not reset the content id metadata", () -> { +// assertThat(entity.getContentId(), is("abcd-efgh")); +// assertThat(entity.getContentLen(), is(0L)); +// }); +// }); +// Context("when the property's ContentId field also is the Spring Id field", () -> { +// BeforeEach(() -> { +// entity = new SharedSpringIdContentIdEntity(); +// entity.setContentId("abcd-efgh"); +// }); +// It("should not reset the content id metadata", () -> { +// assertThat(entity.getContentId(), is("abcd-efgh")); +// assertThat(entity.getContentLen(), is(0L)); +// }); +// }); +// }); +// +// Context("when the content doesnt exist", () -> { +// BeforeEach(() -> { +// when(placer.convert(eq("abcd-efgh"), eq(String.class))) +// .thenReturn("abcd-efgh"); +// +// nonExistentResource = mock(DeletableResource.class); +// when(loader.getResource(eq("abcd-efgh"))) +// .thenReturn(nonExistentResource); +// when(nonExistentResource.exists()).thenReturn(false); +// }); +// It("should unset the content", () -> { +// verify(nonExistentResource, never()).delete(); +// assertThat(entity.getContentId(), is(nullValue())); +// assertThat(entity.getContentLen(), is(0L)); +// }); +// }); +// }); +// }); +// }); +// }); +// } +// +// public interface ContentProperty { +// String getContentId(); +// +// void setContentId(String contentId); +// +// long getContentLen(); +// +// void setContentLen(long contentLen); +// } +// +// public static class TestEntity implements ContentProperty { +// @ContentId +// private String contentId; +// +// @ContentLength +// private long contentLen; +// +// public TestEntity() { +// this.contentId = null; +// } +// +// public TestEntity(String contentId) { +// this.contentId = new String(contentId); +// } +// +// @Override +// public String getContentId() { +// return this.contentId; +// } +// +// @Override +// public void setContentId(String contentId) { +// this.contentId = contentId; +// } +// +// @Override +// public long getContentLen() { +// return contentLen; +// } +// +// @Override +// public void setContentLen(long contentLen) { +// this.contentLen = contentLen; +// } +// } +// +// public static class SharedIdContentIdEntity implements ContentProperty { +// +// @jakarta.persistence.Id +// @ContentId +// private String contentId; +// +// @ContentLength +// private long contentLen; +// +// public SharedIdContentIdEntity() { +// this.contentId = null; +// } +// +// @Override +// public String getContentId() { +// return this.contentId; +// } +// +// @Override +// public void setContentId(String contentId) { +// this.contentId = contentId; +// } +// +// @Override +// public long getContentLen() { +// return contentLen; +// } +// +// @Override +// public void setContentLen(long contentLen) { +// this.contentLen = contentLen; +// } +// } +// +// public static class SharedSpringIdContentIdEntity implements ContentProperty { +// +// @org.springframework.data.annotation.Id +// @ContentId +// private String contentId; +// +// @ContentLength +// private long contentLen; +// +// public SharedSpringIdContentIdEntity() { +// this.contentId = null; +// } +// +// @Override +// public String getContentId() { +// return this.contentId; +// } +// +// @Override +// public void setContentId(String contentId) { +// this.contentId = contentId; +// } +// +// @Override +// public long getContentLen() { +// return contentLen; +// } +// +// @Override +// public void setContentLen(long contentLen) { +// this.contentLen = contentLen; +// } +// } +//} diff --git a/spring-content-fs/src/test/java/it/events/BeforeSetEventIT.java b/spring-content-fs/src/test/java/it/events/BeforeSetEventIT.java index 95791d134..e5364dd7d 100644 --- a/spring-content-fs/src/test/java/it/events/BeforeSetEventIT.java +++ b/spring-content-fs/src/test/java/it/events/BeforeSetEventIT.java @@ -26,9 +26,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.anyObject; diff --git a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java index ada73eb02..dae11e57e 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -31,6 +31,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.fs.config.EnableFilesystemStores; import org.springframework.content.fs.io.FileSystemResourceLoader; @@ -364,6 +365,29 @@ public class FilesystemStoreIT { }); }); + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + FileSystemResourceLoader loader = context.getBean(FileSystemResourceLoader.class); + String contentId = entity.getContentId(); + assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); + + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + entity = repo.save(entity); + + boolean matches = false; + try (InputStream content = store.getContent(entity)) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); + + assertThat(entity.getContentId(), is(not(contentId))); + + assertThat(new File(loader.getFilesystemRoot(), entity.getContentId()).exists(), is(true)); + }); + }); + Context("when content is deleted", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index 5ef24f1b3..ebf88bb05 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -23,6 +23,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.Store; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.StoreInterfaceUtils; import org.springframework.content.rest.RestResource; @@ -186,6 +187,17 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, len = headers.getContentLength(); } argsList.add(len); + } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(SetContentParams.class)) { + SetContentParams params = new SetContentParams(); + + // if available use the original content length + if (headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { + params.setContentLength(headers.getContentLength()); + } + + params.setOverwriteExistingContent(false); + + argsList.add(params); } try { From 56a1cb985b4882a90affec5027b6974b5defacbd Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 9 May 2023 21:21:50 -0700 Subject: [PATCH 03/28] test: fix tests broken by setContent(T, InputStream) refactor --- .../commons/store/SetContentParams.java | 3 +- .../test/java/it/store/FilesystemStoreIT.java | 7 ++--- .../ContentStoreContentService.java | 2 +- .../rest/links/BaseUriContentLinksIT.java | 6 +++- .../content/rest/links/ContentLinkRelIT.java | 19 ++++++------ .../content/rest/links/ContentLinkTests.java | 30 +++++++------------ .../content/rest/links/ContentLinksIT.java | 23 +++++++------- .../rest/links/ContextPathContentLinksIT.java | 4 ++- .../rest/links/EntityContentLinksIT.java | 6 +++- 9 files changed, 51 insertions(+), 49 deletions(-) diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java index bc6b60b4b..7cafcd880 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java @@ -7,5 +7,6 @@ @Builder public class SetContentParams { private long contentLength = -1; - private boolean overwriteExistingContent; + @Builder.Default + private boolean overwriteExistingContent = true; } diff --git a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java index dae11e57e..c17e2289b 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -329,11 +329,8 @@ public class FilesystemStoreIT { assertThat(matches, is(true)); } - assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); - assertThat(new File(loader.getFilesystemRoot(), renditionId).exists(), is(true)); - - assertThat(entity.getContentId(), is(not(contentId))); - assertThat(entity.getRenditionId(), is(not(renditionId))); + assertThat(entity.getContentId(), is(contentId)); + assertThat(entity.getRenditionId(), is(renditionId)); assertThat(new File(loader.getFilesystemRoot(), entity.getContentId()).exists(), is(true)); assertThat(new File(loader.getFilesystemRoot(), entity.getRenditionId()).exists(), is(true)); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index ebf88bb05..c840b5103 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -188,7 +188,7 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, } argsList.add(len); } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(SetContentParams.class)) { - SetContentParams params = new SetContentParams(); + SetContentParams params = SetContentParams.builder().build(); // if available use the original content length if (headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java index 177736159..91ee6804c 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java @@ -25,6 +25,8 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; +import java.io.ByteArrayInputStream; + import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; @@ -69,7 +71,9 @@ public class BaseUriContentLinksIT { Context("given an Entity and a Store with a default store path", () -> { BeforeEach(() -> { - testEntity3 = repository3.save(new TestEntity3()); + testEntity3 = new TestEntity3(); + contentRepository3.setContent(testEntity3, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity3 = repository3.save(testEntity3); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository3); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java index 5ef5cb300..d6f9457d1 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java @@ -130,7 +130,9 @@ public class ContentLinkRelIT { Context("given a store specifying a linkRel and an entity with a top-level uncorrelated content property", () -> { BeforeEach(() -> { - testEntity = repository.save(new TestEntity()); + testEntity = new TestEntity(); + contentRepository.setContent(testEntity, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity = repository.save(testEntity); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository); @@ -145,8 +147,9 @@ public class ContentLinkRelIT { Context("given a store specifying a linkRel and an entity with top-level correlated content properties", () -> { BeforeEach(() -> { - - testEntity5 = repository5.save(new TestEntity5()); + testEntity5 = new TestEntity5(); + store5.setContent(testEntity5, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity5 = repository5.save(testEntity5); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository5); @@ -162,9 +165,8 @@ public class ContentLinkRelIT { Context("given a store specifying a linkrel and an entity a nested content property", () -> { BeforeEach(() -> { testEntity2 = new TestEntity2(); - testEntity2.getChild().setContentId(UUID.randomUUID()); - testEntity2.getChild().setContentLen(1L); testEntity2.getChild().setMimeType("text/plain"); + store2.setContent(testEntity2, PropertyPath.from("child"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); testEntity2 = repository2.save(testEntity2); contentLinkTests.setMvc(mvc); @@ -181,13 +183,12 @@ public class ContentLinkRelIT { Context("given a store specifying a linkrel and an entity with nested content properties", () -> { BeforeEach(() -> { testEntity10 = new TestEntity10(); - testEntity10.getChild().setContentId(UUID.randomUUID()); - testEntity10.getChild().setContentLen(1L); testEntity10.getChild().setContentMimeType("text/plain"); testEntity10.getChild().setContentFileName("test"); - testEntity10.getChild().setPreviewId(UUID.randomUUID()); - testEntity10.getChild().setPreviewLen(1L); + store10.setContent(testEntity10, PropertyPath.from("child/content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity10.getChild().setPreviewMimeType("text/plain"); + store10.setContent(testEntity10, PropertyPath.from("child/preview"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); testEntity10 = repository10.save(testEntity10); contentLinkTests.setMvc(mvc); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java index aad125c3d..f56182346 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java @@ -1,29 +1,23 @@ package internal.org.springframework.content.rest.links; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.text.MatchesPattern.matchesPattern; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.io.ByteArrayInputStream; -import java.io.StringReader; - +import com.theoryinpractise.halbuilder.api.ReadableRepresentation; +import com.theoryinpractise.halbuilder.api.RepresentationFactory; +import com.theoryinpractise.halbuilder.standard.StandardRepresentationFactory; +import lombok.Setter; import org.hamcrest.beans.HasPropertyWithValue; import org.springframework.content.commons.repository.ContentStore; import org.springframework.data.repository.CrudRepository; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MockMvc; -import com.theoryinpractise.halbuilder.api.ReadableRepresentation; -import com.theoryinpractise.halbuilder.api.RepresentationFactory; -import com.theoryinpractise.halbuilder.standard.StandardRepresentationFactory; +import java.io.StringReader; -import lombok.Setter; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.text.MatchesPattern.matchesPattern; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Setter public class ContentLinkTests { @@ -42,8 +36,6 @@ public class ContentLinkTests { { Context("given content is associated", () -> { BeforeEach(() -> { - store.setContent(testEntity, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); - repository.save(testEntity); }); Context("a GET to /{api}?/{repository}/{id}", () -> { It("should provide a response with a content link", () -> { diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java index 10e91b946..548fde550 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java @@ -1,15 +1,15 @@ package internal.org.springframework.content.rest.links; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; import static java.lang.String.format; +import java.io.ByteArrayInputStream; import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.rest.config.HypermediaConfiguration; import org.springframework.content.rest.config.RestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; @@ -101,7 +101,9 @@ public class ContentLinksIT { Context("given a store and an entity with a top-level uncorrelated content property", () -> { BeforeEach(() -> { - testEntity3 = repository3.save(new TestEntity3()); + testEntity3 = new TestEntity3(); + contentRepository3.setContent(testEntity3, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity3 = repository3.save(testEntity3); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository3); @@ -116,7 +118,9 @@ public class ContentLinksIT { Context("given a store and an entity with top-level correlated content properties", () -> { BeforeEach(() -> { - testEntity5 = repository5.save(new TestEntity5()); + testEntity5 = new TestEntity5(); + store5.setContent(testEntity5, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity5 = repository5.save(testEntity5); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository5); @@ -132,9 +136,8 @@ public class ContentLinksIT { Context("given a store specifying a linkrel and an entity a nested content property", () -> { BeforeEach(() -> { testEntity2 = new TestEntity2(); - testEntity2.getChild().setContentId(UUID.randomUUID()); - testEntity2.getChild().setContentLen(1L); testEntity2.getChild().setMimeType("text/plain"); + store2.setContent(testEntity2, PropertyPath.from("child"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); testEntity2 = repository2.save(testEntity2); contentLinkTests.setMvc(mvc); @@ -151,12 +154,10 @@ public class ContentLinksIT { Context("given a store specifying a linkrel and an entity with nested content properties", () -> { BeforeEach(() -> { testEntity10 = new TestEntity10(); - testEntity10.getChild().setContentId(UUID.randomUUID()); - testEntity10.getChild().setContentLen(1L); + store10.setContent(testEntity10, PropertyPath.from("child/content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); testEntity10.getChild().setContentMimeType("text/plain"); testEntity10.getChild().setContentFileName("test"); - testEntity10.getChild().setPreviewId(UUID.randomUUID()); - testEntity10.getChild().setPreviewLen(1L); + store10.setContent(testEntity10, PropertyPath.from("child/preview"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); testEntity10.getChild().setPreviewMimeType("text/plain"); testEntity10 = repository10.save(testEntity10); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java index b7a0e3390..2fb24a87a 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java @@ -76,7 +76,9 @@ public class ContextPathContentLinksIT { Context("given an Entity and a Store with a default store path", () -> { BeforeEach(() -> { - testEntity3 = repository3.save(new TestEntity3()); + testEntity3 = new TestEntity3(); + contentRepository3.setContent(testEntity3, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity3 = repository3.save(testEntity3); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository3); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java index bbd0285e5..bf1352d37 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java @@ -27,6 +27,8 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; +import java.io.ByteArrayInputStream; + import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; @@ -73,7 +75,9 @@ public class EntityContentLinksIT { Context("when entity links are enabled", () -> { BeforeEach(() -> { - testEntity3 = repository3.save(new TestEntity3()); + testEntity3 = new TestEntity3(); + contentRepository3.setContent(testEntity3, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + testEntity3 = repository3.save(testEntity3); contentLinkTests.setMvc(mvc); contentLinkTests.setRepository(repository3); From c6aabed5e25c200ac8230ae9e9dd0a0ff5bcfe11 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 9 May 2023 22:51:20 -0700 Subject: [PATCH 04/28] feat: dont refactor setContent(T, InputStream) - it breaks backward compatibility --- .../fs/store/DefaultFilesystemStoreImpl.java | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 6136c5f4b..2f000dfa6 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -187,65 +187,65 @@ protected String calculateName(String name) { @Transactional public S setContent(S entity, InputStream content) { - Field f = BeanUtils.findFieldWithAnnotation(entity, ContentId.class); - if (f == null) { - throw new StoreAccessException("no content property found"); - } - String propertyName = calculateName(f.getName()); - if (propertyName == null) { - throw new StoreAccessException(String.format("invalid content property name", f.getName())); - } - return this.setContent(entity, PropertyPath.from(propertyName), content); - - -// Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class); -// if (contentId == null) { -// -// Serializable newId = UUID.randomUUID().toString(); -// -// Object convertedId = convertToExternalContentIdType(entity, newId); -// -// BeanUtils.setFieldWithAnnotation(entity, ContentId.class, convertedId); -// } -// -// Resource resource = this.getResource(entity); -// if (resource == null) { -// return entity; -// } -// -// OutputStream os = null; -// try { -// if (resource.exists() == false) { -// File resourceFile = resource.getFile(); -// File parent = resourceFile.getParentFile(); -// this.fileService.mkdirs(parent); -// } -// if (resource instanceof WritableResource) { -// os = ((WritableResource) resource).getOutputStream(); -// IOUtils.copy(content, os); -// } -// } catch (IOException e) { -// logger.error(format("Unexpected io error setting content for entity %s", entity), e); -// throw new StoreAccessException(format("Setting content for entity %s", entity), e); -// } catch (Exception e) { -// logger.error(format("Unexpected error setting content for entity %s", entity), e); -// throw new StoreAccessException(format("Setting content for entity %s", entity), e); -// } -// finally { -// IOUtils.closeQuietly(os); +// Field f = BeanUtils.findFieldWithAnnotation(entity, ContentId.class); +// if (f == null) { +// throw new StoreAccessException("no content property found"); // } -// -// try { -// BeanUtils.setFieldWithAnnotation(entity, ContentLength.class, -// resource.contentLength()); +// String propertyName = calculateName(f.getName()); +// if (propertyName == null) { +// throw new StoreAccessException(String.format("invalid content property name", f.getName())); // } -// catch (IOException e) { -// logger.error(format( -// "Unexpected error setting content length for content for resource %s", -// resource.toString()), e); -// } -// -// return entity; +// return this.setContent(entity, PropertyPath.from(propertyName), content); + + + Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class); + if (contentId == null) { + + Serializable newId = UUID.randomUUID().toString(); + + Object convertedId = convertToExternalContentIdType(entity, newId); + + BeanUtils.setFieldWithAnnotation(entity, ContentId.class, convertedId); + } + + Resource resource = this.getResource(entity); + if (resource == null) { + return entity; + } + + OutputStream os = null; + try { + if (resource.exists() == false) { + File resourceFile = resource.getFile(); + File parent = resourceFile.getParentFile(); + this.fileService.mkdirs(parent); + } + if (resource instanceof WritableResource) { + os = ((WritableResource) resource).getOutputStream(); + IOUtils.copy(content, os); + } + } catch (IOException e) { + logger.error(format("Unexpected io error setting content for entity %s", entity), e); + throw new StoreAccessException(format("Setting content for entity %s", entity), e); + } catch (Exception e) { + logger.error(format("Unexpected error setting content for entity %s", entity), e); + throw new StoreAccessException(format("Setting content for entity %s", entity), e); + } + finally { + IOUtils.closeQuietly(os); + } + + try { + BeanUtils.setFieldWithAnnotation(entity, ContentLength.class, + resource.contentLength()); + } + catch (IOException e) { + logger.error(format( + "Unexpected error setting content length for content for resource %s", + resource.toString()), e); + } + + return entity; } @Transactional From ec1158cd48e4203a97236372a7890ae48b8c0537 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 10 May 2023 18:05:31 -0700 Subject: [PATCH 05/28] ci: set up a tmate if ci fails --- .github/workflows/prs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml index 1f675d1ba..3be297694 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/prs.yml @@ -70,6 +70,10 @@ jobs: git checkout main mvn -B clean install popd + - name: Setup tmate session if anything fails + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 + timeout-minutes: 360 validate-with-gettingstarteds: runs-on: ubuntu-latest From 50818713b28cac9df02c3da0b16b903d2a6566c5 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 10 May 2023 20:53:41 -0700 Subject: [PATCH 06/28] ci: redfine the action/cache key - I think each build is getting a new key because the pom hash is probably different --- .github/workflows/prs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml index 3be297694..b18a396cb 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/prs.yml @@ -23,6 +23,11 @@ jobs: server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} - name: Build and Test id: build run: | @@ -56,7 +61,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ github.run_id }} + key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-examples @@ -92,7 +97,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ github.run_id }} + key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-gettingstarted From 210721787a6bcf04747dce89115774d6571fe487 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 10 May 2023 21:18:53 -0700 Subject: [PATCH 07/28] ci: ensure maven cache is saved --- .github/workflows/prs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml index b18a396cb..f572bc303 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/prs.yml @@ -24,10 +24,10 @@ jobs: gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Cache Maven packages - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - name: Build and Test id: build run: | @@ -42,7 +42,7 @@ jobs: uses: actions/cache/save@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ github.run_id }} + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} validate-with-examples: runs-on: ubuntu-latest @@ -61,7 +61,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-examples @@ -97,7 +97,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ env.GITHUB_SHA }} + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-gettingstarted From 73b7e514e69d86172206680052304f0f6d8345f0 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 10 May 2023 22:11:53 -0700 Subject: [PATCH 08/28] c i: dont restore the cache for the build job, just save it - otherwise the save fails with a 'already creating' error --- .github/workflows/prs.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml index f572bc303..3be297694 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/prs.yml @@ -23,11 +23,6 @@ jobs: server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - name: Build and Test id: build run: | @@ -42,7 +37,7 @@ jobs: uses: actions/cache/save@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-maven-${{ github.run_id }} validate-with-examples: runs-on: ubuntu-latest @@ -61,7 +56,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-maven-${{ github.run_id }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-examples @@ -97,7 +92,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.m2 - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-maven-${{ github.run_id }} - uses: actions/checkout@v2 with: repository: paulcwarren/spring-content-gettingstarted From e550235b81d6d25dbc4e997fba10ce67d4c79bcf Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 15 May 2023 20:19:31 -0700 Subject: [PATCH 09/28] wip --- .../commons/store/factory/StoreImpl.java | 72 +++++++++++++++++++ .../commons/repository/ContentStore.java | 5 +- .../commons/repository/SetContentParams.java | 12 ++++ .../testsupport/TestStoreFactoryBean.java | 6 ++ .../fs/store/DefaultFilesystemStoreImpl.java | 5 ++ .../jpa/store/DefaultJpaStoreImpl.java | 6 ++ .../jpa/config/EnableJpaStoresTest.java | 6 ++ .../mongo/store/DefaultMongoStoreImpl.java | 6 ++ .../ContentStoreContentService.java | 17 ++++- .../http_405/MethodNotAllowedExceptionIT.java | 2 +- 10 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java index bc9a3550e..8e130fe4a 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java @@ -200,6 +200,78 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { return result; } + @Override + public Object setContent(Object property, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + Object result = null; + + File contentCopy = null; + TeeInputStream contentCopyStream = null; + try { + contentCopy = Files.createTempFile(copyContentRootPath, "contentCopy", ".tmp").toFile(); + contentCopyStream = new TeeInputStream(content, new FileOutputStream(contentCopy), true); + + org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = null; + BeforeSetContentEvent before = null; + + oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, propertyPath, delegate, contentCopyStream); + publisher.publishEvent(oldBefore); + + ContentStore contentStore = castToContentStore(delegate); + if (contentStore != null) { + before = new BeforeSetContentEvent(property, propertyPath, contentStore, contentCopyStream); + publisher.publishEvent(before); + } + + // inputstream was processed and replaced + if (oldBefore != null && oldBefore.getInputStream() != null && oldBefore.getInputStream().equals(contentCopyStream) == false) { + content = oldBefore.getInputStream(); + } + else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { + content = before.getInputStream(); + } + // content was processed but not replaced + else if (contentCopyStream != null && contentCopyStream.isDirty()) { + while (contentCopyStream.read(new byte[4096]) != -1) { + } + content = new FileInputStream(contentCopy); + } + + try { + result = castToDeprecatedContentStore(delegate).setContent(property, propertyPath, content, params); + } + catch (Exception e) { + throw e; + } + + org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); + oldAfter.setResult(result); + publisher.publishEvent(oldAfter); + + if (contentStore != null) { + AfterSetContentEvent after = new AfterSetContentEvent(property, propertyPath, contentStore); + after.setResult(result); + publisher.publishEvent(after); + } + } catch (FileNotFoundException fileNotFoundException) { + fileNotFoundException.printStackTrace(); + } catch (IOException ioException) { + ioException.printStackTrace(); + } finally { + if (contentCopyStream != null) { + IOUtils.closeQuietly(contentCopyStream); + } + if (contentCopy != null) { + try { + Files.deleteIfExists(contentCopy.toPath()); + } catch (IOException e) { + logger.error(String.format("Unable to delete content copy %s", contentCopy.toPath()), e); + } + } + } + + return result; + } + @Override public Object setContent(Object property, PropertyPath propertyPath, InputStream content, SetContentParams params) { Object result = null; diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java index a947695e6..897bafc9c 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java @@ -21,7 +21,10 @@ public interface ContentStore extends AssociativeSt @LockParticipant S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen); - @LockParticipant + @LockParticipant + S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params); + + @LockParticipant S setContent(S entity, Resource resourceContent); @LockParticipant diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java new file mode 100644 index 000000000..ffb623771 --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java @@ -0,0 +1,12 @@ +package org.springframework.content.commons.repository; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SetContentParams { + private long contentLength = -1; + @Builder.Default + private boolean overwriteExistingContent = true; +} diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java index f9183c5d1..d5ec86231 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java @@ -6,6 +6,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.GetResourceParams; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.Store; import org.springframework.content.commons.store.factory.AbstractStoreFactoryBean; import org.springframework.core.io.Resource; @@ -98,6 +99,11 @@ public Object setContent(Object property, PropertyPath propertyPath, InputStream return null; } + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return null; + } + @Override public Object setContent(Object property, PropertyPath propertyPath, Resource resourceContent) { // TODO Auto-generated method stub diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 2f000dfa6..0c4734e41 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -260,6 +260,11 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, return this.setContent(property, propertyPath, content, SetContentParams.builder().contentLength(contentLen).build()); } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + throw new UnsupportedOperationException(); + } + @Transactional @Override public S setContent(S property, PropertyPath propertyPath, InputStream content, SetContentParams params) { diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index 8ad80883d..d6461c447 100644 --- a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java +++ b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java @@ -21,6 +21,7 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; @@ -286,6 +287,11 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + throw new UnsupportedOperationException(); + } + @Transactional @Override public S setContent(S entity, Resource resourceContent) { diff --git a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java index 0462384c2..1b2cf05a4 100644 --- a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java +++ b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java @@ -25,6 +25,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.GetResourceParams; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.jpa.config.EnableJpaContentRepositories; import org.springframework.content.jpa.config.EnableJpaStores; import org.springframework.content.jpa.io.BlobResourceLoader; @@ -272,6 +273,11 @@ public TestEntity setContent(TestEntity entity, PropertyPath propertyPath, Input return null; } + @Override + public TestEntity setContent(TestEntity entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return null; + } + @Override public TestEntity setContent(TestEntity property, PropertyPath propertyPath, Resource resourceContent) { // TODO Auto-generated method stub diff --git a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java index bf31ff7ef..d76ff6022 100644 --- a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java +++ b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java @@ -19,6 +19,7 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; @@ -290,6 +291,11 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + throw new UnsupportedOperationException(); + } + @Override @Transactional public S setContent(S property, Resource resourceContent) { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index c840b5103..bbae0cdd3 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -22,8 +22,8 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.ContentStore; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.StoreInterfaceUtils; import org.springframework.content.rest.RestResource; @@ -187,6 +187,17 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, len = headers.getContentLength(); } argsList.add(len); + } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(org.springframework.content.commons.store.SetContentParams.class)) { + org.springframework.content.commons.store.SetContentParams params = org.springframework.content.commons.store.SetContentParams.builder().build(); + + // if available use the original content length + if (headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { + params.setContentLength(headers.getContentLength()); + } + + params.setOverwriteExistingContent(false); + + argsList.add(params); } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(SetContentParams.class)) { SetContentParams params = SetContentParams.builder().build(); @@ -356,6 +367,10 @@ public static class StoreExportedMethodsMap { static { SETCONTENT_METHODS = new Method[] { + ReflectionUtils.findMethod(org.springframework.content.commons.store.ContentStore.class, "setContent", Object.class, PropertyPath.class, InputStream.class, org.springframework.content.commons.store.SetContentParams.class), + ReflectionUtils.findMethod(org.springframework.content.commons.store.ContentStore.class, "setContent", Object.class, PropertyPath.class, InputStream.class, long.class), + ReflectionUtils.findMethod(org.springframework.content.commons.store.ContentStore.class, "setContent", Object.class, PropertyPath.class, Resource.class), + ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, PropertyPath.class, InputStream.class, SetContentParams.class), ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, PropertyPath.class, InputStream.class, long.class), ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, PropertyPath.class, Resource.class), }; diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java index a07071e33..4a599d87a 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java @@ -105,7 +105,7 @@ public class MethodNotAllowedExceptionIT { RestAssuredMockMvc.webAppContextSetup(webApplicationContext); }); - It("should throw a 405 Not Allowed", () -> { + FIt("should throw a 405 Not Allowed", () -> { TEntity tentity = new TEntity(); tentity = repo.save(tentity); From e608523f72f7f4059e6ec7f50ca43898361b4fda Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 15 May 2023 21:56:41 -0700 Subject: [PATCH 10/28] feat: implement the repository version of setContnt with SetContentParams --- .../fs/store/DefaultFilesystemStoreImpl.java | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 0c4734e41..5bb111e65 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -174,30 +174,9 @@ public boolean matches(Field field) { }); } - protected String calculateName(String name) { - Pattern p = Pattern.compile("^(.+)(Id|Len|Length|MimeType|Mimetype|ContentType|(? Date: Mon, 15 May 2023 21:57:29 -0700 Subject: [PATCH 11/28] feat: s3 module implements setContent with SetContentParams (unsupported operation) --- .../content/s3/store/DefaultS3StoreImpl.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java index 2400ad8e5..929ebaa2e 100644 --- a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java +++ b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java @@ -22,7 +22,9 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; @@ -49,7 +51,7 @@ public class DefaultS3StoreImpl implements org.springframework.content.commons.repository.Store, org.springframework.content.commons.repository.AssociativeStore, org.springframework.content.commons.repository.ContentStore, - AssociativeStore { + ContentStore { private static Log logger = LogFactory.getLog(DefaultS3StoreImpl.class); @@ -368,6 +370,16 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { + throw new UnsupportedOperationException(); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + throw new UnsupportedOperationException(); + } + @Override public S setContent(S property, Resource resourceContent) { try { From abd5cd68b5e557d3934119816232e22f817cd024 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 15 May 2023 21:59:39 -0700 Subject: [PATCH 12/28] feat: wire overwriteExistingContent property through from auto configuration to content store content service --- .../ContentRestAutoConfiguration.java | 9 +++++++ .../SpringBootContentRestConfigurer.java | 2 ++ .../ContentRestAutoConfigurationTest.java | 3 +++ .../ContentStoreContentService.java | 24 ++++++++++++------- .../rest/config/RestConfiguration.java | 12 +++++++++- .../http_405/MethodNotAllowedExceptionIT.java | 8 ++++--- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java index 5eb635142..8894b5b0e 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java @@ -25,6 +25,7 @@ public static class ContentRestProperties { private URI baseUri; private boolean fullyQualifiedLinks = RestConfiguration.FULLY_QUALIFIED_DEFAULTS_DEFAULT; private ShortcutRequestMappings requestMappings = new ShortcutRequestMappings(); + private boolean overwriteExistingContent = RestConfiguration.OVERWRITE_EXISTING_CONTENT_DEFAULT; public URI getBaseUri() { return baseUri; @@ -50,6 +51,14 @@ public void setShortcutRequestMappings(ShortcutRequestMappings requestMappings) this.requestMappings = requestMappings; } + public boolean getOverwriteExistingContent() { + return this.overwriteExistingContent; + } + + public void setOverwriteExistingContent(boolean overwriteExistingContent) { + this.overwriteExistingContent = overwriteExistingContent; + } + public static class ShortcutRequestMappings { private boolean disabled = false; diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java index 4321472bb..801d1ea1a 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java @@ -57,5 +57,7 @@ public void configure(RestConfiguration config) { } } } + + config.setOverwriteExistingContent(properties.getOverwriteExistingContent()); } } diff --git a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java index 0a9e81813..d644c38dc 100644 --- a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java +++ b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java @@ -55,12 +55,14 @@ public class ContentRestAutoConfigurationTest { System.setProperty("spring.content.rest.fully-qualified-links", "false"); System.setProperty("spring.content.rest.shortcut-request-mappings.disabled", "true"); System.setProperty("spring.content.rest.shortcut-request-mappings.excludes", "GET=a/b,c/d:PUT=*/*"); + System.setProperty("spring.content.rest.overwrite-existing-content", "false"); }); AfterEach(() -> { System.clearProperty("spring.content.rest.base-uri"); System.clearProperty("spring.content.rest.fully-qualified-links"); System.clearProperty("spring.content.rest.shortcut-request-mappings.disabled"); System.clearProperty("spring.content.rest.shortcut-request-mappings.excludes"); + System.clearProperty("spring.content.rest.overwrite-existing-content"); }); It("should have a filesystem properties bean with the correct properties set", () -> { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -72,6 +74,7 @@ public class ContentRestAutoConfigurationTest { assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).fullyQualifiedLinks(), is(false)); assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).shortcutRequestMappings().disabled(), is(true)); assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).shortcutRequestMappings().excludes(), is("GET=a/b,c/d:PUT=*/*")); + assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).getOverwriteExistingContent(), is(false)); assertThat(context.getBean(SpringBootContentRestConfigurer.class), is(not(nullValue()))); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index bbae0cdd3..04a6f03c2 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -195,7 +195,7 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, params.setContentLength(headers.getContentLength()); } - params.setOverwriteExistingContent(false); + params.setOverwriteExistingContent(config.overwriteExistingContent()); argsList.add(params); } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(SetContentParams.class)) { @@ -206,7 +206,7 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, params.setContentLength(headers.getContentLength()); } - params.setOverwriteExistingContent(false); + params.setOverwriteExistingContent(config.overwriteExistingContent()); argsList.add(params); } @@ -361,18 +361,20 @@ public static StoreExportedMethodsMap getExportedMethodsFor(Class storeInterface, PropertyPa this.storeInterface = storeInterface; this.path = path; this.getContentMethods = calculateExports(GETCONTENT_METHODS, path, exportContext); - this.setContentMethods = calculateExports(SETCONTENT_METHODS, path, exportContext); + if (org.springframework.content.commons.store.ContentStore.class.isAssignableFrom(storeInterface)) { + this.setContentMethods = calculateExports(SETCONTENT_METHODS_3x, path, exportContext); + } else { + this.setContentMethods = calculateExports(SETCONTENT_METHODS_2x, path, exportContext); + } this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS, path, exportContext); } diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java index d8a5189ad..acfbf3df9 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java @@ -49,7 +49,8 @@ @ComponentScan("internal.org.springframework.content.rest.controllers, org.springframework.data.rest.extensions, org.springframework.data.rest.versioning") public class RestConfiguration implements InitializingBean { - public static boolean FULLY_QUALIFIED_DEFAULTS_DEFAULT = true; + public static boolean OVERWRITE_EXISTING_CONTENT_DEFAULT = true; + public static boolean FULLY_QUALIFIED_DEFAULTS_DEFAULT = true; public static boolean SHORTCUT_LINKS_DEFAULT = true; private static final URI NO_URI = URI.create(""); @@ -64,6 +65,7 @@ public class RestConfiguration implements InitializingBean { private StoreCorsRegistry corsRegistry; private boolean fullyQualifiedLinks = FULLY_QUALIFIED_DEFAULTS_DEFAULT; private boolean shortcutLinks = SHORTCUT_LINKS_DEFAULT; + private boolean overwriteExistingContent = OVERWRITE_EXISTING_CONTENT_DEFAULT; private ConverterRegistry converters = new DefaultConversionService(); private Map, DomainTypeConfig> domainTypeConfigMap = new HashMap<>(); @@ -100,6 +102,14 @@ public void setShortcutLinks(boolean shortcutLinks) { this.shortcutLinks = shortcutLinks; } + public boolean overwriteExistingContent() { + return this.overwriteExistingContent; + } + + public void setOverwriteExistingContent(boolean overwriteExistingContent) { + this.overwriteExistingContent = overwriteExistingContent; + } + public StoreCorsRegistry getCorsRegistry() { return corsRegistry; } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java index 4a599d87a..d18c47f29 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java @@ -25,6 +25,8 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.fs.config.EnableFilesystemStores; import org.springframework.content.fs.io.FileSystemResourceLoader; import org.springframework.content.fs.store.FilesystemContentStore; @@ -99,7 +101,7 @@ public class MethodNotAllowedExceptionIT { }); }); - Describe("when no setContent methods are not exported", () -> { + Describe("when setContent methods are not exported", () -> { BeforeEach(() -> { RestAssuredMockMvc.webAppContextSetup(webApplicationContext); @@ -190,7 +192,7 @@ public interface UnexportedContentStore extends FilesystemContentStore Date: Tue, 16 May 2023 22:16:47 -0700 Subject: [PATCH 13/28] feat: implement new setContent with SetContentParams method in azure storage module --- .../azure/store/DefaultAzureStorageImpl.java | 20 +++++++++-- .../content/azure/it/AzureStorageIT.java | 34 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java index fb2f70eff..fcfed0e32 100644 --- a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java +++ b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java @@ -21,7 +21,9 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; @@ -45,7 +47,7 @@ public class DefaultAzureStorageImpl implements org.springframework.content.commons.repository.Store, org.springframework.content.commons.repository.AssociativeStore, org.springframework.content.commons.repository.ContentStore, - AssociativeStore { + ContentStore { private static Log logger = LogFactory.getLog(DefaultAzureStorageImpl.class); @@ -290,14 +292,26 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content) { @Transactional @Override public S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(entity, propertyPath, content, org.springframework.content.commons.store.SetContentParams.builder().contentLength(contentLen).build()); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()).build()); + } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } Object contentId = property.getContentId(entity); - if (contentId == null) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -323,7 +337,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo } try { - long lenToSet = contentLen; + long lenToSet = params.getContentLength(); if (lenToSet == -1L) { lenToSet = resource.contentLength(); } diff --git a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java index 4b77904a9..97e53edb1 100644 --- a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java +++ b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java @@ -1,12 +1,9 @@ package internal.org.springframework.content.azure.it; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.AfterEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -18,6 +15,7 @@ import java.io.OutputStream; import java.util.UUID; +import com.azure.storage.blob.specialized.BlockBlobClient; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -37,6 +35,7 @@ import org.springframework.content.commons.repository.StoreAccessException; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -379,6 +378,31 @@ public class AzureStorageIT { }); }); + + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + BlobContainerClient c = builder.buildClient().getBlobContainerClient("azure-test-bucket"); + + String contentId = entity.getContentId(); + assertThat(c.getBlobClient(contentId).getBlockBlobClient().exists(), is(true)); + + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + entity = repo.save(entity); + + boolean matches = false; + try (InputStream content = store.getContent(entity)) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(c.getBlobClient(contentId).getBlockBlobClient().exists(), is(true)); + + assertThat(entity.getContentId(), is(not(contentId))); + + assertThat(c.getBlobClient(entity.getContentId()).getBlockBlobClient().exists(), is(true)); + }); + }); + Context("when content is deleted", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); From c5cac3c61e1c6a2554b65698a670b754766092fc Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 16 May 2023 22:17:12 -0700 Subject: [PATCH 14/28] feat: implement new setContent with SetContentParams method in the mongo storage module --- .../mongo/store/DefaultMongoStoreImpl.java | 25 +++++++++---- .../content/mongo/it/MongoStoreIT.java | 37 ++++++++++++++----- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java index d76ff6022..f7affe3e1 100644 --- a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java +++ b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java @@ -21,6 +21,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; @@ -38,7 +39,7 @@ public class DefaultMongoStoreImpl implements org.springframework.content.commons.repository.Store, org.springframework.content.commons.repository.AssociativeStore, org.springframework.content.commons.repository.ContentStore, - AssociativeStore { + ContentStore { private static Log logger = LogFactory.getLog(DefaultMongoStoreImpl.class); @@ -244,14 +245,27 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content) { @Transactional @Override public S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(entity, propertyPath, content, org.springframework.content.commons.store.SetContentParams.builder().contentLength(contentLen).build()); + } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()) + .build()); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } Object contentId = property.getContentId(entity); - if (contentId == null) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -278,7 +292,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo } try { - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = resource.contentLength(); } @@ -291,11 +305,6 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } - @Override - public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { - throw new UnsupportedOperationException(); - } - @Override @Transactional public S setContent(S property, Resource resourceContent) { diff --git a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java index 83b951de8..a2a5c33c3 100644 --- a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java +++ b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java @@ -1,13 +1,7 @@ package internal.org.springframework.content.mongo.it; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.AfterEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; @@ -29,6 +23,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.PlacementService; import org.springframework.content.mongo.config.EnableMongoStores; @@ -87,6 +82,8 @@ public class MongoStoreIT { context.register(TestConfig.class); context.refresh(); + gridFsTemplate = context.getBean(GridFsTemplate.class); + repo = context.getBean(TestEntityRepository.class); store = context.getBean(TestEntityStore.class); @@ -359,7 +356,29 @@ public class MongoStoreIT { }); }); - Context("when content is deleted", () -> { + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + String contentId = entity.getContentId(); + assertThat(gridFsTemplate.getResource(contentId).exists(), is(true)); + + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + entity = repo.save(entity); + + boolean matches = false; + try (InputStream content = store.getContent(entity)) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(gridFsTemplate.getResource(contentId).exists(), is(true)); + + assertThat(entity.getContentId(), is(not(contentId))); + + assertThat(gridFsTemplate.getResource(entity.getContentId()).exists(), is(true)); + }); + }); + + Context("when content is deleted", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); From 42d2c1d0b37a7e1f87e67ec344dc030707cde923 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 17 May 2023 20:52:04 -0700 Subject: [PATCH 15/28] feat: implement new setContent with SetContentParams method in gcs storage module --- .../content/azure/it/AzureStorageIT.java | 1 - .../gcs/store/DefaultGCPStorageImpl.java | 24 ++++++++++++-- .../content/gcs/it/GCPStorageIT.java | 31 ++++++++++++++++--- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java index 97e53edb1..eaf9f3199 100644 --- a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java +++ b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java @@ -378,7 +378,6 @@ public class AzureStorageIT { }); }); - Context("when content is updated and not overwritten", () -> { It("should have the updated content", () -> { BlobContainerClient c = builder.buildClient().getBlobContainerClient("azure-test-bucket"); diff --git a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java index 236929228..1e308a520 100644 --- a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java +++ b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java @@ -20,7 +20,9 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; @@ -45,7 +47,7 @@ public class DefaultGCPStorageImpl implements org.springframework.content.commons.repository.Store, org.springframework.content.commons.repository.AssociativeStore, org.springframework.content.commons.repository.ContentStore, - AssociativeStore { + ContentStore { private static Log logger = LogFactory.getLog(DefaultGCPStorageImpl.class); @@ -290,14 +292,30 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content) { @Transactional @Override public S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(contentLen) + .build()); + } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()) + .build()); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { ContentProperty property = this.mappingContext .getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } Object contentId = property.getContentId(entity); - if (contentId == null) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -325,7 +343,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo } try { - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = resource.contentLength(); } diff --git a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java index 68f8fcdc9..1b1d46ea5 100644 --- a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java +++ b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java @@ -1,12 +1,9 @@ package internal.org.springframework.content.gcs.it; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.AfterEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -18,6 +15,7 @@ import java.io.OutputStream; import java.util.UUID; +import com.google.cloud.storage.BlobId; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -34,6 +32,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.gcs.config.EnableGCPStorage; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -299,6 +298,7 @@ public class GCPStorageIT { store.setContent(entity, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + entity = repo.save(entity); }); It("should be able to store new content", () -> { @@ -372,6 +372,27 @@ public class GCPStorageIT { }); }); + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + String contentId = entity.getContentId(); + assertThat(contentId, is(not(nullValue()))); + assertThat(storage.get(BlobId.of("test-bucket", contentId)).exists(), is(true)); + + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + entity = repo.save(entity); + + boolean matches = false; + try (InputStream content = store.getContent(entity)) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(entity.getContentId(), is(not(contentId))); + + assertThat(storage.get(BlobId.of("test-bucket", entity.getContentId())).exists(), is(true)); + }); + }); + Context("when content is deleted", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); From fdfed43e72c491e1b907d121ade81f0cd157e841 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 17 May 2023 21:25:28 -0700 Subject: [PATCH 16/28] feat: implement new setContent with SetContentParams method in jpa storage module --- .../jpa/store/DefaultJpaStoreImpl.java | 28 +++++++++++++------ .../content/jpa/ContentStoreIT.java | 25 +++++++++++++---- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index d6461c447..34159a9a3 100644 --- a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java +++ b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java @@ -23,6 +23,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.store.AssociativeStore; +import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.StoreAccessException; import org.springframework.content.commons.utils.BeanUtils; @@ -41,7 +42,7 @@ public class DefaultJpaStoreImpl implements org.springframework.content.commons.repository.Store, org.springframework.content.commons.repository.AssociativeStore, org.springframework.content.commons.repository.ContentStore, - AssociativeStore { + ContentStore { private static Log logger = LogFactory.getLog(DefaultJpaStoreImpl.class); @@ -240,12 +241,28 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content) { @Transactional @Override public S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(entity, propertyPath, content, org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(contentLen) + .build()); + } + @Transactional + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.setContent(entity, propertyPath, content, org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()) + .build()); + } + + @Transactional + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); // TODO: property == null? SID contentId = getContentId(entity, propertyPath); - if (contentId == null) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -278,7 +295,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo property.setContentId(entity, ((BlobResource) resource).getId(), null); - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = readLen; } @@ -287,11 +304,6 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } - @Override - public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { - throw new UnsupportedOperationException(); - } - @Transactional @Override public S setContent(S entity, Resource resourceContent) { diff --git a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java index 1bde9fe5e..e60eea243 100644 --- a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java +++ b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java @@ -1,12 +1,9 @@ package internal.org.springframework.content.jpa; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.AfterEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; import static internal.org.springframework.content.jpa.StoreIT.getContextName; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; @@ -22,6 +19,7 @@ import org.junit.Assert; import org.junit.runner.RunWith; import org.springframework.content.commons.property.PropertyPath; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; @@ -199,6 +197,23 @@ public class ContentStoreIT { }); }); + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + String contentId = claim.getClaimForm().getContentId(); + + claimFormStore.setContent(claim, PropertyPath.from("claimForm/content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + claim = claimRepo.save(claim); + + boolean matches = false; + try (InputStream content = claimFormStore.getContent(claim, PropertyPath.from("claimForm/content"))) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(claim.getClaimForm().getContentId(), is(not(contentId))); + }); + }); + Context("when content is deleted", () -> { BeforeEach(() -> { id = claim.getClaimForm().getContentId(); From c9498b3e356da46ee60f845775b5c34dfcc3e681 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 17 May 2023 22:14:07 -0700 Subject: [PATCH 17/28] feat: implement new setContent with SetContentParams method in s3 storage module --- .../content/s3/store/DefaultS3StoreImpl.java | 30 ++++++++------ .../content/s3/config/EnableS3StoresTest.java | 2 +- .../content/s3/it/S3StoreIT.java | 40 +++++++++++++++++-- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java index 929ebaa2e..75bdf3073 100644 --- a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java +++ b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java @@ -321,14 +321,30 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content) { @Transactional @Override public S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(contentLen) + .build()); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.setContent(entity, propertyPath, content, + org.springframework.content.commons.store.SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()) + .build()); + } + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } Object contentId = property.getContentId(entity); - if (contentId == null) { + if (contentId == null || !params.isOverwriteExistingContent()) { Serializable newId = UUID.randomUUID().toString(); @@ -358,7 +374,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo } try { - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = resource.contentLength(); } @@ -370,16 +386,6 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, lo return entity; } - @Override - public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { - throw new UnsupportedOperationException(); - } - - @Override - public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { - throw new UnsupportedOperationException(); - } - @Override public S setContent(S property, Resource resourceContent) { try { diff --git a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/config/EnableS3StoresTest.java b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/config/EnableS3StoresTest.java index d48a18e66..5b5e05e06 100644 --- a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/config/EnableS3StoresTest.java +++ b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/config/EnableS3StoresTest.java @@ -47,7 +47,7 @@ @RunWith(Ginkgo4jRunner.class) public class EnableS3StoresTest { - private static final String BUCKET = "test-bucket"; + private static final String BUCKET = "aws-test-bucket"; static { System.setProperty("spring.content.s3.bucket", BUCKET); diff --git a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java index aaed2002e..98fa1f63b 100644 --- a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java +++ b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java @@ -25,6 +25,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.StoreAccessException; import org.springframework.content.commons.store.ContentStore; +import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.s3.config.EnableS3Stores; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -42,9 +43,7 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.CreateBucketRequest; -import software.amazon.awssdk.services.s3.model.HeadBucketRequest; -import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.*; import javax.sql.DataSource; import java.io.ByteArrayInputStream; @@ -107,6 +106,21 @@ public class S3StoreIT { .bucket(BUCKET) .build(); client.createBucket(bucketRequest); + + boolean found = false; + while (!found) { + headBucketRequest = HeadBucketRequest.builder() + .bucket(BUCKET) + .build(); + try { + client.headBucket(headBucketRequest); + found = true; + } catch (NoSuchBucketException e2) { + } + + System.out.println("sleeping..."); + Thread.sleep(100); + } } RandomString random = new RandomString(5); @@ -335,7 +349,6 @@ public class S3StoreIT { Assert.assertEquals(entity.getRenditionLen(), 40L); }); - It("should set Content-Type of stored content to value from field annotated with @MimeType", () -> { // content S3StoreResource resource = (S3StoreResource) store.getResource(entity); @@ -395,6 +408,25 @@ public class S3StoreIT { }); }); + Context("when content is updated and not overwritten", () -> { + It("should have the updated content", () -> { + String contentId = entity.getContentId(); + client.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(contentId).build()); + + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + entity = repo.save(entity); + + boolean matches = false; + try (InputStream content = store.getContent(entity)) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content); + assertThat(matches, is(true)); + } + + assertThat(entity.getContentId(), is(not(contentId))); + client.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(entity.getContentId()).build()); + }); + }); + Context("when content is deleted", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); From 52b729eac63d6220a1b702836067dd1b8134b53b Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 18 May 2023 21:53:05 -0700 Subject: [PATCH 18/28] feat: encryption storage module now support new ContentStore - as does ContentStoreAware interface optionally implemented by fragments --- .../store/factory/StoreMethodInterceptor.java | 10 +- .../content/fragments/RenderableImpl.java | 23 +- .../commons/fragments/ContentStoreAware.java | 2 +- .../content/commons/utils/AssertUtils.java | 15 ++ .../fragments/EncryptingContentStoreImpl.java | 230 +++++++++++++----- .../encryption/EncryptingContentStore.java | 4 + .../content/encryption/s3/EncryptionIT.java | 19 +- .../gcs/it/DeprecatedGCPStorageIT.java | 2 +- .../content/gcs/it/GCPStorageIT.java | 2 +- .../content/fragments/SearchableImpl.java | 4 + 10 files changed, 229 insertions(+), 82 deletions(-) create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/utils/AssertUtils.java diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreMethodInterceptor.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreMethodInterceptor.java index b3b228691..321732ac7 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreMethodInterceptor.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreMethodInterceptor.java @@ -29,11 +29,14 @@ public class StoreMethodInterceptor implements MethodInterceptor { private Map methodCache = new ConcurrentReferenceHashMap<>(); // ContentStoreAware methods + private static Method deprecatedSetContentStoreMethod; private static Method setContentStoreMethod; static { - setContentStoreMethod = ReflectionUtils.findMethod(ContentStoreAware.class, "setContentStore", ContentStore.class); - Assert.notNull(setContentStoreMethod); + deprecatedSetContentStoreMethod = ReflectionUtils.findMethod(ContentStoreAware.class, "setContentStore", ContentStore.class); + Assert.notNull(deprecatedSetContentStoreMethod, "setContentStore method not found"); + setContentStoreMethod = ReflectionUtils.findMethod(ContentStoreAware.class, "setContentStore", org.springframework.content.commons.store.ContentStore.class); + Assert.notNull(setContentStoreMethod, "setContentStore method not found"); } public StoreMethodInterceptor() { @@ -60,6 +63,9 @@ public Object invoke(MethodInvocation invocation) throws Throwable { fragment.orElseThrow(() -> new IllegalStateException(format("No fragment found for method %s", invocation.getMethod()))); StoreFragment f = fragment.get(); + if (f.hasImplementationMethod(deprecatedSetContentStoreMethod)) { + ReflectionUtils.invokeMethod(deprecatedSetContentStoreMethod, f.getImplementation(), invocation.getThis()); + } if (f.hasImplementationMethod(setContentStoreMethod)) { ReflectionUtils.invokeMethod(setContentStoreMethod, f.getImplementation(), invocation.getThis()); } diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/fragments/RenderableImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/fragments/RenderableImpl.java index f71218cb4..fb35c6c07 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/fragments/RenderableImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/fragments/RenderableImpl.java @@ -25,6 +25,7 @@ public class RenderableImpl implements Renderable, ContentStoreAware { private static final Log LOGGER = LogFactory.getLog(RenderableImpl.class); + private org.springframework.content.commons.store.ContentStore store; private ContentStore contentStore; private MappingContext mappingContext; @@ -33,7 +34,7 @@ public class RenderableImpl implements Renderable, ContentStoreAware { private List providers = new ArrayList<>(); - public RenderableImpl() { + public RenderableImpl() { this.mappingContext = new MappingContext("/", "."); } @@ -57,7 +58,12 @@ public void setContentStore(ContentStore store) { this.contentStore = store; } - public RenditionService getRenditionService() { + @Override + public void setContentStore(org.springframework.content.commons.store.ContentStore store) { + this.store = store; + } + + public RenditionService getRenditionService() { if (this.renditionService == null) { this.renditionService = new RenditionServiceImpl(providers.toArray(new RenditionProvider[0])); } @@ -80,7 +86,11 @@ public InputStream getRendition(Object entity, String mimeType) { if (this.getRenditionService().canConvert(fromMimeType, mimeType)) { InputStream content = null; try { - content = contentStore.getContent(entity); + if (store != null) { + content = store.getContent(entity); + } else if (contentStore != null) { + content = contentStore.getContent(entity); + } if (content != null) { return this.getRenditionService().convert(fromMimeType, content, mimeType); } @@ -110,7 +120,12 @@ public InputStream getRendition(Object entity, PropertyPath propertyPath, String if (this.getRenditionService().canConvert(fromMimeType.toString(), mimeType)) { try { - Resource r = contentStore.getResource(entity, propertyPath); + Resource r = null; + if (store != null) { + r = store.getResource(entity, propertyPath); + } else if (contentStore != null) { + r = contentStore.getResource(entity, propertyPath); + } if (r != null) { try (InputStream content = r.getInputStream()) { if (content != null) { diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/fragments/ContentStoreAware.java b/spring-content-commons/src/main/java/org/springframework/content/commons/fragments/ContentStoreAware.java index c5769c440..48b7c5e5c 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/fragments/ContentStoreAware.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/fragments/ContentStoreAware.java @@ -7,5 +7,5 @@ public interface ContentStoreAware { void setDomainClass(Class domainClass); void setIdClass(Class idClass); void setContentStore(ContentStore store); - + void setContentStore(org.springframework.content.commons.store.ContentStore store); } diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/utils/AssertUtils.java b/spring-content-commons/src/main/java/org/springframework/content/commons/utils/AssertUtils.java new file mode 100644 index 000000000..87dbee775 --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/utils/AssertUtils.java @@ -0,0 +1,15 @@ +package org.springframework.content.commons.utils; + +public final class AssertUtils { + private AssertUtils() {} + + public static void atLeastOneNotNull(Object[] objects, String message) { + boolean isNull = true; + for (Object o : objects) { + isNull &= o == null; + } + if (isNull) { + throw new IllegalArgumentException(message); + } + } +} diff --git a/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java b/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java index aa3e4824d..1bab5f22b 100644 --- a/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java +++ b/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java @@ -1,31 +1,32 @@ package internal.org.springframework.content.fragments; import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.content.commons.fragments.ContentStoreAware; import org.springframework.content.commons.io.RangeableResource; import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; -import org.springframework.content.commons.repository.ContentStore; -import org.springframework.content.commons.repository.GetResourceParams; -import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.repository.StoreAccessException; +import org.springframework.content.commons.repository.*; +import org.springframework.content.commons.utils.AssertUtils; import org.springframework.content.encryption.EnvelopeEncryptionService; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.util.Pair; import org.springframework.util.Assert; -import org.springframework.vault.core.VaultOperations; import javax.crypto.CipherInputStream; -import java.io.*; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; -public class EncryptingContentStoreImpl implements ContentStore, ContentStoreAware { +public class EncryptingContentStoreImpl implements ContentStore, org.springframework.content.commons.store.ContentStore, ContentStoreAware { @Autowired(required = false) private MappingContext mappingContext = null; @@ -42,6 +43,8 @@ public class EncryptingContentStoreImpl implements private ContentStore delegate; + private org.springframework.content.commons.store.ContentStore storeDelegate; + private Class domainClass; public EncryptingContentStoreImpl() { @@ -60,35 +63,70 @@ public S setContent(S o, InputStream inputStream) { } @Override - public S setContent(S o, PropertyPath propertyPath, InputStream inputStream) { - Assert.notNull(o); - Assert.notNull(propertyPath); - Assert.notNull(inputStream); + public S setContent(S entity, PropertyPath propertyPath, InputStream content) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(content, "content not set"); + AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } - Pair encryptionContext = encrypter.encrypt(inputStream, this.keyRing); - contentProperty.setCustomProperty(o, this.encryptionKeyContentProperty, encryptionContext.getSecond()); - return (S) delegate.setContent(o, propertyPath, encryptionContext.getFirst()); + Pair encryptionContext = encrypter.encrypt(content, this.keyRing); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, encryptionContext.getSecond()); + if (storeDelegate != null) { + return (S) storeDelegate.setContent(entity, propertyPath, encryptionContext.getFirst()); + } else if (delegate != null) { + return (S) delegate.setContent(entity, propertyPath, encryptionContext.getFirst()); + } + throw new IllegalStateException("no store set"); } @Override - public S setContent(S o, PropertyPath propertyPath, InputStream inputStream, long l) { - Assert.notNull(o); - Assert.notNull(propertyPath); - Assert.notNull(inputStream); + public S setContent(S entity, PropertyPath propertyPath, InputStream inputStream, long l) { + AssertUtils.atLeastOneNotNull(new Object[] {storeDelegate, delegate}, "store not set"); + if (storeDelegate != null) { + return this.setContent(entity, propertyPath, inputStream, org.springframework.content.commons.store.SetContentParams.builder().contentLength(l).build()); + } + return this.setContent(entity, propertyPath, inputStream, SetContentParams.builder().contentLength(l).build()); + } - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(content, "content not set"); + Assert.notNull(params, "params not set"); + Assert.notNull(delegate, "store not set"); + + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } - Pair encryptionContext = encrypter.encrypt(inputStream, this.keyRing); - contentProperty.setCustomProperty(o, this.encryptionKeyContentProperty, encryptionContext.getSecond()); - return (S) delegate.setContent(o, propertyPath, encryptionContext.getFirst(), l); + Pair encryptionContext = encrypter.encrypt(content, this.keyRing); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, encryptionContext.getSecond()); + return (S) delegate.setContent(entity, propertyPath, encryptionContext.getFirst(), params); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(content, "content not set"); + Assert.notNull(params, "params not set"); + Assert.notNull(storeDelegate, "store not set"); + + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); + if (contentProperty == null) { + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); + } + + Pair encryptionContext = encrypter.encrypt(content, this.keyRing); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, encryptionContext.getSecond()); + return (S) storeDelegate.setContent(entity, propertyPath, encryptionContext.getFirst(), params); } @Override @@ -97,21 +135,25 @@ public S setContent(S o, Resource resource) { } @Override - public S setContent(S o, PropertyPath propertyPath, Resource resource) { - Assert.notNull(o); - Assert.notNull(propertyPath); - Assert.notNull(resource); + public S setContent(S entity, PropertyPath propertyPath, Resource resource) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(resource, "resource not set"); + AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); try { - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } Pair encryptionContext = null; encryptionContext = encrypter.encrypt(resource.getInputStream(), this.keyRing); - contentProperty.setCustomProperty(o, this.encryptionKeyContentProperty, encryptionContext.getSecond()); - return (S) delegate.setContent(o, propertyPath, new InputStreamResource(encryptionContext.getFirst())); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, encryptionContext.getSecond()); + if (storeDelegate != null) { + return (S) delegate.setContent(entity, propertyPath, new InputStreamResource(encryptionContext.getFirst())); + } + return (S) delegate.setContent(entity, propertyPath, new InputStreamResource(encryptionContext.getFirst())); } catch (IOException e) { throw new StoreAccessException("error encrypting resource", e); } @@ -123,18 +165,24 @@ public S unsetContent(S o) { } @Override - public S unsetContent(S o, PropertyPath propertyPath) { - Assert.notNull(o); - Assert.notNull(propertyPath); + public S unsetContent(S entity, PropertyPath propertyPath) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } - S entityToReturn = (S) delegate.unsetContent(o, propertyPath); + S entityToReturn = null; + if (storeDelegate != null) { + entityToReturn = (S) storeDelegate.unsetContent(entity, propertyPath); + } else if (delegate != null) { + entityToReturn = (S) delegate.unsetContent(entity, propertyPath); + } - contentProperty.setCustomProperty(o, this.encryptionKeyContentProperty, null); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, null); return entityToReturn; } @@ -145,22 +193,28 @@ public InputStream getContent(S o) { } @Override - public InputStream getContent(S o, PropertyPath propertyPath) { - Assert.notNull(o); - Assert.notNull(propertyPath); - - InputStream encryptedContentStream = delegate.getContent(o, propertyPath); + public InputStream getContent(S entity, PropertyPath propertyPath) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); + + InputStream encryptedContentStream = null; + if (storeDelegate != null) { + encryptedContentStream = delegate.getContent(entity, propertyPath); + } else if (delegate != null) { + encryptedContentStream = delegate.getContent(entity, propertyPath); + } InputStream unencryptedStream = null; if (encryptedContentStream != null) { - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } // remove cast and use conversion service - unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(o, this.encryptionKeyContentProperty), encryptedContentStream, 0, this.keyRing); + unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(entity, this.encryptionKeyContentProperty), encryptedContentStream, 0, this.keyRing); } return unencryptedStream; @@ -172,22 +226,28 @@ public Resource getResource(S o) { } @Override - public Resource getResource(S o, PropertyPath propertyPath) { - Assert.notNull(o); - Assert.notNull(propertyPath); - - Resource r = delegate.getResource(o, propertyPath); + public Resource getResource(S entity, PropertyPath propertyPath) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); + + Resource r = null; + if (storeDelegate != null) { + r = storeDelegate.getResource(entity, propertyPath); + } else if (delegate != null) { + r = delegate.getResource(entity, propertyPath); + } if (r != null) { InputStream unencryptedStream = null; try { - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } // remove cast and use conversion service - unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(o, this.encryptionKeyContentProperty), r.getInputStream(), 0, this.keyRing); + unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(entity, this.encryptionKeyContentProperty), r.getInputStream(), 0, this.keyRing); r = new InputStreamResource(new SkipInputStream(unencryptedStream)); } catch (IOException e) { throw new StoreAccessException("error encrypting resource", e); @@ -198,23 +258,50 @@ public Resource getResource(S o, PropertyPath propertyPath) { } @Override - public Resource getResource(S o, PropertyPath propertyPath, GetResourceParams params) { - Assert.notNull(o); - Assert.notNull(propertyPath); + public Resource getResource(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.GetResourceParams params) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(storeDelegate, "store not set"); + + Resource r = storeDelegate.getResource(entity, propertyPath, rewriteParamsForCTR(params)); + + if (r != null) { + InputStream unencryptedStream = null; + try { + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); + if (contentProperty == null) { + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); + } + + // remove cast and use conversion service + unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(entity, this.encryptionKeyContentProperty), r.getInputStream(), getOffset(r, params), this.keyRing); + r = new InputStreamResource(unencryptedStream); + } catch (IOException e) { + throw new StoreAccessException("error encrypting resource", e); + } + } + + return r; + } + + @Override + public Resource getResource(S entity, PropertyPath propertyPath, GetResourceParams params) { + Assert.notNull(entity, "entity not set"); + Assert.notNull(propertyPath, "propertyPath not set"); + Assert.notNull(delegate, "store not set"); - GetResourceParams ctrParams = rewriteParamsForCTR(params); - Resource r = delegate.getResource(o, propertyPath, ctrParams); + Resource r = delegate.getResource(entity, propertyPath, rewriteParamsForCTR(params)); if (r != null) { InputStream unencryptedStream = null; try { - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + ContentProperty contentProperty = getMappingContext().getContentProperty(entity.getClass(), propertyPath.getName()); if (contentProperty == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } // remove cast and use conversion service - unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(o, this.encryptionKeyContentProperty), r.getInputStream(), getOffset(r, params), this.keyRing); + unencryptedStream = encrypter.decrypt((byte[]) contentProperty.getCustomProperty(entity, this.encryptionKeyContentProperty), r.getInputStream(), getOffset(r, params), this.keyRing); r = new InputStreamResource(unencryptedStream); } catch (IOException e) { throw new StoreAccessException("error encrypting resource", e); @@ -233,6 +320,15 @@ private GetResourceParams rewriteParamsForCTR(GetResourceParams params) { return GetResourceParams.builder().range("bytes=" + blockBegin + "-" + StringUtils.substringAfter(params.getRange(), "-")).build(); } + private org.springframework.content.commons.store.GetResourceParams rewriteParamsForCTR(org.springframework.content.commons.store.GetResourceParams params) { + if (params.getRange() == null) { + return params; + } + int begin = Integer.parseInt(StringUtils.substringBetween(params.getRange(), "bytes=", "-")); + int blockBegin = begin - (begin % 16); + return org.springframework.content.commons.store.GetResourceParams.builder().range("bytes=" + blockBegin + "-" + StringUtils.substringAfter(params.getRange(), "-")).build(); + } + private int getOffset(Resource r, GetResourceParams params) { int offset = 0; @@ -244,6 +340,17 @@ private int getOffset(Resource r, GetResourceParams params) { return Integer.parseInt(StringUtils.substringBetween(params.getRange(), "bytes=", "-")); } + private int getOffset(Resource r, org.springframework.content.commons.store.GetResourceParams params) { + int offset = 0; + + if (r instanceof RangeableResource == false) + return offset; + if (params.getRange() == null) + return offset; + + return Integer.parseInt(StringUtils.substringBetween(params.getRange(), "bytes=", "-")); + } + @Override public void associate(S o, SID serializable) { throw new UnsupportedOperationException(); @@ -283,6 +390,11 @@ public void setContentStore(ContentStore store) { this.delegate = store; } + @Override + public void setContentStore(org.springframework.content.commons.store.ContentStore store) { + this.storeDelegate = store; + } + public void setStoreInterfaceClass(Class storeInterfaceClass) { configure(storeInterfaceClass); } diff --git a/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java b/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java index 1a76a47b8..d931915ee 100644 --- a/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java +++ b/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java @@ -3,6 +3,7 @@ import org.springframework.content.commons.fragments.ContentStoreAware; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.GetResourceParams; +import org.springframework.content.commons.repository.SetContentParams; import org.springframework.core.io.Resource; import java.io.InputStream; @@ -14,8 +15,11 @@ public interface EncryptingContentStore extends Con InputStream getContent(S o, PropertyPath propertyPath); S setContent(S o, PropertyPath propertyPath, InputStream inputStream); S setContent(S entity, PropertyPath propertyPath, InputStream content, long contentLen); + S setContent(S entity, PropertyPath propertyPath, InputStream content, SetContentParams params); + S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.store.SetContentParams params); S setContent(S o, PropertyPath propertyPath, Resource resource); Resource getResource(S entity, PropertyPath propertyPath); Resource getResource(S entity, PropertyPath propertyPath, GetResourceParams params); + Resource getResource(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.GetResourceParams params); S unsetContent(S entity, PropertyPath propertyPath); } diff --git a/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java b/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java index a76fb376f..bad2a4f24 100644 --- a/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java +++ b/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java @@ -5,9 +5,12 @@ import internal.org.springframework.content.fragments.EncryptingContentStoreConfiguration; import internal.org.springframework.content.fragments.EncryptingContentStoreConfigurer; import internal.org.springframework.content.rest.boot.autoconfigure.ContentRestAutoConfiguration; -import internal.org.springframework.content.s3.boot.autoconfigure.S3ContentAutoConfiguration; import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.restassured.module.mockmvc.response.MockMvcResponse; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -18,11 +21,8 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.content.commons.annotations.ContentId; import org.springframework.content.commons.annotations.ContentLength; @@ -37,7 +37,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.repository.CrudRepository; -import org.springframework.test.context.ContextConfiguration; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.client.VaultEndpoint; @@ -48,24 +47,16 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import java.lang.reflect.Field; import java.net.URISyntaxException; -import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.UUID; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; -import static io.restassured.module.mockmvc.RestAssuredMockMvc.*; - @RunWith(Ginkgo4jSpringRunner.class) @SpringBootTest(classes = EncryptionIT.Application.class, webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class EncryptionIT { diff --git a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/DeprecatedGCPStorageIT.java b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/DeprecatedGCPStorageIT.java index fe1616254..5374e1d5a 100644 --- a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/DeprecatedGCPStorageIT.java +++ b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/DeprecatedGCPStorageIT.java @@ -73,7 +73,7 @@ public class DeprecatedGCPStorageIT { private String resourceLocation; static { - System.setProperty("spring.content.gcp.storage.bucket", "test"); + System.setProperty("spring.content.gcp.storage.bucket", "test-bucket"); } { diff --git a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java index 1b1d46ea5..079c2c308 100644 --- a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java +++ b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java @@ -82,7 +82,7 @@ public class GCPStorageIT { private String resourceLocation; static { - System.setProperty("spring.content.gcp.storage.bucket", "test"); + System.setProperty("spring.content.gcp.storage.bucket", "test-bucket"); } { diff --git a/spring-content-solr/src/main/java/internal/org/springframework/content/fragments/SearchableImpl.java b/spring-content-solr/src/main/java/internal/org/springframework/content/fragments/SearchableImpl.java index fcf7eb936..bbc404af2 100644 --- a/spring-content-solr/src/main/java/internal/org/springframework/content/fragments/SearchableImpl.java +++ b/spring-content-solr/src/main/java/internal/org/springframework/content/fragments/SearchableImpl.java @@ -287,6 +287,10 @@ protected Class getDomainClass() { public void setContentStore(ContentStore store) { } + @Override + public void setContentStore(org.springframework.content.commons.store.ContentStore store) { + } + @SuppressWarnings("unchecked") private T wrapResult(Class returnType, List content, Pageable pageable, long total) { From b5e737a437b12ca26f749434d2f68fdd33a210bf Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 23 May 2023 22:16:07 -0700 Subject: [PATCH 19/28] fix: store rest controller multipart form put handler mis-specified an argument meaning the input Resource could not be resolved --- .../rest/controllers/StoreRestController.java | 4 +-- .../content/rest/controllers/Content.java | 28 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java index 68c32fa45..6d1d0cb9d 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java @@ -121,10 +121,10 @@ public void putContent(HttpServletRequest request, HttpServletResponse response, @ResponseBody public void putMultipartContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, @RequestParam("file") MultipartFile multiPart, - StoreResource resource) + Resource resource) throws IOException, MethodNotAllowedException { - StoreResource storeResource = resource; + StoreResource storeResource = (StoreResource)resource; ContentService contentService = contentServiceFactory.getContentService(storeResource); diff --git a/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/Content.java b/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/Content.java index 4ca7193f7..fe8e85cf5 100644 --- a/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/Content.java +++ b/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/Content.java @@ -6,20 +6,19 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.ByteArrayInputStream; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.Optional; +import java.util.UUID; import org.apache.commons.io.IOUtils; import org.springframework.content.commons.repository.ContentStore; import org.springframework.data.repository.CrudRepository; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; @@ -298,6 +297,27 @@ public static Content tests() { assertThat(fetched.get().getLen(), is(new Long(content.length()))); }); }); + Context("a PUT to /{store}/{id} with a multi-part request", () -> { + It("should overwrite the content and return 200", () -> { + + String content = "This is Modified Spring Content!"; + + mvc.perform(multipart(HttpMethod.PUT, url) + .file(new MockMultipartFile("file", + "tests-file-modified.txt", + "text/plain", content.getBytes())) + .contextPath(contextPath) + ) + .andExpect(status().isOk()); + + Optional fetched = repository.findById(entity.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getContentId(), is(not(nullValue()))); + assertThat(fetched.get().getOriginalFileName(), is("tests-file-modified.txt")); + assertThat(fetched.get().getMimeType(), is("text/plain")); + assertThat(fetched.get().getLen(), is(new Long(content.length()))); + }); + }); Context("a DELETE to /{store}/{id} with the mimetype", () -> { It("should delete the content, attributes and return a 200 response", () -> { mvc.perform(delete(url) From 0191af87c3e7fbefd3c9bd63399a35f2d0b1c98b Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 30 May 2023 20:41:14 -0700 Subject: [PATCH 20/28] feat: add unsetContent with UnsetContentParams to all storage modules allowing content to be kept --- .../azure/store/DefaultAzureStorageImpl.java | 28 +++++-- .../content/azure/it/AzureStorageIT.java | 27 +++++- .../commons/store/factory/StoreImpl.java | 73 +++++++++++++---- .../commons/repository/ContentStore.java | 3 + .../commons/repository/SetContentParams.java | 1 + .../repository/UnsetContentParams.java | 17 ++++ .../content/commons/store/ContentStore.java | 3 + .../commons/store/UnsetContentParams.java | 16 ++++ .../factory/AbstractStoreFactoryBeanTest.java | 12 +-- .../testsupport/TestStoreFactoryBean.java | 8 +- .../fragments/EncryptingContentStoreImpl.java | 23 +++++- .../content/encryption/s3/EncryptionIT.java | 2 - .../fs/store/DefaultFilesystemStoreImpl.java | 49 +++++++---- .../test/java/it/store/FilesystemStoreIT.java | 31 +++++-- .../gcs/store/DefaultGCPStorageImpl.java | 82 +++++++++++-------- .../content/gcs/it/GCPStorageIT.java | 27 ++++-- .../jpa/store/DefaultJpaStoreImpl.java | 20 ++++- .../content/jpa/ContentStoreIT.java | 29 +++++++ .../jpa/config/EnableJpaStoresTest.java | 8 +- .../mongo/store/DefaultMongoStoreImpl.java | 51 +++++++----- .../content/mongo/it/MongoStoreIT.java | 31 +++++-- .../rest/config/SetContentDisposition.java | 5 ++ .../content/s3/store/DefaultS3StoreImpl.java | 71 ++++++++++------ .../content/s3/it/S3StoreIT.java | 30 ++++++- 24 files changed, 497 insertions(+), 150 deletions(-) create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/repository/UnsetContentParams.java create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/store/UnsetContentParams.java create mode 100644 spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java diff --git a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java index fcfed0e32..a6ba12948 100644 --- a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java +++ b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java @@ -22,10 +22,7 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; -import org.springframework.content.commons.store.AssociativeStore; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.*; import org.springframework.content.commons.utils.BeanUtils; import org.springframework.content.commons.utils.Condition; import org.springframework.content.commons.utils.PlacementService; @@ -460,7 +457,22 @@ public boolean matches(Field field) { @Transactional @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, UnsetContentParams.builder().build()); + } + + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.repository.UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + UnsetContentParams params1 = UnsetContentParams.builder() + .disposition(UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); @@ -470,7 +482,7 @@ public S unsetContent(S entity, PropertyPath propertyPath) { return entity; Resource resource = this.getResource(entity, propertyPath); - if (resource != null && resource.exists() && resource instanceof DeletableResource) { + if (resource != null && resource.exists() && resource instanceof DeletableResource && params.getDisposition().equals(UnsetContentParams.Disposition.Remove)) { try { ((DeletableResource)resource).delete(); @@ -488,8 +500,8 @@ public boolean matches(TypeDescriptor descriptor) { if ("jakarta.persistence.Id".equals( annotation.annotationType().getCanonicalName()) || "org.springframework.data.annotation.Id" - .equals(annotation.annotationType() - .getCanonicalName())) { + .equals(annotation.annotationType() + .getCanonicalName())) { return false; } } @@ -502,7 +514,7 @@ public boolean matches(TypeDescriptor descriptor) { return entity; } - private String absolutify(String bucket, String location) { + private String absolutify(String bucket, String location) { String locationToUse = null; Assert.state(location.startsWith("azure-blob://") == false); if (location.startsWith("/")) { diff --git a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java index eaf9f3199..cb54302e4 100644 --- a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java +++ b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java @@ -36,6 +36,7 @@ import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.SetContentParams; +import org.springframework.content.commons.store.UnsetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -402,7 +403,7 @@ public class AzureStorageIT { }); }); - Context("when content is deleted", () -> { + Context("when content is unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -419,13 +420,37 @@ public class AzureStorageIT { assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + BlobContainerClient c = builder.buildClient().getBlobContainerClient("azure-test-bucket"); + assertThat(c.getBlobClient(resourceLocation).getBlockBlobClient().exists(), is(false)); + //rendition try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { assertThat(content, is(Matchers.nullValue())); } + assertThat(entity.getRenditionId(), is(Matchers.nullValue())); + Assert.assertEquals(entity.getRenditionLen(), 0); + }); + }); + + Context("when content is unset but kept", () -> { + BeforeEach(() -> { + resourceLocation = entity.getContentId().toString(); + entity = store.unsetContent(entity, PropertyPath.from("content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + entity = repo.save(entity); + }); + + It("should have no content", () -> { + //content + try (InputStream content = store.getContent(entity)) { + assertThat(content, is(Matchers.nullValue())); + } + assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + + BlobContainerClient c = builder.buildClient().getBlobContainerClient("azure-test-bucket"); + assertThat(c.getBlobClient(resourceLocation).getBlockBlobClient().exists(), is(true)); }); }); diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java index 8e130fe4a..0f63c3466 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java @@ -10,16 +10,14 @@ import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Supplier; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.SetContentParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.*; import org.springframework.content.commons.store.events.AfterAssociateEvent; import org.springframework.content.commons.store.events.AfterGetContentEvent; import org.springframework.content.commons.store.events.AfterGetResourceEvent; @@ -444,33 +442,78 @@ public Object unsetContent(Object property) { @Override public Object unsetContent(Object property, PropertyPath propertyPath) { + return this.unsetContent(property, propertyPath, UnsetContentParams.builder().build()); + } + + @Override + public Object unsetContent(Object entity, PropertyPath propertyPath, org.springframework.content.commons.repository.UnsetContentParams params) { + + return this.internalUnsetContent(entity, propertyPath, + () -> { + Object result; + try { + if (delegate instanceof org.springframework.content.commons.repository.ContentStore) { + return castToDeprecatedContentStore(delegate).unsetContent(entity, propertyPath, params); + } else { + int ordinal = params.getDisposition().ordinal(); + UnsetContentParams params1 = UnsetContentParams.builder() + .disposition(UnsetContentParams.Disposition.values()[ordinal]) + .build(); + + return castToContentStore(delegate).unsetContent(entity, propertyPath, params1); + } + } + catch (Exception e) { + throw e; + } + }); + } + + @Override + public Object unsetContent(Object entity, PropertyPath propertyPath, UnsetContentParams params) { + return this.internalUnsetContent(entity, propertyPath, + () -> { + Object result; + try { + if (delegate instanceof org.springframework.content.commons.repository.ContentStore) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.repository.UnsetContentParams params1 = org.springframework.content.commons.repository.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.repository.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return castToDeprecatedContentStore(delegate).unsetContent(entity, propertyPath, params1); + } else { + return castToContentStore(delegate).unsetContent(entity, propertyPath, params); + } + } + catch (Exception e) { + throw e; + } + }); + } + + public Object internalUnsetContent(Object entity, PropertyPath propertyPath, Supplier invocation) { - org.springframework.content.commons.repository.events.BeforeUnsetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeUnsetContentEvent(property, propertyPath, delegate); + org.springframework.content.commons.repository.events.BeforeUnsetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeUnsetContentEvent(entity, propertyPath, delegate); publisher.publishEvent(oldBefore); ContentStore contentStore = castToContentStore(delegate); if (contentStore != null) { - BeforeUnsetContentEvent before = new BeforeUnsetContentEvent(property, propertyPath, contentStore); + BeforeUnsetContentEvent before = new BeforeUnsetContentEvent(entity, propertyPath, contentStore); publisher.publishEvent(before); } - Object result; - try { - result = castToDeprecatedContentStore(delegate).unsetContent(property, propertyPath); - } - catch (Exception e) { - throw e; - } + Object result = invocation.get(); - org.springframework.content.commons.repository.events.AfterUnsetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterUnsetContentEvent(property, propertyPath, delegate); + org.springframework.content.commons.repository.events.AfterUnsetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterUnsetContentEvent(entity, propertyPath, delegate); oldAfter.setResult(result); publisher.publishEvent(oldAfter); if (contentStore != null) { - AfterUnsetContentEvent after = new AfterUnsetContentEvent(property, propertyPath, contentStore); + AfterUnsetContentEvent after = new AfterUnsetContentEvent(entity, propertyPath, contentStore); after.setResult(result); publisher.publishEvent(after); } + return result; } diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java index 897bafc9c..a54a1a9bd 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java @@ -36,6 +36,9 @@ public interface ContentStore extends AssociativeSt @LockParticipant S unsetContent(S entity, PropertyPath propertyPath); + @LockParticipant + S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params); + InputStream getContent(S entity); InputStream getContent(S entity, PropertyPath propertyPath); diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java index ffb623771..a267312ee 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java @@ -3,6 +3,7 @@ import lombok.Builder; import lombok.Data; +@Deprecated @Data @Builder public class SetContentParams { diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/UnsetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/UnsetContentParams.java new file mode 100644 index 000000000..1d6123d37 --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/UnsetContentParams.java @@ -0,0 +1,17 @@ +package org.springframework.content.commons.repository; + +import lombok.Builder; +import lombok.Data; + +@Deprecated +@Data +@Builder +public class UnsetContentParams { + + @Builder.Default + private Disposition disposition = Disposition.Remove; + + public enum Disposition { + Keep, Remove + } +} diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java index 5c3208e43..2059a97f2 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java @@ -34,6 +34,9 @@ public interface ContentStore extends AssociativeSt @LockParticipant S unsetContent(S entity, PropertyPath propertyPath); + @LockParticipant + S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params); + InputStream getContent(S entity); InputStream getContent(S entity, PropertyPath propertyPath); diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/UnsetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/UnsetContentParams.java new file mode 100644 index 000000000..1ff5df73e --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/UnsetContentParams.java @@ -0,0 +1,16 @@ +package org.springframework.content.commons.store; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UnsetContentParams { + @Builder.Default + + private Disposition disposition = Disposition.Remove; + + public enum Disposition { + Keep, Remove + } +} diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java index 26fc50609..c1f8f70f9 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/AbstractStoreFactoryBeanTest.java @@ -13,10 +13,7 @@ import org.junit.runner.RunWith; import org.springframework.content.commons.property.PropertyPath; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.SetContentParams; -import org.springframework.content.commons.store.Store; +import org.springframework.content.commons.store.*; import org.springframework.core.io.Resource; import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; @@ -161,7 +158,12 @@ public Object unsetContent(Object property, PropertyPath propertyPath) { return null; } - @Override + @Override + public Object unsetContent(Object entity, PropertyPath propertyPath, UnsetContentParams params) { + return null; + } + + @Override public InputStream getContent(Object property, PropertyPath propertyPath) { // TODO Auto-generated method stub return null; diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java index d5ec86231..5eab4ea84 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/repository/factory/testsupport/TestStoreFactoryBean.java @@ -7,6 +7,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.GetResourceParams; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.Store; import org.springframework.content.commons.store.factory.AbstractStoreFactoryBean; import org.springframework.core.io.Resource; @@ -116,7 +117,12 @@ public Object unsetContent(Object property, PropertyPath propertyPath) { return null; } - @Override + @Override + public Object unsetContent(Object entity, PropertyPath propertyPath, UnsetContentParams params) { + return null; + } + + @Override public InputStream getContent(Object property, PropertyPath propertyPath) { // TODO Auto-generated method stub return null; diff --git a/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java b/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java index 1bab5f22b..fea2e32ef 100644 --- a/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java +++ b/spring-content-encryption/src/main/java/internal/org/springframework/content/fragments/EncryptingContentStoreImpl.java @@ -166,6 +166,21 @@ public S unsetContent(S o) { @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, org.springframework.content.commons.store.UnsetContentParams.builder().build()); + } + + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } + + + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { Assert.notNull(entity, "entity not set"); Assert.notNull(propertyPath, "propertyPath not set"); AssertUtils.atLeastOneNotNull(new Object[]{storeDelegate, delegate}, "store not set"); @@ -177,9 +192,13 @@ public S unsetContent(S entity, PropertyPath propertyPath) { S entityToReturn = null; if (storeDelegate != null) { - entityToReturn = (S) storeDelegate.unsetContent(entity, propertyPath); + entityToReturn = (S) storeDelegate.unsetContent(entity, propertyPath, params); } else if (delegate != null) { - entityToReturn = (S) delegate.unsetContent(entity, propertyPath); + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.repository.UnsetContentParams params1 = org.springframework.content.commons.repository.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.repository.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + entityToReturn = (S) delegate.unsetContent(entity, propertyPath, params1); } contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, null); diff --git a/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java b/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java index bad2a4f24..f1ce84524 100644 --- a/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java +++ b/spring-content-encryption/src/test/java/org/springframework/content/encryption/s3/EncryptionIT.java @@ -208,8 +208,6 @@ public class EncryptionIT { f = repo.findById(f.getId()).get(); assertThat(f.getContentKey(), is(nullValue())); - //todo: refactor to check s3 bucket -// assertThat(new java.io.File(filesystemRoot, contentId).exists(), is(false)); HeadObjectRequest getObjectRequest = HeadObjectRequest.builder() .bucket("test-bucket") .key(contentId) diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 5bb111e65..5c7a79794 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -30,6 +30,8 @@ import org.springframework.content.commons.store.GetResourceParams; import org.springframework.content.commons.store.SetContentParams; import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.UnsetContentParams; +import org.springframework.content.commons.store.UnsetContentParams.Disposition; import org.springframework.content.commons.utils.BeanUtils; import org.springframework.content.commons.utils.Condition; import org.springframework.content.commons.utils.FileService; @@ -404,28 +406,41 @@ public S unsetContent(S entity) { @Transactional @Override - public S unsetContent(S property, PropertyPath propertyPath) { + public S unsetContent(S entity, PropertyPath propertyPath) { + return unsetContent(entity, propertyPath, UnsetContentParams.builder().disposition(Disposition.Remove).build()); + } - if (property == null) - return property; + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.repository.UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + return unsetContent(entity, propertyPath, UnsetContentParams.builder().disposition(Disposition.values()[ordinal]).build()); + } - Resource resource = getResource(property, propertyPath); + @Transactional + @Override + public S unsetContent(S property, PropertyPath propertyPath, UnsetContentParams params) { - if (resource != null && resource.exists() && resource instanceof DeletableResource) { - try { - ((DeletableResource) resource).delete(); - } catch (IOException e) { - logger.warn(format("Unable to get file for resource %s", resource)); - } - } + if (property == null) + return property; - // reset content fields - unassociate(property, propertyPath); - ContentProperty contentProperty = this.mappingContext.getContentProperty(property.getClass(), propertyPath.getName()); - contentProperty.setContentLength(property, 0); + Resource resource = getResource(property, propertyPath); - return property; - } + if (resource != null && resource.exists() && resource instanceof DeletableResource && params.getDisposition().equals(Disposition.Remove)) { + try { + ((DeletableResource) resource).delete(); + } catch (IOException e) { + logger.warn(format("Unable to get file for resource %s", resource)); + } + } + + // reset content fields + unassociate(property, propertyPath); + ContentProperty contentProperty = this.mappingContext.getContentProperty(property.getClass(), propertyPath.getName()); + contentProperty.setContentLength(property, 0); + + return property; + } private Object convertToExternalContentIdType(S property, Object contentId) { if (placer.canConvert(TypeDescriptor.forObject(contentId), diff --git a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java index c17e2289b..60301ffa9 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -29,10 +29,7 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.io.DeletableResource; import org.springframework.content.commons.property.PropertyPath; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.SetContentParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.*; import org.springframework.content.fs.config.EnableFilesystemStores; import org.springframework.content.fs.io.FileSystemResourceLoader; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -385,7 +382,7 @@ public class FilesystemStoreIT { }); }); - Context("when content is deleted", () -> { + Context("when content is unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -409,6 +406,30 @@ public class FilesystemStoreIT { assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + + FileSystemResourceLoader loader = context.getBean(FileSystemResourceLoader.class); + assertThat(new File(loader.getFilesystemRoot(), resourceLocation).exists(), is(false)); + }); + }); + + Context("when content is unset but kept", () -> { + BeforeEach(() -> { + resourceLocation = entity.getContentId().toString(); + entity = store.unsetContent(entity, PropertyPath.from("content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + entity = repo.save(entity); + }); + + It("should have no content", () -> { + //content + try (InputStream content = store.getContent(entity)) { + assertThat(content, is(Matchers.nullValue())); + } + + assertThat(entity.getContentId(), is(Matchers.nullValue())); + Assert.assertEquals(entity.getContentLen(), 0); + + FileSystemResourceLoader loader = context.getBean(FileSystemResourceLoader.class); + assertThat(new File(loader.getFilesystemRoot(), resourceLocation).exists(), is(true)); }); }); diff --git a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java index 1e308a520..d951f22c7 100644 --- a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java +++ b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java @@ -21,6 +21,7 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; @@ -456,46 +457,59 @@ public boolean matches(Field field) { @Transactional @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, org.springframework.content.commons.store.UnsetContentParams.builder().build()); + } - ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); - if (property == null) { - throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); - } + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } - if (entity == null) - return entity; + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { + ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); + if (property == null) { + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); + } - Resource resource = this.getResource(entity, propertyPath); - if (resource != null && resource.exists() && resource instanceof DeletableResource) { + if (entity == null) + return entity; - try { - ((DeletableResource)resource).delete(); - } catch (Exception e) { - logger.error(format("Unexpected error unsetting content for entity %s", entity)); - throw new StoreAccessException(format("Unsetting content for entity %s", entity), e); - } - } + Resource resource = this.getResource(entity, propertyPath); + if (resource != null && resource.exists() && resource instanceof DeletableResource && params.getDisposition().equals(org.springframework.content.commons.store.UnsetContentParams.Disposition.Remove)) { - // reset content fields - property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { - @Override - public boolean matches(TypeDescriptor descriptor) { - for (Annotation annotation : descriptor.getAnnotations()) { - if ("jakarta.persistence.Id".equals( - annotation.annotationType().getCanonicalName()) - || "org.springframework.data.annotation.Id" - .equals(annotation.annotationType() - .getCanonicalName())) { - return false; - } - } - return true; - } - }); - property.setContentLength(entity, 0); + try { + ((DeletableResource)resource).delete(); + } catch (Exception e) { + logger.error(format("Unexpected error unsetting content for entity %s", entity)); + throw new StoreAccessException(format("Unsetting content for entity %s", entity), e); + } + } - return entity; - } + // reset content fields + property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { + @Override + public boolean matches(TypeDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { + if ("jakarta.persistence.Id".equals( + annotation.annotationType().getCanonicalName()) + || "org.springframework.data.annotation.Id" + .equals(annotation.annotationType() + .getCanonicalName())) { + return false; + } + } + return true; + } + }); + property.setContentLength(entity, 0); + + return entity; + } private String absolutify(String bucket, String location) { String locationToUse = null; diff --git a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java index 079c2c308..44ee54a6f 100644 --- a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java +++ b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java @@ -30,10 +30,7 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.io.DeletableResource; import org.springframework.content.commons.property.PropertyPath; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.SetContentParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.*; import org.springframework.content.gcs.config.EnableGCPStorage; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -393,7 +390,7 @@ public class GCPStorageIT { }); }); - Context("when content is deleted", () -> { + Context("when content is unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -409,14 +406,34 @@ public class GCPStorageIT { assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + assertThat(storage.get(BlobId.of("test-bucket", resourceLocation)), is(nullValue())); //rendition try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { assertThat(content, is(Matchers.nullValue())); } + assertThat(entity.getRenditionId(), is(Matchers.nullValue())); + Assert.assertEquals(entity.getRenditionLen(), 0); + }); + }); + + Context("when content is unset but kept", () -> { + BeforeEach(() -> { + resourceLocation = entity.getContentId().toString(); + entity = store.unsetContent(entity, PropertyPath.from("content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + entity = repo.save(entity); + }); + + It("should have no content", () -> { + //content + try (InputStream content = store.getContent(entity)) { + assertThat(content, is(Matchers.nullValue())); + } + assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + assertThat(storage.get(BlobId.of("test-bucket", resourceLocation)).exists(), is(true)); }); }); diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index 34159a9a3..c33ec5711 100644 --- a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java +++ b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java @@ -22,6 +22,7 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; @@ -351,7 +352,22 @@ public S unsetContent(S metadata) { @Transactional @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, org.springframework.content.commons.store.UnsetContentParams.builder().build()); + } + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } + + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { // TODO @@ -361,7 +377,7 @@ public S unsetContent(S entity, PropertyPath propertyPath) { id = -1L; } Resource resource = loader.getResource(id.toString()); - if (resource instanceof DeletableResource) { + if (resource instanceof DeletableResource && params.getDisposition().equals(UnsetContentParams.Disposition.Remove)) { try { ((DeletableResource) resource).delete(); } catch (Exception e) { @@ -375,7 +391,7 @@ public S unsetContent(S entity, PropertyPath propertyPath) { return entity; } - protected Object convertToExternalContentIdType(S property, Object contentId) { + protected Object convertToExternalContentIdType(S property, Object contentId) { ConversionService converter = new DefaultConversionService(); if (converter.canConvert(TypeDescriptor.forObject(contentId), TypeDescriptor.valueOf(BeanUtils.getFieldWithAnnotationType(property, diff --git a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java index e60eea243..98cea8d84 100644 --- a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java +++ b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java @@ -20,6 +20,7 @@ import org.junit.runner.RunWith; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.store.SetContentParams; +import org.springframework.content.commons.store.UnsetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; @@ -256,6 +257,34 @@ public class ContentStoreIT { }); }); + Context("when content is deleted", () -> { + BeforeEach(() -> { + id = claim.getClaimForm().getContentId(); + claimFormStore.unsetContent(claim, PropertyPath.from("claimForm/content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + claim = claimRepo.save(claim); + }); + + AfterEach(() -> { + claimRepo.delete(claim); + }); + + It("should have no content", () -> { + ClaimForm deletedClaimForm = new ClaimForm(); + deletedClaimForm.setContentId((String)id); + + // content + doInTransaction(ptm, () -> { + try (InputStream content = claimFormStore.getContent(claim, PropertyPath.from("claimForm/content"))) { + Assert.assertThat(content, is(nullValue())); + } catch (IOException e) { + } + return null; + }); + + Assert.assertThat(claim.getClaimForm().getContentId(), is(nullValue())); + Assert.assertEquals(claim.getClaimForm().getContentLength(), 0); + }); + }); }); }); } diff --git a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java index 1b2cf05a4..fd64be72d 100644 --- a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java +++ b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/config/EnableJpaStoresTest.java @@ -26,6 +26,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.GetResourceParams; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.jpa.config.EnableJpaContentRepositories; import org.springframework.content.jpa.config.EnableJpaStores; import org.springframework.content.jpa.io.BlobResourceLoader; @@ -290,7 +291,12 @@ public TestEntity unsetContent(TestEntity property, PropertyPath propertyPath) { return null; } - @Override + @Override + public TestEntity unsetContent(TestEntity entity, PropertyPath propertyPath, UnsetContentParams params) { + return null; + } + + @Override public InputStream getContent(TestEntity property, PropertyPath propertyPath) { // TODO Auto-generated method stub return null; diff --git a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java index f7affe3e1..42cf1e34b 100644 --- a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java +++ b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java @@ -20,6 +20,7 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; @@ -426,7 +427,20 @@ public boolean matches(Field field) { @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, org.springframework.content.commons.store.UnsetContentParams.builder().build()); + } + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } + + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); @@ -442,28 +456,27 @@ public S unsetContent(S entity, PropertyPath propertyPath) { try { String location = placer.convert(contentId, String.class); Resource resource = gridFs.getResource(location); - if (resource != null && resource.exists()) { + if (resource != null && resource.exists() && params.getDisposition().equals(org.springframework.content.commons.store.UnsetContentParams.Disposition.Remove)) { gridFs.delete(query(whereFilename().is(resource.getFilename()))); + } - // reset content fields - property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { - @Override - public boolean matches(TypeDescriptor descriptor) { - for (Annotation annotation : descriptor.getAnnotations()) { - if ("jakarta.persistence.Id".equals( - annotation.annotationType().getCanonicalName()) - || "org.springframework.data.annotation.Id" - .equals(annotation.annotationType() - .getCanonicalName())) { - return false; - } + // reset content fields + property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { + @Override + public boolean matches(TypeDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { + if ("jakarta.persistence.Id".equals( + annotation.annotationType().getCanonicalName()) + || "org.springframework.data.annotation.Id" + .equals(annotation.annotationType() + .getCanonicalName())) { + return false; } - return true; } - }); - - property.setContentLength(entity, 0); - } + return true; + } + }); + property.setContentLength(entity, 0); } catch (Exception ase) { logger.error(format("Unexpected error unsetting content for entity %s", entity), ase); @@ -473,7 +486,7 @@ public boolean matches(TypeDescriptor descriptor) { return entity; } - protected Object convertToExternalContentIdType(S property, Object contentId) { + protected Object convertToExternalContentIdType(S property, Object contentId) { if (placer.canConvert(TypeDescriptor.forObject(contentId), TypeDescriptor.valueOf(BeanUtils.getFieldWithAnnotationType(property, ContentId.class)))) { diff --git a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java index a2a5c33c3..c1cc3f45d 100644 --- a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java +++ b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java @@ -21,10 +21,7 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.io.DeletableResource; import org.springframework.content.commons.property.PropertyPath; -import org.springframework.content.commons.store.ContentStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.SetContentParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.store.*; import org.springframework.content.commons.utils.PlacementService; import org.springframework.content.mongo.config.EnableMongoStores; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -378,7 +375,7 @@ public class MongoStoreIT { }); }); - Context("when content is deleted", () -> { + Context("when content is unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -395,7 +392,9 @@ public class MongoStoreIT { assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); - //rendition + assertThat(gridFsTemplate.getResource(resourceLocation).exists(), is(false)); + + //rendition try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { assertThat(content, is(Matchers.nullValue())); } @@ -405,6 +404,26 @@ public class MongoStoreIT { }); }); + Context("when content is unset but kept", () -> { + BeforeEach(() -> { + resourceLocation = entity.getContentId().toString(); + entity = store.unsetContent(entity, PropertyPath.from("content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + entity = repo.save(entity); + }); + + It("should have no content", () -> { + //content + try (InputStream content = store.getContent(entity)) { + assertThat(content, is(Matchers.nullValue())); + } + + assertThat(entity.getContentId(), is(Matchers.nullValue())); + Assert.assertEquals(entity.getContentLen(), 0); + + assertThat(gridFsTemplate.getResource(resourceLocation).exists(), is(true)); + }); + }); + Context("when an invalid property path is used to setContent", () -> { It("should throw an error", () -> { try { diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java new file mode 100644 index 000000000..5378b7f7d --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java @@ -0,0 +1,5 @@ +package org.springframework.content.rest.config; + +public enum SetContentDisposition { + Create, Overwrite +} diff --git a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java index 75bdf3073..f5907d999 100644 --- a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java +++ b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java @@ -23,6 +23,7 @@ import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.AssociativeStore; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.GetResourceParams; @@ -482,37 +483,55 @@ public boolean matches(Field field) { @Transactional @Override public S unsetContent(S entity, PropertyPath propertyPath) { + return this.unsetContent(entity, propertyPath, org.springframework.content.commons.store.UnsetContentParams.builder().build()); + } - ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); - if (property == null) { - throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); - } - if (entity == null) - return entity; + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { + int ordinal = params.getDisposition().ordinal(); + org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + .build(); + return this.unsetContent(entity, propertyPath, params1); + } - deleteIfExists(entity, propertyPath); + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { + ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); + if (property == null) { + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); + } - // reset content fields - property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { - @Override - public boolean matches(TypeDescriptor descriptor) { - for (Annotation annotation : descriptor.getAnnotations()) { - if ("jakarta.persistence.Id".equals( - annotation.annotationType().getCanonicalName()) - || "org.springframework.data.annotation.Id" - .equals(annotation.annotationType() - .getCanonicalName())) { - return false; - } - } - return true; - } - }); - property.setContentLength(entity, 0); + if (entity == null) + return entity; - return entity; - } + if (params.getDisposition().equals(org.springframework.content.commons.store.UnsetContentParams.Disposition.Remove)) { + deleteIfExists(entity, propertyPath); + } + + // reset content fields + property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { + @Override + public boolean matches(TypeDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { + if ("jakarta.persistence.Id".equals( + annotation.annotationType().getCanonicalName()) + || "org.springframework.data.annotation.Id" + .equals(annotation.annotationType() + .getCanonicalName())) { + return false; + } + } + return true; + } + }); + property.setContentLength(entity, 0); + + return entity; + } private String absolutify(String bucket, String location) { String locationToUse = null; diff --git a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java index 98fa1f63b..de4004f3a 100644 --- a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java +++ b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java @@ -26,6 +26,7 @@ import org.springframework.content.commons.repository.StoreAccessException; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.commons.store.SetContentParams; +import org.springframework.content.commons.store.UnsetContentParams; import org.springframework.content.s3.config.EnableS3Stores; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -58,6 +59,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.fail; @RunWith(Ginkgo4jRunner.class) @Ginkgo4jConfiguration(threads=1) @@ -427,7 +429,7 @@ public class S3StoreIT { }); }); - Context("when content is deleted", () -> { + Context("when content is unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -444,13 +446,39 @@ public class S3StoreIT { assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + try { + client.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(resourceLocation).build()); + fail("expected content to be removed but is still exists"); + } catch (NoSuchKeyException nske) { + } + //rendition try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { assertThat(content, is(Matchers.nullValue())); } + assertThat(entity.getRenditionId(), is(Matchers.nullValue())); + Assert.assertEquals(entity.getRenditionLen(), 0); + }); + }); + + Context("when content is unset but kept", () -> { + BeforeEach(() -> { + resourceLocation = entity.getContentId().toString(); + entity = store.unsetContent(entity, PropertyPath.from("content"), UnsetContentParams.builder().disposition(UnsetContentParams.Disposition.Keep).build()); + entity = repo.save(entity); + }); + + It("should have no content", () -> { + //content + try (InputStream content = store.getContent(entity)) { + assertThat(content, is(Matchers.nullValue())); + } + assertThat(entity.getContentId(), is(Matchers.nullValue())); Assert.assertEquals(entity.getContentLen(), 0); + + client.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(resourceLocation).build()); }); }); From 764ec2d8c93af25ac5f5ca6ffbfd9ad2d4ed36a4 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 31 May 2023 21:03:34 -0700 Subject: [PATCH 21/28] feat: refactor SetContentParams.overwriteExistingContent as ContentDisposition --- .../content/azure/store/DefaultAzureStorageImpl.java | 2 +- .../springframework/content/azure/it/AzureStorageIT.java | 2 +- .../content/commons/store/SetContentParams.java | 7 +++++++ .../content/fs/store/DefaultFilesystemStoreImpl.java | 2 +- .../src/test/java/it/store/FilesystemStoreIT.java | 2 +- .../content/gcs/store/DefaultGCPStorageImpl.java | 2 +- .../org/springframework/content/gcs/it/GCPStorageIT.java | 2 +- .../content/jpa/store/DefaultJpaStoreImpl.java | 2 +- .../org/springframework/content/jpa/ContentStoreIT.java | 2 +- .../content/mongo/store/DefaultMongoStoreImpl.java | 2 +- .../org/springframework/content/mongo/it/MongoStoreIT.java | 2 +- .../content/s3/store/DefaultS3StoreImpl.java | 2 +- .../org/springframework/content/s3/it/S3StoreIT.java | 2 +- 13 files changed, 19 insertions(+), 12 deletions(-) diff --git a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java index a6ba12948..34b0924a7 100644 --- a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java +++ b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java @@ -308,7 +308,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, or } Object contentId = property.getContentId(entity); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java index cb54302e4..18dedd1cb 100644 --- a/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java +++ b/spring-content-azure-storage/src/test/java/internal/org/springframework/content/azure/it/AzureStorageIT.java @@ -386,7 +386,7 @@ public class AzureStorageIT { String contentId = entity.getContentId(); assertThat(c.getBlobClient(contentId).getBlockBlobClient().exists(), is(true)); - store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); entity = repo.save(entity); boolean matches = false; diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java index 7cafcd880..521767b66 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java @@ -9,4 +9,11 @@ public class SetContentParams { private long contentLength = -1; @Builder.Default private boolean overwriteExistingContent = true; + + @Builder.Default + private ContentDisposition disposition = ContentDisposition.Overwrite; + + public enum ContentDisposition { + Overwrite, CreateNew + } } diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java index 5c7a79794..93ab38848 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/store/DefaultFilesystemStoreImpl.java @@ -260,7 +260,7 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, } Object contentId = contentProperty.getContentId(property); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java index 60301ffa9..fc7c0530f 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -365,7 +365,7 @@ public class FilesystemStoreIT { String contentId = entity.getContentId(); assertThat(new File(loader.getFilesystemRoot(), contentId).exists(), is(true)); - store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); entity = repo.save(entity); boolean matches = false; diff --git a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java index d951f22c7..14acd71ac 100644 --- a/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java +++ b/spring-content-gcs/src/main/java/internal/org/springframework/content/gcs/store/DefaultGCPStorageImpl.java @@ -316,7 +316,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, or } Object contentId = property.getContentId(entity); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java index 44ee54a6f..de6aae140 100644 --- a/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java +++ b/spring-content-gcs/src/test/java/internal/org/springframework/content/gcs/it/GCPStorageIT.java @@ -375,7 +375,7 @@ public class GCPStorageIT { assertThat(contentId, is(not(nullValue()))); assertThat(storage.get(BlobId.of("test-bucket", contentId)).exists(), is(true)); - store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); entity = repo.save(entity); boolean matches = false; diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index c33ec5711..638429383 100644 --- a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java +++ b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java @@ -263,7 +263,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, or // TODO: property == null? SID contentId = getContentId(entity, propertyPath); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java index 98cea8d84..6986ff150 100644 --- a/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java +++ b/spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java @@ -202,7 +202,7 @@ public class ContentStoreIT { It("should have the updated content", () -> { String contentId = claim.getClaimForm().getContentId(); - claimFormStore.setContent(claim, PropertyPath.from("claimForm/content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + claimFormStore.setContent(claim, PropertyPath.from("claimForm/content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); claim = claimRepo.save(claim); boolean matches = false; diff --git a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java index 42cf1e34b..ba6150659 100644 --- a/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java +++ b/spring-content-mongo/src/main/java/internal/org/springframework/content/mongo/store/DefaultMongoStoreImpl.java @@ -266,7 +266,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, or } Object contentId = property.getContentId(entity); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java index c1cc3f45d..8ec55e999 100644 --- a/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java +++ b/spring-content-mongo/src/test/java/internal/org/springframework/content/mongo/it/MongoStoreIT.java @@ -358,7 +358,7 @@ public class MongoStoreIT { String contentId = entity.getContentId(); assertThat(gridFsTemplate.getResource(contentId).exists(), is(true)); - store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); entity = repo.save(entity); boolean matches = false; diff --git a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java index f5907d999..02f57534e 100644 --- a/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java +++ b/spring-content-s3/src/main/java/internal/org/springframework/content/s3/store/DefaultS3StoreImpl.java @@ -345,7 +345,7 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, or } Object contentId = property.getContentId(entity); - if (contentId == null || !params.isOverwriteExistingContent()) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); diff --git a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java index de4004f3a..ed7bf1257 100644 --- a/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java +++ b/spring-content-s3/src/test/java/internal/org/springframework/content/s3/it/S3StoreIT.java @@ -415,7 +415,7 @@ public class S3StoreIT { String contentId = entity.getContentId(); client.headObject(HeadObjectRequest.builder().bucket(BUCKET).key(contentId).build()); - store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().overwriteExistingContent(false).build()); + store.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), SetContentParams.builder().disposition(SetContentParams.ContentDisposition.CreateNew).build()); entity = repo.save(entity); boolean matches = false; From 8bacb29df4866f3ce1a846d85000a54ec4e38cb6 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 5 Jun 2023 20:56:36 -0700 Subject: [PATCH 22/28] feat: refactor boot and rest layer overwrite existing content property to use SetContentDisposition enum - add and wire through an UnsetContentDisposition enum --- .../ContentRestAutoConfiguration.java | 22 +++++++++- .../SpringBootContentRestConfigurer.java | 3 ++ .../commons/repository/SetContentParams.java | 6 +++ .../commons/store/SetContentParams.java | 1 - .../ContentStoreContentService.java | 40 +++++++++++++++---- .../rest/controllers/StoreRestController.java | 3 +- .../rest/config/RestConfiguration.java | 21 +++++++++- .../rest/config/SetContentDisposition.java | 2 +- .../content/rest/config/SetContentParams.java | 16 ++++++++ .../rest/config/UnsetContentDisposition.java | 5 +++ 10 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentParams.java create mode 100644 spring-content-rest/src/main/java/org/springframework/content/rest/config/UnsetContentDisposition.java diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java index 8894b5b0e..7f558bcf5 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java @@ -7,8 +7,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.content.rest.config.SetContentDisposition; +import org.springframework.content.rest.config.UnsetContentDisposition; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.stereotype.Component; @@ -27,6 +28,9 @@ public static class ContentRestProperties { private ShortcutRequestMappings requestMappings = new ShortcutRequestMappings(); private boolean overwriteExistingContent = RestConfiguration.OVERWRITE_EXISTING_CONTENT_DEFAULT; + private SetContentDisposition setContentDisposition = RestConfiguration.SETCONTENT_CONTENT_DISPOSITION_DEFAULT; + private UnsetContentDisposition unsetContentDisposition = RestConfiguration.UNSETCONTENT_CONTENT_DISPOSITION_DEFAULT; + public URI getBaseUri() { return baseUri; } @@ -59,6 +63,22 @@ public void setOverwriteExistingContent(boolean overwriteExistingContent) { this.overwriteExistingContent = overwriteExistingContent; } + public SetContentDisposition getSetContentDisposition() { + return this.setContentDisposition; + } + + public void setSetContentDisposition(SetContentDisposition setContentDisposition) { + this.setContentDisposition = setContentDisposition; + } + + public UnsetContentDisposition getUnsetContentDisposition() { + return this.unsetContentDisposition; + } + + public void setUnsetContentDisposition(UnsetContentDisposition unsetContentDisposition) { + this.unsetContentDisposition = unsetContentDisposition; + } + public static class ShortcutRequestMappings { private boolean disabled = false; diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java index 801d1ea1a..f0e1dc7de 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java @@ -59,5 +59,8 @@ public void configure(RestConfiguration config) { } config.setOverwriteExistingContent(properties.getOverwriteExistingContent()); + + config.setSetContentDisposition(properties.getSetContentDisposition()); + config.setUnsetContentDisposition(properties.getUnsetContentDisposition()); } } diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java index a267312ee..ef469737e 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java @@ -10,4 +10,10 @@ public class SetContentParams { private long contentLength = -1; @Builder.Default private boolean overwriteExistingContent = true; + @Builder.Default + private ContentDisposition disposition = ContentDisposition.Overwrite; + + public enum ContentDisposition { + Overwrite, CreateNew + } } diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java index 521767b66..1c26c0e64 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java @@ -9,7 +9,6 @@ public class SetContentParams { private long contentLength = -1; @Builder.Default private boolean overwriteExistingContent = true; - @Builder.Default private ContentDisposition disposition = ContentDisposition.Overwrite; diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index 04a6f03c2..3e899b158 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -24,6 +24,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.repository.Store; +import org.springframework.content.commons.store.UnsetContentParams; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.StoreInterfaceUtils; import org.springframework.content.rest.RestResource; @@ -195,7 +196,8 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, params.setContentLength(headers.getContentLength()); } - params.setOverwriteExistingContent(config.overwriteExistingContent()); + int ordinal = config.getSetContentDisposition().ordinal(); + params.setDisposition(org.springframework.content.commons.store.SetContentParams.ContentDisposition.values()[ordinal]); argsList.add(params); } else if (methodToUse.getParameters().length > 3 && methodToUse.getParameters()[3].getType().equals(SetContentParams.class)) { @@ -206,7 +208,8 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, params.setContentLength(headers.getContentLength()); } - params.setOverwriteExistingContent(config.overwriteExistingContent()); + int ordinal = config.getSetContentDisposition().ordinal(); + params.setDisposition(SetContentParams.ContentDisposition.values()[ordinal]); argsList.add(params); } @@ -242,9 +245,26 @@ public void unsetContent(Resource resource) throws MethodNotAllowedException { Object targetObj = storeResource.getStoreInfo().getImplementation(ContentStore.class); + Object unsetParams = null; + if (methodsToUse[0].getParameters().length == 2 && methodsToUse[0].getParameters()[2].getType().equals(org.springframework.content.commons.store.UnsetContentParams.class)) { + org.springframework.content.commons.store.UnsetContentParams params = org.springframework.content.commons.store.UnsetContentParams.builder().build(); + + int ordinal = config.getUnsetContentDisposition().ordinal(); + params.setDisposition(UnsetContentParams.Disposition.values()[ordinal]); + + unsetParams = params; + } else if (methodsToUse[0].getParameters().length == 2 && methodsToUse[0].getParameters()[2].getType().equals(UnsetContentParams.class)) { + UnsetContentParams params = UnsetContentParams.builder().build(); + + int ordinal = config.getUnsetContentDisposition().ordinal(); + params.setDisposition(UnsetContentParams.Disposition.values()[ordinal]); + + unsetParams = params; + } + ReflectionUtils.makeAccessible(methodsToUse[0]); - Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, updateObject, storeResource.getPropertyPath()); + Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, updateObject, storeResource.getPropertyPath(), unsetParams); updateObject = updatedDomainObj; property.setMimeType(updateObject, null); @@ -363,7 +383,8 @@ public static class StoreExportedMethodsMap { private static Method[] SETCONTENT_METHODS_3x = null; private static Method[] SETCONTENT_METHODS_2x = null; - private static Method[] UNSETCONTENT_METHODS = null; + private static Method[] UNSETCONTENT_METHODS_3x = null; + private static Method[] UNSETCONTENT_METHODS_2x = null; private static Method[] GETCONTENT_METHODS = null; static { @@ -377,8 +398,12 @@ public static class StoreExportedMethodsMap { ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, PropertyPath.class, Resource.class), }; - UNSETCONTENT_METHODS = new Method[] { - ReflectionUtils.findMethod(ContentStore.class, "unsetContent", Object.class, PropertyPath.class), + UNSETCONTENT_METHODS_3x = new Method[] { + ReflectionUtils.findMethod(ContentStore.class, "unsetContent", Object.class, PropertyPath.class, org.springframework.content.commons.store.UnsetContentParams.class), + }; + + UNSETCONTENT_METHODS_2x = new Method[] { + ReflectionUtils.findMethod(ContentStore.class, "unsetContent", Object.class, PropertyPath.class, UnsetContentParams.class), }; GETCONTENT_METHODS = new Method[] { @@ -399,10 +424,11 @@ public StoreExportedMethodsMap(Class storeInterface, PropertyPa this.getContentMethods = calculateExports(GETCONTENT_METHODS, path, exportContext); if (org.springframework.content.commons.store.ContentStore.class.isAssignableFrom(storeInterface)) { this.setContentMethods = calculateExports(SETCONTENT_METHODS_3x, path, exportContext); + this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS_3x, path, exportContext); } else { this.setContentMethods = calculateExports(SETCONTENT_METHODS_2x, path, exportContext); + this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS_2x, path, exportContext); } - this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS, path, exportContext); } public Method[] getContentMethods() { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java index 6d1d0cb9d..54c0a7a77 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java @@ -179,8 +179,7 @@ public void postContent(HttpServletRequest request, HttpServletResponse response } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.DELETE, headers = "accept!=application/hal+json") - public void deleteContent(@RequestHeader HttpHeaders headers, HttpServletResponse response, - Resource resource) + public void deleteContent(@RequestHeader HttpHeaders headers, HttpServletResponse response, Resource resource) throws IOException, MethodNotAllowedException { StoreResource storeResource = (StoreResource)resource; diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java index acfbf3df9..0440d6c8f 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java @@ -32,7 +32,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -50,6 +49,8 @@ public class RestConfiguration implements InitializingBean { public static boolean OVERWRITE_EXISTING_CONTENT_DEFAULT = true; + public static SetContentDisposition SETCONTENT_CONTENT_DISPOSITION_DEFAULT = SetContentDisposition.Overwrite; + public static UnsetContentDisposition UNSETCONTENT_CONTENT_DISPOSITION_DEFAULT = UnsetContentDisposition.Remove; public static boolean FULLY_QUALIFIED_DEFAULTS_DEFAULT = true; public static boolean SHORTCUT_LINKS_DEFAULT = true; @@ -66,6 +67,8 @@ public class RestConfiguration implements InitializingBean { private boolean fullyQualifiedLinks = FULLY_QUALIFIED_DEFAULTS_DEFAULT; private boolean shortcutLinks = SHORTCUT_LINKS_DEFAULT; private boolean overwriteExistingContent = OVERWRITE_EXISTING_CONTENT_DEFAULT; + private SetContentDisposition setContentDisposition = SETCONTENT_CONTENT_DISPOSITION_DEFAULT; + private UnsetContentDisposition unsetContentDisposition = UNSETCONTENT_CONTENT_DISPOSITION_DEFAULT; private ConverterRegistry converters = new DefaultConversionService(); private Map, DomainTypeConfig> domainTypeConfigMap = new HashMap<>(); @@ -110,6 +113,22 @@ public void setOverwriteExistingContent(boolean overwriteExistingContent) { this.overwriteExistingContent = overwriteExistingContent; } + public SetContentDisposition getSetContentDisposition() { + return setContentDisposition; + } + + public void setSetContentDisposition(SetContentDisposition setContentDisposition) { + this.setContentDisposition = setContentDisposition; + } + + public UnsetContentDisposition getUnsetContentDisposition() { + return unsetContentDisposition; + } + + public void setUnsetContentDisposition(UnsetContentDisposition unsetContentDisposition) { + this.unsetContentDisposition = unsetContentDisposition; + } + public StoreCorsRegistry getCorsRegistry() { return corsRegistry; } diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java index 5378b7f7d..075558db4 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java @@ -1,5 +1,5 @@ package org.springframework.content.rest.config; public enum SetContentDisposition { - Create, Overwrite + Overwrite, CreateNew } diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentParams.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentParams.java new file mode 100644 index 000000000..747c908e2 --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentParams.java @@ -0,0 +1,16 @@ +package org.springframework.content.rest.config; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SetContentParams { + + @Builder.Default + private ContentDisposition disposition = ContentDisposition.Overwrite; + + private enum ContentDisposition { + Overwrite, CreateNew + } +} diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/UnsetContentDisposition.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/UnsetContentDisposition.java new file mode 100644 index 000000000..b3efda3ee --- /dev/null +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/UnsetContentDisposition.java @@ -0,0 +1,5 @@ +package org.springframework.content.rest.config; + +public enum UnsetContentDisposition { + Keep, Remove +} From cf9b0d2c49fea95eed603fe0caf315c83de18637 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 12 Jun 2023 20:51:54 -0700 Subject: [PATCH 23/28] test: update tests broken by introduction of UnsetContentParams - Unfocus accidentally focussed tests --- .../rest/contentservice/ContentStoreContentService.java | 8 ++++---- .../rest/it/http_405/MethodNotAllowedExceptionIT.java | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index 3e899b158..02874f0cd 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -24,7 +24,7 @@ import org.springframework.content.commons.repository.ContentStore; import org.springframework.content.commons.repository.SetContentParams; import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.store.UnsetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.StoreInterfaceUtils; import org.springframework.content.rest.RestResource; @@ -246,14 +246,14 @@ public void unsetContent(Resource resource) throws MethodNotAllowedException { Object targetObj = storeResource.getStoreInfo().getImplementation(ContentStore.class); Object unsetParams = null; - if (methodsToUse[0].getParameters().length == 2 && methodsToUse[0].getParameters()[2].getType().equals(org.springframework.content.commons.store.UnsetContentParams.class)) { + if (methodsToUse[0].getParameters().length == 3 && methodsToUse[0].getParameters()[2].getType().equals(org.springframework.content.commons.store.UnsetContentParams.class)) { org.springframework.content.commons.store.UnsetContentParams params = org.springframework.content.commons.store.UnsetContentParams.builder().build(); int ordinal = config.getUnsetContentDisposition().ordinal(); - params.setDisposition(UnsetContentParams.Disposition.values()[ordinal]); + params.setDisposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]); unsetParams = params; - } else if (methodsToUse[0].getParameters().length == 2 && methodsToUse[0].getParameters()[2].getType().equals(UnsetContentParams.class)) { + } else if (methodsToUse[0].getParameters().length == 3 && methodsToUse[0].getParameters()[2].getType().equals(UnsetContentParams.class)) { UnsetContentParams params = UnsetContentParams.builder().build(); int ordinal = config.getUnsetContentDisposition().ordinal(); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java index d18c47f29..1e922431f 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/it/http_405/MethodNotAllowedExceptionIT.java @@ -26,6 +26,7 @@ import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.content.commons.store.ContentStore; import org.springframework.content.fs.config.EnableFilesystemStores; import org.springframework.content.fs.io.FileSystemResourceLoader; @@ -107,7 +108,7 @@ public class MethodNotAllowedExceptionIT { RestAssuredMockMvc.webAppContextSetup(webApplicationContext); }); - FIt("should throw a 405 Not Allowed", () -> { + It("should throw a 405 Not Allowed", () -> { TEntity tentity = new TEntity(); tentity = repo.save(tentity); @@ -200,7 +201,7 @@ public interface UnexportedContentStore extends FilesystemContentStore Date: Mon, 12 Jun 2023 21:21:00 -0700 Subject: [PATCH 24/28] feat: encrypting content store interface should implement UnsetContentParams --- .../content/encryption/EncryptingContentStore.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java b/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java index d931915ee..bbfffcede 100644 --- a/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java +++ b/spring-content-encryption/src/main/java/org/springframework/content/encryption/EncryptingContentStore.java @@ -4,6 +4,7 @@ import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.GetResourceParams; import org.springframework.content.commons.repository.SetContentParams; +import org.springframework.content.commons.repository.UnsetContentParams; import org.springframework.core.io.Resource; import java.io.InputStream; @@ -22,4 +23,6 @@ public interface EncryptingContentStore extends Con Resource getResource(S entity, PropertyPath propertyPath, GetResourceParams params); Resource getResource(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.GetResourceParams params); S unsetContent(S entity, PropertyPath propertyPath); + S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params); + S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params); } From 68d896db339cff46ad6a41279f0f98d83967762f Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Mon, 12 Jun 2023 22:04:01 -0700 Subject: [PATCH 25/28] ci: use feat/content-preservation branch of gettingstarted to verify --- .github/workflows/prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml index 3be297694..f486e5b4d 100644 --- a/.github/workflows/prs.yml +++ b/.github/workflows/prs.yml @@ -97,7 +97,7 @@ jobs: with: repository: paulcwarren/spring-content-gettingstarted path: spring-content-gettingstarted - ref: refs/heads/main + ref: refs/heads/feat/content-preservation - name: Validate against Getting Started Guides run: | export AWS_REGION=us-west-1 From 49e4921a0b03da9010e4fe654007bebc7708ca52 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 13 Jun 2023 22:25:53 -0700 Subject: [PATCH 26/28] code: refactor StoreImpl --- .../commons/store/factory/StoreImpl.java | 641 ++++++------------ .../events/BeforeSetContentEvent.java | 6 + .../store/events/BeforeSetContentEvent.java | 6 + 3 files changed, 219 insertions(+), 434 deletions(-) diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java index 0f63c3466..888d47be5 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java @@ -50,156 +50,78 @@ public StoreImpl(Store delegate, ApplicationEventPublisher publish } @Override - public Object setContent(Object property, InputStream content) { - - Object result = null; - - File contentCopy = null; - TeeInputStream contentCopyStream = null; - try { - contentCopy = Files.createTempFile(copyContentRootPath, "contentCopy", ".tmp").toFile(); - contentCopyStream = new TeeInputStream(content, new FileOutputStream(contentCopy), true); - - org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = null; - BeforeSetContentEvent before = null; - oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, delegate, contentCopyStream); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - before = new BeforeSetContentEvent(property, contentStore, contentCopyStream); - publisher.publishEvent(before); - } - - // inputstream was processed and replaced - if (oldBefore != null && oldBefore.getInputStream() != null && oldBefore.getInputStream().equals(contentCopyStream) == false) { - content = oldBefore.getInputStream(); - } - else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { - content = before.getInputStream(); - } - // content was processed but not replaced - else if (contentCopyStream != null && contentCopyStream.isDirty()) { - while (contentCopyStream.read(new byte[4096]) != -1) { - } - content = new FileInputStream(contentCopy); - } - + public Object setContent(Object entity, InputStream content) { + return this.internalSetContent(entity, null, content, () -> { try { - result = castToDeprecatedContentStore(delegate).setContent(property, content); + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, content); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, content); + } } catch (Exception e) { throw e; } - - org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(result, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterSetContentEvent after = new AfterSetContentEvent(result, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - } catch (FileNotFoundException fileNotFoundException) { - fileNotFoundException.printStackTrace(); - } catch (IOException ioException) { - ioException.printStackTrace(); - } finally { - if (contentCopyStream != null) { - IOUtils.closeQuietly(contentCopyStream); - } - if (contentCopy != null) { - try { - Files.deleteIfExists(contentCopy.toPath()); - } catch (IOException e) { - logger.error(String.format("Unable to delete content copy %s", contentCopy.toPath()), e); - } - } - } - - return result; + }); } @Override - public Object setContent(Object property, PropertyPath propertyPath, InputStream content) { - return this.setContent(property, propertyPath, content, -1); + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content) { + return this.internalSetContent(entity, propertyPath, content, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, content); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content); + } + } + catch (Exception e) { + throw e; + } + }); } @Override - public Object setContent(Object property, PropertyPath propertyPath, InputStream content, long contentLen) { - Object result = null; - - File contentCopy = null; - TeeInputStream contentCopyStream = null; - try { - contentCopy = Files.createTempFile(copyContentRootPath, "contentCopy", ".tmp").toFile(); - contentCopyStream = new TeeInputStream(content, new FileOutputStream(contentCopy), true); - - org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = null; - BeforeSetContentEvent before = null; - - oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, propertyPath, delegate, contentCopyStream); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - before = new BeforeSetContentEvent(property, propertyPath, contentStore, contentCopyStream); - publisher.publishEvent(before); - } - - // inputstream was processed and replaced - if (oldBefore != null && oldBefore.getInputStream() != null && oldBefore.getInputStream().equals(contentCopyStream) == false) { - content = oldBefore.getInputStream(); - } - else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { - content = before.getInputStream(); - } - // content was processed but not replaced - else if (contentCopyStream != null && contentCopyStream.isDirty()) { - while (contentCopyStream.read(new byte[4096]) != -1) { + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.internalSetContent(entity, propertyPath, content, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, content, contentLen); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content, contentLen); } - content = new FileInputStream(contentCopy); } + catch (Exception e) { + throw e; + } + }); + } + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + return this.internalSetContent(entity, propertyPath, content, () -> { try { - result = castToDeprecatedContentStore(delegate).setContent(property, propertyPath, content, contentLen); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content, params); } catch (Exception e) { throw e; } + }); + } - org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterSetContentEvent after = new AfterSetContentEvent(property, propertyPath, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - } catch (FileNotFoundException fileNotFoundException) { - fileNotFoundException.printStackTrace(); - } catch (IOException ioException) { - ioException.printStackTrace(); - } finally { - if (contentCopyStream != null) { - IOUtils.closeQuietly(contentCopyStream); + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.internalSetContent(entity, propertyPath, content, () -> { + try { + return ((org.springframework.content.commons.store.ContentStore) delegate).setContent(entity, propertyPath, content, params); } - if (contentCopy != null) { - try { - Files.deleteIfExists(contentCopy.toPath()); - } catch (IOException e) { - logger.error(String.format("Unable to delete content copy %s", contentCopy.toPath()), e); - } + catch (Exception e) { + throw e; } - } - - return result; + }); } - @Override - public Object setContent(Object property, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + public Object internalSetContent(Object property, PropertyPath propertyPath, InputStream content, Supplier invocation) { Object result = null; File contentCopy = null; @@ -234,12 +156,7 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { content = new FileInputStream(contentCopy); } - try { - result = castToDeprecatedContentStore(delegate).setContent(property, propertyPath, content, params); - } - catch (Exception e) { - throw e; - } + result = invocation.get(); org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); oldAfter.setResult(result); @@ -271,113 +188,38 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { } @Override - public Object setContent(Object property, PropertyPath propertyPath, InputStream content, SetContentParams params) { - Object result = null; - - File contentCopy = null; - TeeInputStream contentCopyStream = null; - try { - contentCopy = Files.createTempFile(copyContentRootPath, "contentCopy", ".tmp").toFile(); - contentCopyStream = new TeeInputStream(content, new FileOutputStream(contentCopy), true); - - org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = null; - BeforeSetContentEvent before = null; - - oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, propertyPath, delegate, contentCopyStream); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - before = new BeforeSetContentEvent(property, propertyPath, contentStore, contentCopyStream); - publisher.publishEvent(before); - } - - // inputstream was processed and replaced - if (oldBefore != null && oldBefore.getInputStream() != null && oldBefore.getInputStream().equals(contentCopyStream) == false) { - content = oldBefore.getInputStream(); - } - else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { - content = before.getInputStream(); - } - // content was processed but not replaced - else if (contentCopyStream != null && contentCopyStream.isDirty()) { - while (contentCopyStream.read(new byte[4096]) != -1) { - } - content = new FileInputStream(contentCopy); - } - + public Object setContent(Object entity, Resource resourceContent) { + return this.internalSetContent(entity, null, resourceContent, () -> { try { - result = ((org.springframework.content.commons.store.ContentStore)delegate).setContent(property, propertyPath, content, params); + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, resourceContent); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, resourceContent); + } } catch (Exception e) { throw e; } - - org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterSetContentEvent after = new AfterSetContentEvent(property, propertyPath, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - } catch (FileNotFoundException fileNotFoundException) { - fileNotFoundException.printStackTrace(); - } catch (IOException ioException) { - ioException.printStackTrace(); - } finally { - if (contentCopyStream != null) { - IOUtils.closeQuietly(contentCopyStream); - } - if (contentCopy != null) { - try { - Files.deleteIfExists(contentCopy.toPath()); - } catch (IOException e) { - logger.error(String.format("Unable to delete content copy %s", contentCopy.toPath()), e); - } - } - } - - return result; + }); } @Override - public Object setContent(Object property, Resource resourceContent) { - - org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, delegate, resourceContent); - publisher.publishEvent(oldBefore); - ContentStore contentStore = castToContentStore(delegate); - - if (contentStore != null) { - BeforeSetContentEvent before = new BeforeSetContentEvent(property, contentStore, resourceContent); - publisher.publishEvent(before); - } - - Object result; - try { - result = castToDeprecatedContentStore(delegate).setContent(property, resourceContent); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterSetContentEvent after = new AfterSetContentEvent(property, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - - return result; + public Object setContent(Object entity, PropertyPath propertyPath, Resource resourceContent) { + return this.internalSetContent(entity, propertyPath, resourceContent, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, resourceContent); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, resourceContent); + } + } + catch (Exception e) { + throw e; + } + }); } - @Override - public Object setContent(Object property, PropertyPath propertyPath, Resource resourceContent) { - + public Object internalSetContent(Object property, PropertyPath propertyPath, Resource resourceContent, Supplier invocation) { org.springframework.content.commons.repository.events.BeforeSetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeSetContentEvent(property, propertyPath, delegate, resourceContent); publisher.publishEvent(oldBefore); @@ -387,13 +229,7 @@ public Object setContent(Object property, PropertyPath propertyPath, Resource re publisher.publishEvent(before); } - Object result; - try { - result = castToDeprecatedContentStore(delegate).setContent(property, propertyPath, resourceContent); - } - catch (Exception e) { - throw e; - } + Object result = invocation.get(); org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); oldAfter.setResult(result); @@ -408,59 +244,55 @@ public Object setContent(Object property, PropertyPath propertyPath, Resource re } @Override - public Object unsetContent(Object property) { - - org.springframework.content.commons.repository.events.BeforeUnsetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeUnsetContentEvent(property, delegate); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - BeforeUnsetContentEvent before = new BeforeUnsetContentEvent(property, contentStore); - publisher.publishEvent(before); - } - - Object result; - try { - result = castToDeprecatedContentStore(delegate).unsetContent(property); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterUnsetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterUnsetContentEvent(property, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterUnsetContentEvent after = new AfterUnsetContentEvent(property, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - return result; + public Object unsetContent(Object entity) { + return this.internalUnsetContent(entity, null, + () -> { + Object result; + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).unsetContent(entity); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).unsetContent(entity); + } + } + catch (Exception e) { + throw e; + } + }); } - @Override - public Object unsetContent(Object property, PropertyPath propertyPath) { - return this.unsetContent(property, propertyPath, UnsetContentParams.builder().build()); + public Object unsetContent(Object entity, PropertyPath propertyPath) { + return this.internalUnsetContent(entity, propertyPath, + () -> { + Object result; + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).unsetContent(entity, propertyPath); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).unsetContent(entity, propertyPath); + } + } + catch (Exception e) { + throw e; + } + }); } @Override public Object unsetContent(Object entity, PropertyPath propertyPath, org.springframework.content.commons.repository.UnsetContentParams params) { - return this.internalUnsetContent(entity, propertyPath, () -> { Object result; try { - if (delegate instanceof org.springframework.content.commons.repository.ContentStore) { - return castToDeprecatedContentStore(delegate).unsetContent(entity, propertyPath, params); - } else { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { int ordinal = params.getDisposition().ordinal(); UnsetContentParams params1 = UnsetContentParams.builder() .disposition(UnsetContentParams.Disposition.values()[ordinal]) .build(); - - return castToContentStore(delegate).unsetContent(entity, propertyPath, params1); + return ((org.springframework.content.commons.store.ContentStore)(delegate)).unsetContent(entity, propertyPath, params1); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).unsetContent(entity, propertyPath, params); } } catch (Exception e) { @@ -475,14 +307,14 @@ public Object unsetContent(Object entity, PropertyPath propertyPath, UnsetConten () -> { Object result; try { - if (delegate instanceof org.springframework.content.commons.repository.ContentStore) { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).unsetContent(entity, propertyPath, params); + } else { int ordinal = params.getDisposition().ordinal(); org.springframework.content.commons.repository.UnsetContentParams params1 = org.springframework.content.commons.repository.UnsetContentParams.builder() .disposition(org.springframework.content.commons.repository.UnsetContentParams.Disposition.values()[ordinal]) .build(); - return castToDeprecatedContentStore(delegate).unsetContent(entity, propertyPath, params1); - } else { - return castToContentStore(delegate).unsetContent(entity, propertyPath, params); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).unsetContent(entity, propertyPath, params1); } } catch (Exception e) { @@ -518,58 +350,48 @@ public Object internalUnsetContent(Object entity, PropertyPath propertyPath, Sup } @Override - public InputStream getContent(Object property) { - - org.springframework.content.commons.repository.events.BeforeGetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetContentEvent(property, delegate); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - BeforeGetContentEvent before = new BeforeGetContentEvent(property, contentStore); - publisher.publishEvent(before); - } - - InputStream result; - try { - result = castToDeprecatedContentStore(delegate).getContent(property); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterGetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetContentEvent(property, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterGetContentEvent after = new AfterGetContentEvent(property, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - return result; + public InputStream getContent(Object entity) { + return this.internalGetContent(entity, null, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).getContent(entity); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).getContent(entity); + } + } catch (Exception e) { + throw e; + } + }); } @Override - public InputStream getContent(Object property, PropertyPath propertyPath) { + public InputStream getContent(Object entity, PropertyPath propertyPath) { + return this.internalGetContent(entity, propertyPath, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.ContentStore) { + return ((org.springframework.content.commons.store.ContentStore)(delegate)).getContent(entity, propertyPath); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).getContent(entity, propertyPath); + } + } catch (Exception e) { + throw e; + } + }); + } - org.springframework.content.commons.repository.events.BeforeGetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetContentEvent(property, propertyPath, delegate); + public InputStream internalGetContent(Object entity, PropertyPath propertyPath, Supplier invocation) { + org.springframework.content.commons.repository.events.BeforeGetContentEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetContentEvent(entity, propertyPath, delegate); publisher.publishEvent(oldBefore); ContentStore contentStore = castToContentStore(delegate); if (contentStore != null) { - BeforeGetContentEvent before = new BeforeGetContentEvent(property, propertyPath, contentStore); + BeforeGetContentEvent before = new BeforeGetContentEvent(entity, propertyPath, contentStore); publisher.publishEvent(before); } - InputStream result; - try { - result = castToDeprecatedContentStore(delegate).getContent(property, propertyPath); - } - catch (Exception e) { - throw e; - } + InputStream result = invocation.get(); - org.springframework.content.commons.repository.events.AfterGetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetContentEvent(property, propertyPath, delegate); + org.springframework.content.commons.repository.events.AfterGetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetContentEvent(entity, propertyPath, delegate); oldAfter.setResult(result); publisher.publishEvent(oldAfter); if (oldAfter.getResult() != null) { @@ -577,7 +399,7 @@ public InputStream getContent(Object property, PropertyPath propertyPath) { } if (contentStore != null) { - AfterGetContentEvent after = new AfterGetContentEvent(property, propertyPath, contentStore); + AfterGetContentEvent after = new AfterGetContentEvent(entity, propertyPath, contentStore); after.setResult(result); publisher.publishEvent(after); if (after.getResult() != null) { @@ -590,117 +412,61 @@ public InputStream getContent(Object property, PropertyPath propertyPath) { @Override public Resource getResource(Object entity) { - - org.springframework.content.commons.repository.events.BeforeGetResourceEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetResourceEvent(entity, delegate); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - BeforeGetResourceEvent before = new BeforeGetResourceEvent(entity, contentStore); - publisher.publishEvent(before); - } - - Resource result; - try { - result = castToDeprecatedContentStore(delegate).getResource(entity); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterGetResourceEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetResourceEvent(entity, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - - if (contentStore != null) { - AfterGetResourceEvent after = new AfterGetResourceEvent(entity, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - return result; + return this.internalGetResource(entity, null, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.Store) { + return ((org.springframework.content.commons.store.AssociativeStore)(delegate)).getResource(entity); + } else { + return ((org.springframework.content.commons.repository.AssociativeStore) (delegate)).getResource(entity); + } + } + catch (Exception e) { + throw e; + } + }); } @Override public Resource getResource(Object entity, PropertyPath propertyPath) { - - org.springframework.content.commons.repository.events.BeforeGetResourceEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetResourceEvent(entity, propertyPath, delegate); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - BeforeGetResourceEvent before = new BeforeGetResourceEvent(entity, propertyPath, contentStore); - publisher.publishEvent(before); - } - - Resource result; - try { - result = castToDeprecatedContentStore(delegate).getResource(entity, propertyPath); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterGetResourceEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetResourceEvent(entity, propertyPath, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - if (oldAfter.getResult() != null) { - result = (Resource) oldAfter.getResult(); - } - - if (contentStore != null) { - AfterGetResourceEvent after = new AfterGetResourceEvent(entity, propertyPath, contentStore); - after.setResult(result); - publisher.publishEvent(after); - if (after.getStore() != null) { - result = (Resource) after.getResult(); + return this.internalGetResource(entity, propertyPath, () -> { + try { + if (delegate instanceof org.springframework.content.commons.store.AssociativeStore) { + return ((org.springframework.content.commons.store.AssociativeStore)(delegate)).getResource(entity, propertyPath); + } else { + return ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).getResource(entity, propertyPath); + } } - } - - return result; + catch (Exception e) { + throw e; + } + }); } @Override public Resource getResource(Object entity, PropertyPath propertyPath, org.springframework.content.commons.repository.GetResourceParams oldParams) { - - org.springframework.content.commons.repository.events.BeforeGetResourceEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetResourceEvent(entity, propertyPath, delegate); - publisher.publishEvent(oldBefore); - - ContentStore contentStore = castToContentStore(delegate); - if (contentStore != null) { - BeforeGetResourceEvent before = new BeforeGetResourceEvent(entity, propertyPath, contentStore); - publisher.publishEvent(before); - } - - Resource result; - try { - result = castToDeprecatedContentStore(delegate).getResource(entity, propertyPath, oldParams); - } - catch (Exception e) { - throw e; - } - - org.springframework.content.commons.repository.events.AfterGetResourceEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetResourceEvent(entity, propertyPath, delegate); - oldAfter.setResult(result); - publisher.publishEvent(oldAfter); - if (oldAfter.getResult() != null) { - result = (Resource) oldAfter.getResult(); - } - - if (contentStore != null) { - AfterGetResourceEvent after = new AfterGetResourceEvent(entity, propertyPath, contentStore); - after.setResult(result); - publisher.publishEvent(after); - if (after.getResult() != null) { - result = (Resource) after.getResult(); + return this.internalGetResource(entity, propertyPath, () -> { + try { + return ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).getResource(entity, propertyPath, oldParams); } - } - - return result; + catch (Exception e) { + throw e; + } + }); } @Override public Resource getResource(Object entity, PropertyPath propertyPath, GetResourceParams params) { + return this.internalGetResource(entity, propertyPath, () -> { + try { + return ((org.springframework.content.commons.store.AssociativeStore) delegate).getResource(entity, propertyPath, params); + } + catch (Exception e) { + throw e; + } + }); + } + public Resource internalGetResource(Object entity, PropertyPath propertyPath, Supplier invocation) { org.springframework.content.commons.repository.events.BeforeGetResourceEvent oldBefore = new org.springframework.content.commons.repository.events.BeforeGetResourceEvent(entity, propertyPath, delegate); publisher.publishEvent(oldBefore); @@ -710,13 +476,7 @@ public Resource getResource(Object entity, PropertyPath propertyPath, GetResourc publisher.publishEvent(before); } - Resource result; - try { - result = ((org.springframework.content.commons.store.AssociativeStore)delegate).getResource(entity, propertyPath, params); - } - catch (Exception e) { - throw e; - } + Resource result = invocation.get(); org.springframework.content.commons.repository.events.AfterGetResourceEvent oldAfter = new org.springframework.content.commons.repository.events.AfterGetResourceEvent(entity, propertyPath, delegate); oldAfter.setResult(result); @@ -751,7 +511,11 @@ public Resource getResource(Serializable id) { Resource result; try { - result = castToDeprecatedContentStore(delegate).getResource(id); + if (delegate instanceof org.springframework.content.commons.store.Store) { + result = ((org.springframework.content.commons.store.Store)(delegate)).getResource(id); + } else { + result = ((org.springframework.content.commons.repository.Store)(delegate)).getResource(id); + } } catch (Exception e) { throw e; @@ -784,7 +548,11 @@ public void associate(Object entity, Serializable id) { try { - castToDeprecatedContentStore(delegate).associate(entity, id); + if (delegate instanceof org.springframework.content.commons.store.AssociativeStore) { + ((org.springframework.content.commons.store.AssociativeStore)(delegate)).associate(entity, id); + } else { + ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).associate(entity, id); + } } catch (Exception e) { throw e; @@ -812,7 +580,11 @@ public void associate(Object entity, PropertyPath propertyPath, Serializable id) } try { - castToDeprecatedContentStore(delegate).associate(entity, propertyPath, id); + if (delegate instanceof org.springframework.content.commons.store.AssociativeStore) { + ((org.springframework.content.commons.store.AssociativeStore)(delegate)).associate(entity, propertyPath, id); + } else { + ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).associate(entity, propertyPath, id); + } } catch (Exception e) { throw e; @@ -840,7 +612,11 @@ public void unassociate(Object entity) { } try { - castToDeprecatedContentStore(delegate).unassociate(entity); + if (delegate instanceof org.springframework.content.commons.store.AssociativeStore) { + ((org.springframework.content.commons.store.AssociativeStore)(delegate)).unassociate(entity); + } else { + ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).unassociate(entity); + } } catch (Exception e) { throw e; @@ -868,7 +644,11 @@ public void unassociate(Object entity, PropertyPath propertyPath) { } try { - castToDeprecatedContentStore(delegate).unassociate(entity, propertyPath); + if (delegate instanceof org.springframework.content.commons.store.AssociativeStore) { + ((org.springframework.content.commons.store.AssociativeStore)(delegate)).unassociate(entity, propertyPath); + } else { + ((org.springframework.content.commons.repository.AssociativeStore)(delegate)).unassociate(entity, propertyPath); + } } catch (Exception e) { throw e; @@ -883,13 +663,6 @@ public void unassociate(Object entity, PropertyPath propertyPath) { } } - private org.springframework.content.commons.repository.ContentStore castToDeprecatedContentStore(Store delegate) { - if (delegate instanceof org.springframework.content.commons.repository.ContentStore == false) { - throw new StoreAccessException("store does not implement org.springframework.content.commons.repository.ContentStore"); - } - return (org.springframework.content.commons.repository.ContentStore)delegate; - } - private ContentStore castToContentStore(Store delegate) { if (delegate instanceof ContentStore == false) { return null; diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/events/BeforeSetContentEvent.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/events/BeforeSetContentEvent.java index 1900e0dd8..8a7037cf6 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/repository/events/BeforeSetContentEvent.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/events/BeforeSetContentEvent.java @@ -42,6 +42,12 @@ public BeforeSetContentEvent(Object source, PropertyPath propertyPath, Store store, InputStream is, Resource resource) { + super(source, propertyPath, store); + this.inputStream = is; + this.resource = resource; + } + /** * Deprecated. * diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/store/events/BeforeSetContentEvent.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/events/BeforeSetContentEvent.java index 035a9a833..55925d513 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/store/events/BeforeSetContentEvent.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/events/BeforeSetContentEvent.java @@ -36,6 +36,12 @@ public BeforeSetContentEvent(Object source, PropertyPath propertyPath, Store store, InputStream is, Resource resource) { + super(source, propertyPath, store); + this.inputStream = is; + this.resource = resource; + } + /** * Deprecated. * From ffb04067270a1d7a47f35779ff79c0eb775929b1 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 14 Jun 2023 20:41:55 -0700 Subject: [PATCH 27/28] fix: StoreImpl fragment should pass modified content through to delegate --- .../commons/store/factory/StoreImpl.java | 31 ++++++++++--------- .../jpa/store/DefaultJpaStoreImpl.java | 4 +-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java index 888d47be5..f02752260 100644 --- a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java +++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/store/factory/StoreImpl.java @@ -10,6 +10,7 @@ import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Function; import java.util.function.Supplier; import org.apache.commons.io.IOUtils; @@ -51,12 +52,12 @@ public StoreImpl(Store delegate, ApplicationEventPublisher publish @Override public Object setContent(Object entity, InputStream content) { - return this.internalSetContent(entity, null, content, () -> { + return this.internalSetContent(entity, null, content, (actualContent) -> { try { if (delegate instanceof org.springframework.content.commons.store.ContentStore) { - return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, content); + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, actualContent); } else { - return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, content); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, actualContent); } } catch (Exception e) { @@ -67,12 +68,12 @@ public Object setContent(Object entity, InputStream content) { @Override public Object setContent(Object entity, PropertyPath propertyPath, InputStream content) { - return this.internalSetContent(entity, propertyPath, content, () -> { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { try { if (delegate instanceof org.springframework.content.commons.store.ContentStore) { - return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, content); + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent); } else { - return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent); } } catch (Exception e) { @@ -83,12 +84,12 @@ public Object setContent(Object entity, PropertyPath propertyPath, InputStream c @Override public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, long contentLen) { - return this.internalSetContent(entity, propertyPath, content, () -> { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { try { if (delegate instanceof org.springframework.content.commons.store.ContentStore) { - return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, content, contentLen); + return ((org.springframework.content.commons.store.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent, contentLen); } else { - return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content, contentLen); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent, contentLen); } } catch (Exception e) { @@ -99,9 +100,9 @@ public Object setContent(Object entity, PropertyPath propertyPath, InputStream c @Override public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { - return this.internalSetContent(entity, propertyPath, content, () -> { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { try { - return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, content, params); + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent, params); } catch (Exception e) { throw e; @@ -111,9 +112,9 @@ public Object setContent(Object entity, PropertyPath propertyPath, InputStream c @Override public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { - return this.internalSetContent(entity, propertyPath, content, () -> { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { try { - return ((org.springframework.content.commons.store.ContentStore) delegate).setContent(entity, propertyPath, content, params); + return ((org.springframework.content.commons.store.ContentStore) delegate).setContent(entity, propertyPath, actualContent, params); } catch (Exception e) { throw e; @@ -121,7 +122,7 @@ public Object setContent(Object entity, PropertyPath propertyPath, InputStream c }); } - public Object internalSetContent(Object property, PropertyPath propertyPath, InputStream content, Supplier invocation) { + public Object internalSetContent(Object property, PropertyPath propertyPath, InputStream content, Function invocation) { Object result = null; File contentCopy = null; @@ -156,7 +157,7 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { content = new FileInputStream(contentCopy); } - result = invocation.get(); + result = invocation.apply(content); org.springframework.content.commons.repository.events.AfterSetContentEvent oldAfter = new org.springframework.content.commons.repository.events.AfterSetContentEvent(property, propertyPath, delegate); oldAfter.setResult(result); diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index 638429383..eeadb6a7f 100644 --- a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java +++ b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java @@ -359,8 +359,8 @@ public S unsetContent(S entity, PropertyPath propertyPath) { @Override public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params) { int ordinal = params.getDisposition().ordinal(); - org.springframework.content.commons.store.UnsetContentParams params1 = org.springframework.content.commons.store.UnsetContentParams.builder() - .disposition(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]) + org.springframework.content.commons.repository.UnsetContentParams params1 = org.springframework.content.commons.repository.UnsetContentParams.builder() + .disposition(org.springframework.content.commons.repository.UnsetContentParams.Disposition.values()[ordinal]) .build(); return this.unsetContent(entity, propertyPath, params1); } From 045848b3081dbcc27d2f937ea4d7da308c4f8d82 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 14 Jun 2023 21:35:07 -0700 Subject: [PATCH 28/28] docs: update docs to explain new SetContentParams, UnsetContentParams and associated spring boot properties --- .../src/main/asciidoc/azure.adoc | 19 +++++++++--------- spring-content-fs/src/main/asciidoc/fs.adoc | 20 ++++++++++--------- spring-content-gcs/src/main/asciidoc/gcs.adoc | 19 +++++++++--------- spring-content-jpa/src/main/asciidoc/jpa.adoc | 16 +++++++-------- .../src/main/asciidoc/mongo.adoc | 18 +++++++---------- .../asciidoc/rest-contentdisposition.adoc | 5 +++++ .../src/main/asciidoc/rest-index.adoc | 1 + spring-content-s3/src/main/asciidoc/s3.adoc | 13 ++++++------ 8 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 spring-content-rest/src/main/asciidoc/rest-contentdisposition.adoc diff --git a/spring-content-azure-storage/src/main/asciidoc/azure.adoc b/spring-content-azure-storage/src/main/asciidoc/azure.adoc index 831c73c35..040f3aeac 100644 --- a/spring-content-azure-storage/src/main/asciidoc/azure.adoc +++ b/spring-content-azure-storage/src/main/asciidoc/azure.adoc @@ -99,24 +99,25 @@ instance of `BlobId` See <> for more information on how to register a converter. -=== Setting Content using a ContentStore +=== Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -If content has not yet been stored with this entity and an Id has not been assigned one will be generated -based in `java.util.UUID`. +The `PropertyPath` will be used to resolve the content property to update. + +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based in `java.util.UUID`. The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -If content has previously been stored it will overwritten updating just the @ContentLength attribute, if present. +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. -=== Getting Content from a ContentStore +=== Getting Content -Content can be accessed using the `ContentStore.getContent(entity)` method. +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. -=== Unsetting Content from a ContentStore +=== Unsetting Content -Content can be removed using the `ContentStore.unsetContent(entity)` method. +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned. === Configuring a Spring Converter [[configuring_converters]] diff --git a/spring-content-fs/src/main/asciidoc/fs.adoc b/spring-content-fs/src/main/asciidoc/fs.adoc index 124a691ee..9f2e2f0e8 100644 --- a/spring-content-fs/src/main/asciidoc/fs.adoc +++ b/spring-content-fs/src/main/asciidoc/fs.adoc @@ -100,23 +100,25 @@ convert the value to a String location. See <> for more information on how to register a converter. -=== Setting Content using a ContentStore +=== Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -If content has not yet been associated with this entity before and an ID has not been assigned by the application, one will be generated based on `java.util.UUID` and converted to the type of the @ContentId field. +The `PropertyPath` will be used to resolve the content property to update. -The @ContentId and @ContentLength annotations will be updated on `entity`. +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based in `java.util.UUID`. -If content has been previously stored it will overwritten updating just the @ContentLength attribute, if appropriate. +The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -=== Getting Content from a ContentStore +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. -Content can be accessed using the `ContentStore.getContent(entity)` method. +=== Getting Content -=== Unsetting Content from a ContentStore +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. -Content can be removed using the `ContentStore.unsetContent(entity)` method. +=== Unsetting Content + +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned. === Storage Customization [[configuring_converters]] diff --git a/spring-content-gcs/src/main/asciidoc/gcs.adoc b/spring-content-gcs/src/main/asciidoc/gcs.adoc index a5080d424..91cd2398c 100644 --- a/spring-content-gcs/src/main/asciidoc/gcs.adoc +++ b/spring-content-gcs/src/main/asciidoc/gcs.adoc @@ -101,24 +101,25 @@ instance of `BlobId` See <> for more information on how to register a converter. -=== Setting Content using a ContentStore +=== Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -If content has not yet been stored with this entity and an Id has not been assigned one will be generated -based in `java.util.UUID`. +The `PropertyPath` will be used to resolve the content property to update. + +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based in `java.util.UUID`. The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -If content has previously been stored it will overwritten updating just the @ContentLength attribute, if present. +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. -=== Getting Content from a ContentStore +=== Getting Content -Content can be accessed using the `ContentStore.getContent(entity)` method. +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. -=== Unsetting Content from a ContentStore +=== Unsetting Content -Content can be removed using the `ContentStore.unsetContent(entity)` method. +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned. === Configuring a Spring Converter [[configuring_converters]] diff --git a/spring-content-jpa/src/main/asciidoc/jpa.adoc b/spring-content-jpa/src/main/asciidoc/jpa.adoc index 1c601dbd7..eccf60207 100644 --- a/spring-content-jpa/src/main/asciidoc/jpa.adoc +++ b/spring-content-jpa/src/main/asciidoc/jpa.adoc @@ -144,22 +144,20 @@ of whatever driver version is in use will apply. === Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -The entity's `@ContentId` and `@ContentLength` fields will be updated. +The `PropertyPath` will be used to resolve the content property to update. -If content has been previously stored this will overwritten with the new content updating just the `@ContentLength` -field, if appropriate. +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based on `java.util.UUID`. -==== How the @ContentId field is handled +The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -Spring Data JPA requires that content entities have an `@ContentId` field for identity that will be generated when -content is initially set. +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. === Getting Content -Content can be accessed using the `ContentStore.getContent(entity)` method. +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. === Unsetting Content -Content can be removed using the `ContentStore.unsetContent(entity)` method. +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned. diff --git a/spring-content-mongo/src/main/asciidoc/mongo.adoc b/spring-content-mongo/src/main/asciidoc/mongo.adoc index e513d89aa..b8bc6768d 100644 --- a/spring-content-mongo/src/main/asciidoc/mongo.adoc +++ b/spring-content-mongo/src/main/asciidoc/mongo.adoc @@ -66,24 +66,20 @@ The module id for the `spring.content.storage.type.default` property is `gridfs` === Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -The fields annotated with @ContentId and @ContentLength will be updated on `entity`. +The `PropertyPath` will be used to resolve the content property to update. -If content has been previously stored it will overwritten updating just the @ContentLength attribute, if appropriate. +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based in `java.util.UUID`. -==== How the @ContentId field is handled +The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -The MongoDB Store uses a dedicated `ConversionService` to convert the content entity's ID into a resource path. - -It is possible to influence this conversion and therefore the storage model by configuring your application to contribute one (or more) `org.springframework.content.mongo.config.MongoStoreConverter` beans. +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. === Getting Content -Content can be accessed using the `ContentStore.getContent(entity)` method. +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. === Unsetting Content -Content can be removed using the `ContentStore.unsetContent(entity)` method. - -When content is unset the fields annotated with @ContentId and @ContentLength will also be reset to default values; +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned. diff --git a/spring-content-rest/src/main/asciidoc/rest-contentdisposition.adoc b/spring-content-rest/src/main/asciidoc/rest-contentdisposition.adoc new file mode 100644 index 000000000..7f1f7247c --- /dev/null +++ b/spring-content-rest/src/main/asciidoc/rest-contentdisposition.adoc @@ -0,0 +1,5 @@ +== Content Disposition +For easier management of the content in the backend storage `setContent` methods overwrite existing content and `unsetContent` methods delete it. This is easy and typically a good starting point but some applications may want to preserve old content no longer referenced by the application. + +Using the `spring.content.rest.set-content-disposition=CreateNew` and `spring.content.rest.unset-content-disposition=Keep` properties this behavior can be changed to preserve the old content. Please note that as far as Spring Content is concerned this old content is then orphaned from the application and must be managed out-of-band. This behavior change is application-wide. + diff --git a/spring-content-rest/src/main/asciidoc/rest-index.adoc b/spring-content-rest/src/main/asciidoc/rest-index.adoc index 735e5a75d..c99c14c97 100644 --- a/spring-content-rest/src/main/asciidoc/rest-index.adoc +++ b/spring-content-rest/src/main/asciidoc/rest-index.adoc @@ -40,3 +40,4 @@ include::rest-cachecontrol.adoc[leveloffset=+1] include::rest-fullyqualifiedlinks.adoc[leveloffset=+1] include::rest-storeresolver.adoc[leveloffset=+1] include::rest-putpostresolver.adoc[leveloffset=+1] +include::rest-contentdisposition.adoc[leveloffset=+1] diff --git a/spring-content-s3/src/main/asciidoc/s3.adoc b/spring-content-s3/src/main/asciidoc/s3.adoc index 027674c93..f14cd86f2 100644 --- a/spring-content-s3/src/main/asciidoc/s3.adoc +++ b/spring-content-s3/src/main/asciidoc/s3.adoc @@ -207,19 +207,20 @@ public class S3StoreConfiguration { === Setting Content -Storing content is achieved using the `ContentStore.setContent(entity, InputStream)` method. +Storing content is achieved using the `ContentStore.setContent(T entity, PropertyPath path, InputStream content, SetContentParams params)` method. -If content has not yet been stored with this entity and an Id has not been assigned one will be generated -based in `java.util.UUID`. +The `PropertyPath` will be used to resolve the content property to update. + +If content has not yet been stored with this entity and an Id has not been assigned, one will be generated based in `java.util.UUID`. The `@ContentId` and `@ContentLength` annotations will be updated on `entity`. -If content has previously been stored it will overwritten updating just the @ContentLength attribute, if present. +If content has previously been stored it will be overwritten also updating the @ContentLength attribute, if present. However, using `ContentDisposition.Create` on the `SetContentParams` a new Id will be assigned and content stored, leaving the existing content in place and orphaned. === Getting Content -Content can be accessed using the `ContentStore.getContent(entity)` method. +Content can be accessed using the `ContentStore.getContent(T entity, PropertyPath path)` method. === Unsetting Content -Content can be removed using the `ContentStore.unsetContent(entity)` method. +Content can be removed using the `ContentStore.unsetContent(T entity, PropertyPath path, UnsetContentParams params)` method. Using `ContentDisposition.Keep` on `UnsetContentParams` will leave the content in storage and orphaned.