From bc1ae5b28d6bbf32920221087fe0a5ccf9c30a6a Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 14 Jun 2023 22:06:59 -0700 Subject: [PATCH] Feat/content preservation (#1406) * wip * feat: add a new setContent method accepting a SetContentParams arg with content length and overwriteExistingContent - implement in filesystem storage module * test: fix tests broken by setContent(T, InputStream) refactor * feat: dont refactor setContent(T, InputStream) - it breaks backward compatibility * ci: set up a tmate if ci fails * ci: redfine the action/cache key - I think each build is getting a new key because the pom hash is probably different * ci: ensure maven cache is saved * c i: dont restore the cache for the build job, just save it - otherwise the save fails with a 'already creating' error * wip * feat: implement the repository version of setContnt with SetContentParams * feat: s3 module implements setContent with SetContentParams (unsupported operation) * feat: wire overwriteExistingContent property through from auto configuration to content store content service * feat: implement new setContent with SetContentParams method in azure storage module * feat: implement new setContent with SetContentParams method in the mongo storage module * feat: implement new setContent with SetContentParams method in gcs storage module * feat: implement new setContent with SetContentParams method in jpa storage module * feat: implement new setContent with SetContentParams method in s3 storage module * feat: encryption storage module now support new ContentStore - as does ContentStoreAware interface optionally implemented by fragments * fix: store rest controller multipart form put handler mis-specified an argument meaning the input Resource could not be resolved * feat: add unsetContent with UnsetContentParams to all storage modules allowing content to be kept * feat: refactor SetContentParams.overwriteExistingContent as ContentDisposition * feat: refactor boot and rest layer overwrite existing content property to use SetContentDisposition enum - add and wire through an UnsetContentDisposition enum * test: update tests broken by introduction of UnsetContentParams - Unfocus accidentally focussed tests * feat: encrypting content store interface should implement UnsetContentParams * ci: use feat/content-preservation branch of gettingstarted to verify * code: refactor StoreImpl * fix: StoreImpl fragment should pass modified content through to delegate * docs: update docs to explain new SetContentParams, UnsetContentParams and associated spring boot properties --- .github/workflows/prs.yml | 6 +- .../ContentRestAutoConfiguration.java | 31 +- .../SpringBootContentRestConfigurer.java | 5 + .../ContentRestAutoConfigurationTest.java | 3 + .../src/main/asciidoc/azure.adoc | 19 +- .../azure/store/DefaultAzureStorageImpl.java | 46 +- .../content/azure/it/AzureStorageIT.java | 60 +- .../commons/store/factory/StoreImpl.java | 568 +++--- .../store/factory/StoreMethodInterceptor.java | 10 +- .../content/fragments/RenderableImpl.java | 23 +- .../commons/fragments/ContentStoreAware.java | 2 +- .../commons/repository/ContentStore.java | 8 +- .../commons/repository/SetContentParams.java | 19 + .../repository/UnsetContentParams.java | 17 + .../events/BeforeSetContentEvent.java | 6 + .../content/commons/store/ContentStore.java | 8 +- .../commons/store/SetContentParams.java | 18 + .../commons/store/UnsetContentParams.java | 16 + .../store/events/BeforeSetContentEvent.java | 6 + .../content/commons/utils/AssertUtils.java | 15 + .../factory/AbstractStoreFactoryBeanTest.java | 16 +- .../testsupport/TestStoreFactoryBean.java | 14 +- .../fragments/EncryptingContentStoreImpl.java | 249 ++- .../encryption/EncryptingContentStore.java | 7 + .../content/encryption/s3/EncryptionIT.java | 21 +- spring-content-fs/src/main/asciidoc/fs.adoc | 20 +- .../fs/store/DefaultFilesystemStoreImpl.java | 76 +- .../DefaultFilesystemStoresImplTest.java | 1532 ++++++++--------- .../test/java/it/events/BeforeSetEventIT.java | 4 +- .../test/java/it/store/FilesystemStoreIT.java | 85 +- spring-content-gcs/src/main/asciidoc/gcs.adoc | 19 +- .../gcs/store/DefaultGCPStorageImpl.java | 106 +- .../gcs/it/DeprecatedGCPStorageIT.java | 2 +- .../content/gcs/it/GCPStorageIT.java | 58 +- spring-content-jpa/src/main/asciidoc/jpa.adoc | 16 +- .../jpa/store/DefaultJpaStoreImpl.java | 44 +- .../content/jpa/ContentStoreIT.java | 54 +- .../jpa/config/EnableJpaStoresTest.java | 14 +- .../src/main/asciidoc/mongo.adoc | 18 +- .../mongo/store/DefaultMongoStoreImpl.java | 72 +- .../content/mongo/it/MongoStoreIT.java | 64 +- .../asciidoc/rest-contentdisposition.adoc | 5 + .../src/main/asciidoc/rest-index.adoc | 1 + .../ContentStoreContentService.java | 79 +- .../rest/controllers/StoreRestController.java | 7 +- .../rest/config/RestConfiguration.java | 33 +- .../rest/config/SetContentDisposition.java | 5 + .../content/rest/config/SetContentParams.java | 16 + .../rest/config/UnsetContentDisposition.java | 5 + .../http_405/MethodNotAllowedExceptionIT.java | 11 +- .../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 +- .../content/rest/controllers/Content.java | 28 +- spring-content-s3/src/main/asciidoc/s3.adoc | 13 +- .../content/s3/store/DefaultS3StoreImpl.java | 95 +- .../content/s3/config/EnableS3StoresTest.java | 2 +- .../content/s3/it/S3StoreIT.java | 70 +- .../content/fragments/SearchableImpl.java | 4 + 62 files changed, 2376 insertions(+), 1463 deletions(-) create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java 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/SetContentParams.java create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/store/UnsetContentParams.java create mode 100644 spring-content-commons/src/main/java/org/springframework/content/commons/utils/AssertUtils.java create mode 100644 spring-content-rest/src/main/asciidoc/rest-contentdisposition.adoc create mode 100644 spring-content-rest/src/main/java/org/springframework/content/rest/config/SetContentDisposition.java 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/.github/workflows/prs.yml b/.github/workflows/prs.yml index 1f675d1ba..f486e5b4d 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 @@ -93,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 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..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; @@ -25,6 +26,10 @@ 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; + + private SetContentDisposition setContentDisposition = RestConfiguration.SETCONTENT_CONTENT_DISPOSITION_DEFAULT; + private UnsetContentDisposition unsetContentDisposition = RestConfiguration.UNSETCONTENT_CONTENT_DISPOSITION_DEFAULT; public URI getBaseUri() { return baseUri; @@ -50,6 +55,30 @@ public void setShortcutRequestMappings(ShortcutRequestMappings requestMappings) this.requestMappings = requestMappings; } + public boolean getOverwriteExistingContent() { + return this.overwriteExistingContent; + } + + 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 4321472bb..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 @@ -57,5 +57,10 @@ public void configure(RestConfiguration config) { } } } + + config.setOverwriteExistingContent(properties.getOverwriteExistingContent()); + + config.setSetContentDisposition(properties.getSetContentDisposition()); + config.setUnsetContentDisposition(properties.getUnsetContentDisposition()); } } 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-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-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..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 @@ -21,9 +21,8 @@ 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.store.AssociativeStore; -import org.springframework.content.commons.store.GetResourceParams; -import org.springframework.content.commons.store.StoreAccessException; +import org.springframework.content.commons.repository.SetContentParams; +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; @@ -45,7 +44,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 +289,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.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -323,7 +334,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(); } @@ -446,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())); @@ -456,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(); @@ -474,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; } } @@ -488,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 4b77904a9..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 @@ -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,8 @@ 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.content.commons.store.UnsetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -379,7 +379,31 @@ public class AzureStorageIT { }); }); - Context("when content is deleted", () -> { + 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().disposition(SetContentParams.ContentDisposition.CreateNew).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 unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -396,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 fe1e83e69..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,15 +10,15 @@ 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; 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.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; @@ -51,84 +51,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(); + public Object setContent(Object entity, InputStream 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, actualContent); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, actualContent); + } } - else if (before != null && before.getInputStream() != null && before.getInputStream().equals(contentCopyStream) == false) { - content = before.getInputStream(); + catch (Exception e) { + throw e; } - // content was processed but not replaced - else if (contentCopyStream != null && contentCopyStream.isDirty()) { - while (contentCopyStream.read(new byte[4096]) != -1) { + }); + } + + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream 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, actualContent); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent); } - content = new FileInputStream(contentCopy); } + catch (Exception e) { + throw e; + } + }); + } + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, long contentLen) { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { 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, propertyPath, actualContent, contentLen); + } else { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent, contentLen); + } } 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); + @Override + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { + try { + return ((org.springframework.content.commons.repository.ContentStore)(delegate)).setContent(entity, propertyPath, actualContent, 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) { - return this.setContent(property, propertyPath, content, -1); + public Object setContent(Object entity, PropertyPath propertyPath, InputStream content, SetContentParams params) { + return this.internalSetContent(entity, propertyPath, content, (actualContent) -> { + try { + return ((org.springframework.content.commons.store.ContentStore) delegate).setContent(entity, propertyPath, actualContent, params); + } + catch (Exception e) { + throw e; + } + }); } - @Override - public Object setContent(Object property, PropertyPath propertyPath, InputStream content, long contentLen) { + public Object internalSetContent(Object property, PropertyPath propertyPath, InputStream content, Function invocation) { Object result = null; File contentCopy = null; @@ -163,12 +157,7 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { content = new FileInputStream(contentCopy); } - try { - result = castToDeprecatedContentStore(delegate).setContent(property, propertyPath, content, contentLen); - } - catch (Exception e) { - throw e; - } + 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); @@ -200,41 +189,38 @@ else if (contentCopyStream != null && contentCopyStream.isDirty()) { } @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, Resource resourceContent) { + return this.internalSetContent(entity, null, resourceContent, () -> { + try { + 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; + } + }); } @Override - public Object setContent(Object property, PropertyPath propertyPath, Resource resourceContent) { + 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; + } + }); + } + 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); @@ -244,13 +230,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); @@ -265,123 +245,154 @@ 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); + 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; + } + }); + } - if (contentStore != null) { - AfterUnsetContentEvent after = new AfterUnsetContentEvent(property, contentStore); - after.setResult(result); - publisher.publishEvent(after); - } - return result; + @Override + 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.store.ContentStore) { + int ordinal = params.getDisposition().ordinal(); + UnsetContentParams params1 = UnsetContentParams.builder() + .disposition(UnsetContentParams.Disposition.values()[ordinal]) + .build(); + 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) { + throw e; + } + }); + } @Override - public Object unsetContent(Object property, PropertyPath propertyPath) { + public Object unsetContent(Object entity, PropertyPath propertyPath, UnsetContentParams params) { + 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, 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 ((org.springframework.content.commons.repository.ContentStore)(delegate)).unsetContent(entity, propertyPath, params1); + } + } + 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; } @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) { @@ -389,7 +400,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) { @@ -402,117 +413,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); @@ -522,13 +477,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); @@ -563,7 +512,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; @@ -596,7 +549,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; @@ -624,7 +581,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; @@ -652,7 +613,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; @@ -680,7 +645,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; @@ -695,13 +664,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/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/repository/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/ContentStore.java index a947695e6..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 @@ -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 @@ -33,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 new file mode 100644 index 000000000..ef469737e --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/repository/SetContentParams.java @@ -0,0 +1,19 @@ +package org.springframework.content.commons.repository; + +import lombok.Builder; +import lombok.Data; + +@Deprecated +@Data +@Builder +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/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/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/ContentStore.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/ContentStore.java index 30e177bd9..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 @@ -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 @@ -31,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/SetContentParams.java b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java new file mode 100644 index 000000000..1c26c0e64 --- /dev/null +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/store/SetContentParams.java @@ -0,0 +1,18 @@ +package org.springframework.content.commons.store; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +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/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/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. * 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-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..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,9 +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.Store; +import org.springframework.content.commons.store.*; import org.springframework.core.io.Resource; import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; @@ -143,6 +141,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 @@ -155,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 f9183c5d1..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 @@ -6,6 +6,8 @@ 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.repository.UnsetContentParams; import org.springframework.content.commons.store.Store; import org.springframework.content.commons.store.factory.AbstractStoreFactoryBean; import org.springframework.core.io.Resource; @@ -98,6 +100,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 @@ -110,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 aa3e4824d..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 @@ -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,43 @@ 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) { + 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); + } - ContentProperty contentProperty = getMappingContext().getContentProperty(o.getClass(), propertyPath.getName()); + + @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"); + + 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, params); + } else if (delegate != null) { + 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(o, this.encryptionKeyContentProperty, null); + contentProperty.setCustomProperty(entity, this.encryptionKeyContentProperty, null); return entityToReturn; } @@ -145,22 +212,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 +245,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 +277,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"); - GetResourceParams ctrParams = rewriteParamsForCTR(params); - Resource r = delegate.getResource(o, propertyPath, ctrParams); + Resource r = storeDelegate.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); + } + } + + 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"); + + Resource r = delegate.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); @@ -233,6 +339,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 +359,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 +409,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..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 @@ -3,6 +3,8 @@ 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.content.commons.repository.UnsetContentParams; import org.springframework.core.io.Resource; import java.io.InputStream; @@ -14,8 +16,13 @@ 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); + S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params); + S unsetContent(S entity, PropertyPath propertyPath, org.springframework.content.commons.store.UnsetContentParams params); } 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..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 @@ -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 { @@ -217,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/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-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..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 @@ -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; @@ -26,7 +28,10 @@ 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.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; @@ -41,7 +46,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); @@ -174,8 +179,7 @@ public boolean matches(Field field) { @Override @Transactional public S setContent(S entity, InputStream content) { - - Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class); + Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class); if (contentId == null) { Serializable newId = UUID.randomUUID().toString(); @@ -234,6 +238,21 @@ 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()); + } + + @Override + public S setContent(S entity, PropertyPath propertyPath, InputStream content, org.springframework.content.commons.repository.SetContentParams params) { + SetContentParams params1 = SetContentParams.builder() + .contentLength(params.getContentLength()) + .overwriteExistingContent(params.isOverwriteExistingContent()) + .build(); + return this.setContent(entity, propertyPath, content, params1); + } + + @Transactional + @Override + public S setContent(S property, PropertyPath propertyPath, InputStream content, SetContentParams params) { ContentProperty contentProperty = this.mappingContext.getContentProperty(property.getClass(), propertyPath.getName()); if (contentProperty == null) { @@ -241,7 +260,7 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, } Object contentId = contentProperty.getContentId(property); - if (contentId == null) { + if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -281,7 +300,7 @@ public S setContent(S property, PropertyPath propertyPath, InputStream content, } try { - long len = contentLen; + long len = params.getContentLength(); if (len == -1L) { len = resource.contentLength(); } @@ -387,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/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 6b5e31aa6..fc7c0530f 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -29,9 +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.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; @@ -303,14 +301,18 @@ public class FilesystemStoreIT { }); Context("when content is updated", () -> { - 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 +320,19 @@ 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(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)); + + int i=0; }); }); @@ -349,7 +359,30 @@ public class FilesystemStoreIT { }); }); - Context("when content is deleted", () -> { + 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().disposition(SetContentParams.ContentDisposition.CreateNew).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 unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -373,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/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-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..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 @@ -20,7 +20,10 @@ 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.repository.UnsetContentParams; 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 +48,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 +293,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.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -325,7 +344,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(); } @@ -438,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/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 68f8fcdc9..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 @@ -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; @@ -32,9 +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.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; @@ -83,7 +79,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"); } { @@ -299,6 +295,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,7 +369,28 @@ public class GCPStorageIT { }); }); - Context("when content is deleted", () -> { + 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().disposition(SetContentParams.ContentDisposition.CreateNew).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 unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -388,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/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-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..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 @@ -21,7 +21,10 @@ 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.repository.UnsetContentParams; 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; @@ -40,7 +43,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); @@ -239,12 +242,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.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -277,7 +296,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; } @@ -333,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.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); + } + + @Transactional + @Override + public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { // TODO @@ -343,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) { @@ -357,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 1bde9fe5e..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 @@ -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,8 @@ 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.content.commons.store.UnsetContentParams; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; @@ -199,6 +198,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().disposition(SetContentParams.ContentDisposition.CreateNew).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(); @@ -241,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 0462384c2..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 @@ -25,6 +25,8 @@ 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.repository.UnsetContentParams; import org.springframework.content.jpa.config.EnableJpaContentRepositories; import org.springframework.content.jpa.config.EnableJpaStores; import org.springframework.content.jpa.io.BlobResourceLoader; @@ -272,6 +274,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 @@ -284,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/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-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..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 @@ -19,7 +19,10 @@ 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.repository.UnsetContentParams; 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; @@ -37,7 +40,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); @@ -243,14 +246,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.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -277,7 +293,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(); } @@ -411,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())); @@ -427,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); @@ -458,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 83b951de8..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 @@ -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; @@ -27,9 +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.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; @@ -87,6 +79,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 +353,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().disposition(SetContentParams.ContentDisposition.CreateNew).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 unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -376,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())); } @@ -386,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/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-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..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 @@ -22,7 +22,9 @@ 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.repository.UnsetContentParams; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.StoreInterfaceUtils; import org.springframework.content.rest.RestResource; @@ -186,6 +188,30 @@ 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()); + } + + 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)) { + SetContentParams params = SetContentParams.builder().build(); + + // if available use the original content length + if (headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { + params.setContentLength(headers.getContentLength()); + } + + int ordinal = config.getSetContentDisposition().ordinal(); + params.setDisposition(SetContentParams.ContentDisposition.values()[ordinal]); + + argsList.add(params); } try { @@ -219,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 == 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(org.springframework.content.commons.store.UnsetContentParams.Disposition.values()[ordinal]); + + unsetParams = params; + } 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(); + 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); @@ -338,18 +381,29 @@ 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); - this.unsetContentMethods = calculateExports(UNSETCONTENT_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); + } } 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 68c32fa45..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 @@ -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); @@ -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 d8a5189ad..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; @@ -49,7 +48,10 @@ @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 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; private static final URI NO_URI = URI.create(""); @@ -64,6 +66,9 @@ 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 SetContentDisposition setContentDisposition = SETCONTENT_CONTENT_DISPOSITION_DEFAULT; + private UnsetContentDisposition unsetContentDisposition = UNSETCONTENT_CONTENT_DISPOSITION_DEFAULT; private ConverterRegistry converters = new DefaultConversionService(); private Map, DomainTypeConfig> domainTypeConfigMap = new HashMap<>(); @@ -100,6 +105,30 @@ public void setShortcutLinks(boolean shortcutLinks) { this.shortcutLinks = shortcutLinks; } + public boolean overwriteExistingContent() { + return this.overwriteExistingContent; + } + + 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 new file mode 100644 index 000000000..075558db4 --- /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 { + 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 +} 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..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 @@ -25,6 +25,9 @@ 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.repository.UnsetContentParams; +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 +102,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 +193,7 @@ public interface UnexportedContentStore extends FilesystemContentStore { 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); 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) 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. 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..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 @@ -22,7 +22,10 @@ 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.repository.UnsetContentParams; 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 +52,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); @@ -319,14 +322,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.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { Serializable newId = UUID.randomUUID().toString(); @@ -356,7 +375,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(); } @@ -464,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/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..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 @@ -25,6 +25,8 @@ 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.commons.store.UnsetContentParams; import org.springframework.content.s3.config.EnableS3Stores; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -42,9 +44,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; @@ -59,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) @@ -107,6 +108,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 +351,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,7 +410,26 @@ public class S3StoreIT { }); }); - Context("when content is deleted", () -> { + 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().disposition(SetContentParams.ContentDisposition.CreateNew).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 unset", () -> { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); @@ -412,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()); }); }); 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) {