From 394780c8ade1f496184d882a7b726a460ef32020 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Wed, 12 Jul 2023 23:13:29 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20content=20property=20now=20uses=20s?= =?UTF-8?q?etAutoGrowNestedPaths=20when=20setting=20c=E2=80=A6=20(#1507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: content property now uses setAutoGrowNestedPaths when setting content to provide better support for null embedded content objects - filesystem storage only * test: add @embedded content tests for azure storage * test: add @embedded content tests for gcp storage * test: add @embedded content tests for s3 storage Fixes #1480 --- .../azure/store/DefaultAzureStorageImpl.java | 14 +- .../content/azure/it/AzureStorageIT.java | 112 +++++++--- .../mappingcontext/ContentProperty.java | 77 +++++-- .../ContentPropertyMappingContextVisitor.java | 3 + .../mappingcontext/ClassWalkerTest.java | 18 +- .../fs/store/DefaultFilesystemStoreImpl.java | 16 +- .../test/java/it/store/FilesystemStoreIT.java | 210 +++++++++++------- .../gcs/store/DefaultGCPStorageImpl.java | 4 +- .../content/gcs/it/GCPStorageIT.java | 111 ++++++--- .../content/s3/store/DefaultS3StoreImpl.java | 13 +- .../content/s3/it/S3StoreIT.java | 67 +++++- 11 files changed, 452 insertions(+), 193 deletions(-) diff --git a/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java b/spring-content-azure-storage/src/main/java/internal/org/springframework/content/azure/store/DefaultAzureStorageImpl.java index 34b0924a7..d6726684f 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 @@ -493,11 +493,12 @@ public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams pa } // 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( + if (resource != null) { + 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() @@ -509,7 +510,8 @@ public boolean matches(TypeDescriptor descriptor) { } }); - property.setContentLength(entity, 0); + property.setContentLength(entity, 0); + } return entity; } 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 18dedd1cb..df9a40641 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,27 +1,14 @@ package internal.org.springframework.content.azure.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; -import static org.hamcrest.Matchers.greaterThan; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -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; -import jakarta.persistence.Id; -import javax.sql.DataSource; - +import com.azure.core.http.rest.PagedIterable; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +import jakarta.persistence.*; +import lombok.*; +import net.bytebuddy.utility.RandomString; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.junit.Assert; @@ -51,19 +38,19 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; - import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; -import com.azure.core.http.rest.PagedIterable; -import com.azure.storage.blob.BlobContainerClient; -import com.azure.storage.blob.BlobServiceClientBuilder; -import com.azure.storage.blob.models.BlobItem; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +import javax.sql.DataSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import net.bytebuddy.utility.RandomString; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; @RunWith(Ginkgo4jRunner.class) @Ginkgo4jConfiguration(threads=1) @@ -90,6 +77,9 @@ public class AzureStorageIT { private TestEntityRepository repo; private TestEntityStore store; + private EmbeddedRepository embeddedRepo; + private EmbeddedStore embeddedStore; + private String resourceLocation; { @@ -103,6 +93,9 @@ public class AzureStorageIT { repo = context.getBean(TestEntityRepository.class); store = context.getBean(TestEntityStore.class); + embeddedRepo = context.getBean(EmbeddedRepository.class); + embeddedStore = context.getBean(EmbeddedStore.class); + RandomString random = new RandomString(5); resourceLocation = random.nextString(); }); @@ -521,6 +514,29 @@ public class AzureStorageIT { // }); // }); + Context("@Embedded content", () -> { + Context("given a entity with a null embedded content object", () -> { + It("should return null when content is fetched", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + assertThat(embeddedStore.getContent(entity, PropertyPath.from("content")), is(nullValue())); + }); + + It("should be successful when content is set", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + embeddedStore.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + try (InputStream is = embeddedStore.getContent(entity, PropertyPath.from("content"))) { + assertThat(IOUtils.contentEquals(is, new ByteArrayInputStream("Hello Spring Content World!".getBytes())), is(true)); + } + }); + + It("should return null when content is unset", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + EntityWithEmbeddedContent expected = new EntityWithEmbeddedContent(entity.getId(), entity.getContent()); + assertThat(embeddedStore.unsetContent(entity, PropertyPath.from("content")), is(expected)); + int i = 0; + }); + }); + }); }); }); } @@ -637,5 +653,33 @@ public interface SharedIdStore extends ContentStore {} // public interface SharedSpringIdStore extends ContentStore {} + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name="entity_with_embedded") + public static class EntityWithEmbeddedContent { + + @Id + private String id = UUID.randomUUID().toString(); + + @Embedded + private EmbeddedContent content; + } + + @Embeddable + @NoArgsConstructor + @Data + public static class EmbeddedContent { + + @ContentId + private String contentId; + + @ContentLength + private Long contentLen; + } + + public interface EmbeddedRepository extends JpaRepository {} + public interface EmbeddedStore extends ContentStore {} } -// EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentProperty.java b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentProperty.java index e806f1170..1319d5c20 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentProperty.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentProperty.java @@ -3,11 +3,13 @@ import org.apache.commons.lang.StringUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.NullValueInNestedPathException; import org.springframework.core.convert.TypeDescriptor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.springframework.util.Assert; @Getter @Setter @@ -16,6 +18,7 @@ public class ContentProperty { private String contentPropertyPath; private String contentIdPropertyPath; + private TypeDescriptor contentIdType; private String contentLengthPropertyPath; private String mimeTypePropertyPath; private String originalFileNamePropertyPath; @@ -23,14 +26,18 @@ public class ContentProperty { public Object getCustomProperty(Object entity, String propertyName) { String customContentPropertyPath = contentPropertyPath + StringUtils.capitalize(propertyName); - BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyValue(customContentPropertyPath); + BeanWrapper wrapper = getBeanWrapperForRead(entity); + try { + return wrapper.getPropertyValue(customContentPropertyPath); + } catch (NullValueInNestedPathException nvinpe) { + return null; + } } public void setCustomProperty(Object entity, String propertyName, Object value) { String customContentPropertyPath = contentPropertyPath + StringUtils.capitalize(propertyName); - BeanWrapper wrapper = new BeanWrapperImpl(entity); + BeanWrapper wrapper = getBeanWrapperForWrite(entity); wrapper.setPropertyValue(customContentPropertyPath, value); } @@ -39,8 +46,12 @@ public Object getContentId(Object entity) { return null; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyValue(contentIdPropertyPath); + BeanWrapper wrapper = getBeanWrapperForRead(entity); + try { + return wrapper.getPropertyValue(contentIdPropertyPath); + } catch (NullValueInNestedPathException nvinpe) { + return null; + } } public void setContentId(Object entity, Object value, Condition condition) { @@ -48,7 +59,7 @@ public void setContentId(Object entity, Object value, Condition condition) { return; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); + BeanWrapper wrapper = getBeanWrapperForWrite(entity); if (condition != null) { TypeDescriptor t = wrapper.getPropertyTypeDescriptor(contentIdPropertyPath); @@ -61,12 +72,17 @@ public void setContentId(Object entity, Object value, Condition condition) { } public TypeDescriptor getContentIdType(Object entity) { - if (contentIdPropertyPath == null) { - return null; - } + Assert.notNull(this.contentIdType, "content id property type must be set"); + return this.contentIdType; + } - BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyTypeDescriptor(contentIdPropertyPath); + public TypeDescriptor getContentIdType() { + Assert.notNull(this.contentIdType, "content id property type must be set"); + return this.contentIdType; + } + + public void setContentIdType(TypeDescriptor descriptor) { + this.contentIdType = descriptor; } public Object getContentLength(Object entity) { @@ -74,8 +90,12 @@ public Object getContentLength(Object entity) { return 0L; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyValue(contentLengthPropertyPath); + BeanWrapper wrapper = getBeanWrapperForRead(entity); + try { + return wrapper.getPropertyValue(contentLengthPropertyPath); + } catch (NullValueInNestedPathException nvinpe) { + return null; + } } public void setContentLength(Object entity, Object value) { @@ -83,7 +103,7 @@ public void setContentLength(Object entity, Object value) { return; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); + BeanWrapper wrapper = getBeanWrapperForWrite(entity); wrapper.setPropertyValue(contentLengthPropertyPath, value); } @@ -92,8 +112,12 @@ public Object getMimeType(Object entity) { return null; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyValue(mimeTypePropertyPath); + BeanWrapper wrapper = getBeanWrapperForRead(entity); + try { + return wrapper.getPropertyValue(mimeTypePropertyPath); + } catch (NullValueInNestedPathException nvinpe) { + return null; + } } public void setMimeType(Object entity, Object value) { @@ -101,7 +125,7 @@ public void setMimeType(Object entity, Object value) { return; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); + BeanWrapper wrapper = getBeanWrapperForWrite(entity); wrapper.setPropertyValue(mimeTypePropertyPath, value); } @@ -110,7 +134,7 @@ public void setOriginalFileName(Object entity, Object value) { return; } - BeanWrapper wrapper = new BeanWrapperImpl(entity); + BeanWrapper wrapper = getBeanWrapperForWrite(entity); wrapper.setPropertyValue(originalFileNamePropertyPath, value); } @@ -119,7 +143,22 @@ public Object getOriginalFileName(Object entity) { return null; } + BeanWrapper wrapper = getBeanWrapperForRead(entity); + try { + return wrapper.getPropertyValue(originalFileNamePropertyPath); + } catch (NullValueInNestedPathException nvinpe) { + return null; + } + } + + private BeanWrapper getBeanWrapperForRead(Object entity) { + BeanWrapper wrapper = new BeanWrapperImpl(entity); + return wrapper; + } + + private BeanWrapper getBeanWrapperForWrite(Object entity) { BeanWrapper wrapper = new BeanWrapperImpl(entity); - return wrapper.getPropertyValue(originalFileNamePropertyPath); + wrapper.setAutoGrowNestedPaths(true); + return wrapper; } } diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyMappingContextVisitor.java b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyMappingContextVisitor.java index 734e64b00..d713d16c6 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyMappingContextVisitor.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyMappingContextVisitor.java @@ -13,6 +13,7 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.annotations.OriginalFileName; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.StringUtils; import lombok.Getter; @@ -82,6 +83,7 @@ public boolean visitClassEnd(String path, Class klazz) { if (property.getContentIdPropertyPath() != null) { contentProperty.setContentPropertyPath(property.getContentPropertyPath()); contentProperty.setContentIdPropertyPath(property.getContentIdPropertyPath()); + contentProperty.setContentIdType(property.getContentIdType()); } if (property.getContentLengthPropertyPath() != null) { contentProperty.setContentLengthPropertyPath(property.getContentLengthPropertyPath()); @@ -122,6 +124,7 @@ public boolean visitField(String path, Class klazz, Field f) { } updateContentProperty(property::setContentPropertyPath, fullyQualify(path, this.propertyName(f.getName()), this.getContentPropertySeparator())); updateContentProperty(property::setContentIdPropertyPath, fullyQualify(path, f.getName(), this.getContentPropertySeparator())); + property.setContentIdType(TypeDescriptor.valueOf(f.getType())); } } else if (f.isAnnotationPresent(ContentLength.class)) { LOGGER.trace(String.format("%s.%s is @ContentLength", f.getDeclaringClass().getCanonicalName(), f.getName())); diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java b/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java index 8021dd137..7009ee4e2 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java @@ -20,6 +20,7 @@ import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.annotations.OriginalFileName; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.util.ReflectionUtils; @@ -47,6 +48,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("content"); expectedProperty.setContentIdPropertyPath("contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(String.class)); expectedProperty.setContentLengthPropertyPath("contentLength"); expectedProperty.setMimeTypePropertyPath("contentMimeType"); expectedProperty.setOriginalFileNamePropertyPath("contentOriginalFileName"); @@ -55,6 +57,7 @@ public class ClassWalkerTest { ContentProperty expectedSubClassProperty = new ContentProperty(); expectedSubClassProperty.setContentPropertyPath("child.content"); expectedSubClassProperty.setContentIdPropertyPath("child.contentId"); + expectedSubClassProperty.setContentIdType(TypeDescriptor.valueOf(String.class)); expectedSubClassProperty.setContentLengthPropertyPath("child.contentLength"); expectedSubClassProperty.setMimeTypePropertyPath("child.contentMimeType"); expectedSubClassProperty.setOriginalFileNamePropertyPath("child.contentOriginalFileName"); @@ -63,6 +66,7 @@ public class ClassWalkerTest { ContentProperty expectedSubSubClassProperty = new ContentProperty(); expectedSubSubClassProperty.setContentPropertyPath("child.subChild.content"); expectedSubSubClassProperty.setContentIdPropertyPath("child.subChild.contentId"); + expectedSubSubClassProperty.setContentIdType(TypeDescriptor.valueOf(String.class)); expectedSubSubClassProperty.setContentLengthPropertyPath("child.subChild.contentLength"); expectedSubSubClassProperty.setMimeTypePropertyPath("child.subChild.contentMimeType"); expectedSubSubClassProperty.setOriginalFileNamePropertyPath("child.subChild.contentOriginalFileName"); @@ -71,6 +75,7 @@ public class ClassWalkerTest { ContentProperty expectedCamelCaseProperty = new ContentProperty(); expectedCamelCaseProperty.setContentPropertyPath("camelCaseProperty.camelCaseProperty"); expectedCamelCaseProperty.setContentIdPropertyPath("camelCaseProperty.camelCasePropertyId"); + expectedCamelCaseProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedCamelCaseProperty.setContentLengthPropertyPath("camelCaseProperty.camelCasePropertyLen"); expectedCamelCaseProperty.setMimeTypePropertyPath("camelCaseProperty.camelCasePropertyMimeType"); expectedCamelCaseProperty.setOriginalFileNamePropertyPath("camelCaseProperty.camelCasePropertyOriginalFileName"); @@ -79,6 +84,7 @@ public class ClassWalkerTest { ContentProperty expectedOtherCamelCaseProperty = new ContentProperty(); expectedOtherCamelCaseProperty.setContentPropertyPath("otherCamelCaseProperty.camelCaseProperty"); expectedOtherCamelCaseProperty.setContentIdPropertyPath("otherCamelCaseProperty.camelCasePropertyIds"); + expectedOtherCamelCaseProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedOtherCamelCaseProperty.setContentLengthPropertyPath("otherCamelCaseProperty.camelCasePropertyLens"); expectedOtherCamelCaseProperty.setMimeTypePropertyPath("otherCamelCaseProperty.camelCasePropertyMimetypes"); expectedOtherCamelCaseProperty.setOriginalFileNamePropertyPath("otherCamelCaseProperty.camelCasePropertyFilenames"); @@ -95,6 +101,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty2 = new ContentProperty(); expectedProperty2.setContentPropertyPath("content"); expectedProperty2.setContentIdPropertyPath("contentId"); + expectedProperty2.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty2.setContentLengthPropertyPath("len"); expectedProperty2.setMimeTypePropertyPath("mimeType"); expectedProperty2.setOriginalFileNamePropertyPath("originalFileName"); @@ -111,6 +118,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty2 = new ContentProperty(); expectedProperty2.setContentPropertyPath("content"); expectedProperty2.setContentIdPropertyPath("contentId"); + expectedProperty2.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty2.setContentLengthPropertyPath("contentLen"); expectedProperty2.setMimeTypePropertyPath("contentMimeType"); expectedProperty2.setOriginalFileNamePropertyPath("contentOriginalFileName"); @@ -127,6 +135,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("child.content"); expectedProperty.setContentIdPropertyPath("child.contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty.setContentLengthPropertyPath("child.len"); expectedProperty.setMimeTypePropertyPath("child.mimeType"); expectedProperty.setOriginalFileNamePropertyPath("child.originalFileName"); @@ -153,6 +162,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("content"); expectedProperty.setContentIdPropertyPath("contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(String.class)); expectedProperty.setContentLengthPropertyPath("contentLength"); expectedProperty.setMimeTypePropertyPath("contentMimeType"); expectedProperty.setOriginalFileNamePropertyPath("contentOriginalFileName"); @@ -160,7 +170,7 @@ public class ClassWalkerTest { }); }); - Context("given a class with multple child content property objects", () -> { + Context("given a class with multiple child content property objects", () -> { It("should return two content properties", () -> { ContentPropertyMappingContextVisitor visitor = new ContentPropertyMappingContextVisitor("/", "."); ClassWalker walker = new ClassWalker(visitor); @@ -169,6 +179,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("child1.content"); expectedProperty.setContentIdPropertyPath("child1.contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty.setContentLengthPropertyPath("child1.len"); expectedProperty.setMimeTypePropertyPath("child1.mimeType"); expectedProperty.setOriginalFileNamePropertyPath("child1.originalFileName"); @@ -177,6 +188,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty3 = new ContentProperty(); expectedProperty3.setContentPropertyPath("child2.content"); expectedProperty3.setContentIdPropertyPath("child2.contentId"); + expectedProperty3.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty3.setContentLengthPropertyPath("child2.len"); expectedProperty3.setMimeTypePropertyPath("child2.mimeType"); expectedProperty3.setOriginalFileNamePropertyPath("child2.originalFileName"); @@ -193,6 +205,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("child.content"); expectedProperty.setContentIdPropertyPath("child.contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty.setContentLengthPropertyPath("child.contentLen"); expectedProperty.setMimeTypePropertyPath("child.contentMimeType"); expectedProperty.setOriginalFileNamePropertyPath("child.contentOriginalFileName"); @@ -201,6 +214,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty3 = new ContentProperty(); expectedProperty3.setContentPropertyPath("child.preview"); expectedProperty3.setContentIdPropertyPath("child.previewId"); + expectedProperty3.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty3.setContentLengthPropertyPath("child.previewLen"); expectedProperty3.setMimeTypePropertyPath("child.previewMimeType"); expectedProperty3.setOriginalFileNamePropertyPath("child.previewOriginalFileName"); @@ -217,6 +231,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty = new ContentProperty(); expectedProperty.setContentPropertyPath("child.child.content"); expectedProperty.setContentIdPropertyPath("child.child.contentId"); + expectedProperty.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty.setContentLengthPropertyPath("child.child.contentLen"); expectedProperty.setMimeTypePropertyPath("child.child.contentMimeType"); expectedProperty.setOriginalFileNamePropertyPath("child.child.contentOriginalFileName"); @@ -225,6 +240,7 @@ public class ClassWalkerTest { ContentProperty expectedProperty2 = new ContentProperty(); expectedProperty2.setContentPropertyPath("child.child.preview"); expectedProperty2.setContentIdPropertyPath("child.child.previewId"); + expectedProperty2.setContentIdType(TypeDescriptor.valueOf(UUID.class)); expectedProperty2.setContentLengthPropertyPath("child.child.previewLen"); expectedProperty2.setMimeTypePropertyPath("child.child.previewMimeType"); expectedProperty2.setOriginalFileNamePropertyPath("child.child.previewOriginalFileName"); 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 93ab38848..8d1131281 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 @@ -9,6 +9,7 @@ import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Type; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -159,10 +160,10 @@ public boolean matches(Field field) { @Override public void unassociate(S entity, PropertyPath propertyPath) { - setContentId(entity, propertyPath, null, new Condition() { + setContentId(entity, propertyPath, null, new org.springframework.content.commons.mappingcontext.Condition() { @Override - public boolean matches(Field field) { - for (Annotation annotation : field.getAnnotations()) { + public boolean matches(TypeDescriptor descriptor) { + for (Annotation annotation : descriptor.getAnnotations()) { if ("javax.persistence.Id".equals( annotation.annotationType().getCanonicalName()) || "org.springframework.data.annotation.Id" @@ -435,9 +436,9 @@ public S unsetContent(S property, PropertyPath propertyPath, UnsetContentParams } // reset content fields - unassociate(property, propertyPath); + if (resource != null) {unassociate(property, propertyPath); ContentProperty contentProperty = this.mappingContext.getContentProperty(property.getClass(), propertyPath.getName()); - contentProperty.setContentLength(property, 0); + contentProperty.setContentLength(property, 0);} return property; } @@ -454,16 +455,15 @@ private Object convertToExternalContentIdType(S property, Object contentId) { return contentId.toString(); } - private void setContentId(S entity, PropertyPath propertyPath, SID contentId, Condition condition) { + private void setContentId(S entity, PropertyPath propertyPath, SID contentId, org.springframework.content.commons.mappingcontext.Condition condition) { Assert.notNull(entity, "entity must not be null"); Assert.notNull(propertyPath, "propertyPath must not be null"); - BeanWrapper wrapper = new BeanWrapperImpl(entity); 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())); } - wrapper.setPropertyValue(property.getContentIdPropertyPath(), contentId); + property.setContentId(entity, contentId, condition); } } \ No newline at end of file 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 fc7c0530f..39ed531b7 100644 --- a/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java +++ b/spring-content-fs/src/test/java/it/store/FilesystemStoreIT.java @@ -1,25 +1,12 @@ package it.store; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.UUID; - -import javax.sql.DataSource; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import net.bytebuddy.utility.RandomString; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.junit.Assert; @@ -48,10 +35,19 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +import javax.sql.DataSource; +import java.io.*; +import java.nio.file.Files; +import java.util.UUID; -import net.bytebuddy.utility.RandomString; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.*; @RunWith(Ginkgo4jRunner.class) @Ginkgo4jConfiguration(threads=1) @@ -66,6 +62,9 @@ public class FilesystemStoreIT { private TestEntityRepository repo; private TestEntityStore store; + private EmbeddedRepository embeddedRepo; + private EmbeddedStore embeddedStore; + private String resourceLocation; { @@ -81,6 +80,9 @@ public class FilesystemStoreIT { RandomString random = new RandomString(5); resourceLocation = random.nextString(); + + embeddedRepo = context.getBean(EmbeddedRepository.class); + embeddedStore = context.getBean(EmbeddedStore.class); }); AfterEach(() -> { @@ -273,31 +275,33 @@ public class FilesystemStoreIT { entity = repo.save(entity); store.setContent(entity, new ByteArrayInputStream("Hello Spring Content World!".getBytes())); - store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); }); It("should be able to store new content", () -> { - // content + // content try (InputStream content = store.getContent(entity)) { assertThat(IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring Content World!".getBytes()), content), is(true)); - } catch (IOException ioe) {} + } catch (IOException ioe) { + } //rendition - try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { - assertThat(IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring Content World!".getBytes()), content), is(true)); - } catch (IOException ioe) {} + try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { + assertThat(IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring Content World!".getBytes()), content), is(true)); + } catch (IOException ioe) { + } }); It("should have content metadata", () -> { - // content + // content assertThat(entity.getContentId(), is(notNullValue())); assertThat(entity.getContentId().trim().length(), greaterThan(0)); Assert.assertEquals(entity.getContentLen(), 27L); //rendition - assertThat(entity.getRenditionId(), is(notNullValue())); - assertThat(entity.getRenditionId().trim().length(), greaterThan(0)); - Assert.assertEquals(entity.getRenditionLen(), 40L); + assertThat(entity.getRenditionId(), is(notNullValue())); + assertThat(entity.getRenditionId().trim().length(), greaterThan(0)); + Assert.assertEquals(entity.getRenditionLen(), 40L); }); Context("when content is updated", () -> { @@ -307,7 +311,6 @@ public class FilesystemStoreIT { 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())); entity = repo.save(entity); @@ -339,11 +342,11 @@ public class FilesystemStoreIT { Context("when content is updated with shorter content", () -> { BeforeEach(() -> { store.setContent(entity, new ByteArrayInputStream("Hello Spring World!".getBytes())); - store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Spring World!".getBytes())); + store.setContent(entity, PropertyPath.from("rendition"), new ByteArrayInputStream("Hello Spring World!".getBytes())); entity = repo.save(entity); }); It("should store only the new content", () -> { - //content + //content boolean matches = false; try (InputStream content = store.getContent(entity)) { matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring World!".getBytes()), content); @@ -351,11 +354,11 @@ public class FilesystemStoreIT { } //rendition - matches = false; - try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { - matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring World!".getBytes()), content); - assertThat(matches, is(true)); - } + matches = false; + try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { + matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring World!".getBytes()), content); + assertThat(matches, is(true)); + } }); }); @@ -386,12 +389,12 @@ public class FilesystemStoreIT { BeforeEach(() -> { resourceLocation = entity.getContentId().toString(); entity = store.unsetContent(entity); - entity = store.unsetContent(entity, PropertyPath.from("rendition")); + entity = store.unsetContent(entity, PropertyPath.from("rendition")); entity = repo.save(entity); }); It("should have no content", () -> { - //content + //content try (InputStream content = store.getContent(entity)) { assertThat(content, is(Matchers.nullValue())); } @@ -400,12 +403,12 @@ public class FilesystemStoreIT { Assert.assertEquals(entity.getContentLen(), 0); //rendition - try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { - assertThat(content, is(Matchers.nullValue())); - } + try (InputStream content = store.getContent(entity, PropertyPath.from("rendition"))) { + assertThat(content, is(Matchers.nullValue())); + } - assertThat(entity.getContentId(), is(Matchers.nullValue())); - Assert.assertEquals(entity.getContentLen(), 0); + 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)); @@ -434,37 +437,37 @@ public class FilesystemStoreIT { }); Context("when an invalid property path is used to setContent", () -> { - It("should throw an error", () -> { - try { - store.setContent(entity, PropertyPath.from("does.not.exist"), new ByteArrayInputStream("foo".getBytes())); - } catch (Exception sae) { - this.e = sae; - } - assertThat(e, is(instanceOf(StoreAccessException.class))); - }); + It("should throw an error", () -> { + try { + store.setContent(entity, PropertyPath.from("does.not.exist"), new ByteArrayInputStream("foo".getBytes())); + } catch (Exception sae) { + this.e = sae; + } + assertThat(e, is(instanceOf(StoreAccessException.class))); + }); + }); + + Context("when an invalid property path is used to getContent", () -> { + It("should throw an error", () -> { + try { + store.getContent(entity, PropertyPath.from("does.not.exist")); + } catch (Exception sae) { + this.e = sae; + } + assertThat(e, is(instanceOf(StoreAccessException.class))); + }); }); - Context("when an invalid property path is used to getContent", () -> { - It("should throw an error", () -> { - try { - store.getContent(entity, PropertyPath.from("does.not.exist")); - } catch (Exception sae) { - this.e = sae; - } - assertThat(e, is(instanceOf(StoreAccessException.class))); - }); - }); - - Context("when an invalid property path is used to unsetContent", () -> { - It("should throw an error", () -> { - try { - store.unsetContent(entity, PropertyPath.from("does.not.exist")); - } catch (Exception sae) { - this.e = sae; - } - assertThat(e, is(instanceOf(StoreAccessException.class))); - }); - }); + Context("when an invalid property path is used to unsetContent", () -> { + It("should throw an error", () -> { + try { + store.unsetContent(entity, PropertyPath.from("does.not.exist")); + } catch (Exception sae) { + this.e = sae; + } + assertThat(e, is(instanceOf(StoreAccessException.class))); + }); + }); Context("when content is deleted and the id field is shared with javax id", () -> { @@ -482,6 +485,30 @@ public class FilesystemStoreIT { assertThat(sharedIdContentIdEntity.getContentLen(), is(0L)); }); }); + + Context("@Embedded content", () -> { + Context("given a entity with a null embedded content object", () -> { + It("should return null when content is fetched", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + assertThat(embeddedStore.getContent(entity, PropertyPath.from("content")), is(nullValue())); + }); + + It("should be successful when content is set", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + embeddedStore.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + try (InputStream is = embeddedStore.getContent(entity, PropertyPath.from("content"))) { + assertThat(IOUtils.contentEquals(is, new ByteArrayInputStream("Hello Spring Content World!".getBytes())), is(true)); + } + }); + + It("should return null when content is unset", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + EntityWithEmbeddedContent expected = new EntityWithEmbeddedContent(entity.getId(), entity.getContent()); + assertThat(embeddedStore.unsetContent(entity, PropertyPath.from("content")), is(expected)); + int i = 0; + }); + }); + }); }); }); } @@ -654,4 +681,33 @@ public void setContentLen(long contentLen) { public interface SharedIdRepository extends JpaRepository {} public interface SharedIdStore extends ContentStore {} + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name="entity_with_embedded") + public static class EntityWithEmbeddedContent { + + @Id + private String id = UUID.randomUUID().toString(); + + @Embedded + private EmbeddedContent content; + } + + @Embeddable + @NoArgsConstructor + @Data + public static class EmbeddedContent { + + @ContentId + private String contentId; + + @ContentLength + private Long contentLen; + } + + public interface EmbeddedRepository extends JpaRepository {} + public interface EmbeddedStore extends ContentStore {} } 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 14acd71ac..25c0ec339 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 @@ -491,7 +491,7 @@ public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.c } // reset content fields - property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { + if (resource != null) {property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { @Override public boolean matches(TypeDescriptor descriptor) { for (Annotation annotation : descriptor.getAnnotations()) { @@ -506,7 +506,7 @@ public boolean matches(TypeDescriptor descriptor) { return true; } }); - property.setContentLength(entity, 0); + property.setContentLength(entity, 0);} return entity; } 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 de6aae140..5cef39c83 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,27 +1,16 @@ package internal.org.springframework.content.gcs.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; -import static org.hamcrest.Matchers.greaterThan; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.UUID; - +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; +import com.google.api.gax.paging.Page; +import com.google.cloud.storage.Blob; import com.google.cloud.storage.BlobId; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import javax.sql.DataSource; - +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper; +import jakarta.persistence.*; +import junit.framework.Assert; +import lombok.*; +import net.bytebuddy.utility.RandomString; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.junit.Test; @@ -48,18 +37,17 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; -import com.google.api.gax.paging.Page; -import com.google.cloud.storage.Blob; -import com.google.cloud.storage.Storage; -import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper; +import javax.sql.DataSource; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; -import junit.framework.Assert; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import net.bytebuddy.utility.RandomString; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; @RunWith(Ginkgo4jRunner.class) @Ginkgo4jConfiguration(threads=1) @@ -76,6 +64,9 @@ public class GCPStorageIT { private TestEntityStore store; private Storage storage; + private EmbeddedRepository embeddedRepo; + private EmbeddedStore embeddedStore; + private String resourceLocation; static { @@ -94,6 +85,9 @@ public class GCPStorageIT { store = context.getBean(TestEntityStore.class); storage = context.getBean(Storage.class); + embeddedRepo = context.getBean(EmbeddedRepository.class); + embeddedStore = context.getBean(EmbeddedStore.class); + RandomString random = new RandomString(5); resourceLocation = random.nextString(); }); @@ -504,6 +498,30 @@ public class GCPStorageIT { // }); // }); + + Context("@Embedded content", () -> { + Context("given a entity with a null embedded content object", () -> { + It("should return null when content is fetched", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + assertThat(embeddedStore.getContent(entity, PropertyPath.from("content")), is(nullValue())); + }); + + It("should be successful when content is set", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + embeddedStore.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + try (InputStream is = embeddedStore.getContent(entity, PropertyPath.from("content"))) { + assertThat(IOUtils.contentEquals(is, new ByteArrayInputStream("Hello Spring Content World!".getBytes())), is(true)); + } + }); + + It("should return null when content is unset", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + EntityWithEmbeddedContent expected = new EntityWithEmbeddedContent(entity.getId(), entity.getContent()); + assertThat(embeddedStore.unsetContent(entity, PropertyPath.from("content")), is(expected)); + int i = 0; + }); + }); + }); }); }); } @@ -621,4 +639,33 @@ public interface SharedIdStore extends ContentStore {} // public interface SharedSpringIdStore extends ContentStore {} + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name="entity_with_embedded") + public static class EntityWithEmbeddedContent { + + @Id + private String id = UUID.randomUUID().toString(); + + @Embedded + private EmbeddedContent content; + } + + @Embeddable + @NoArgsConstructor + @Data + public static class EmbeddedContent { + + @ContentId + private String contentId; + + @ContentLength + private Long contentLen; + } + + public interface EmbeddedRepository extends JpaRepository {} + public interface EmbeddedStore extends ContentStore {} } 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 02f57534e..14ba8fd92 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 @@ -508,12 +508,14 @@ public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.c if (entity == null) return entity; + Resource resource = this.getResource(entity, propertyPath); + if (params.getDisposition().equals(org.springframework.content.commons.store.UnsetContentParams.Disposition.Remove)) { - deleteIfExists(entity, propertyPath); + deleteIfExists(entity, resource); } // reset content fields - property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { + if (resource != null) {property.setContentId(entity, null, new org.springframework.content.commons.mappingcontext.Condition() { @Override public boolean matches(TypeDescriptor descriptor) { for (Annotation annotation : descriptor.getAnnotations()) { @@ -528,7 +530,7 @@ public boolean matches(TypeDescriptor descriptor) { return true; } }); - property.setContentLength(entity, 0); + property.setContentLength(entity, 0);} return entity; } @@ -559,11 +561,8 @@ private void deleteIfExists(S entity) { } } - private void deleteIfExists(S entity, PropertyPath path) { - - Resource resource = this.getResource(entity, path); + private void deleteIfExists(S entity, Resource resource) { if (resource != null && resource.exists() && resource instanceof DeletableResource) { - try { ((DeletableResource)resource).delete(); } catch (Exception e) { 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 ed7bf1257..3bd4183af 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 @@ -4,14 +4,9 @@ import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner; import internal.org.springframework.content.s3.io.S3StoreResource; import internal.org.springframework.content.s3.io.SimpleStorageResource; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import junit.framework.Assert; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import net.bytebuddy.utility.RandomString; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; @@ -80,6 +75,9 @@ public class S3StoreIT { private TestEntityRepository repo; private TestEntityStore store; + private EmbeddedRepository embeddedRepo; + private EmbeddedStore embeddedStore; + private S3Client client; private String resourceLocation; @@ -96,6 +94,9 @@ public class S3StoreIT { store = context.getBean(TestEntityStore.class); client = context.getBean(S3Client.class); + embeddedRepo = context.getBean(EmbeddedRepository.class); + embeddedStore = context.getBean(EmbeddedStore.class); + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder() .bucket(BUCKET) .build(); @@ -549,6 +550,29 @@ public class S3StoreIT { // }); // }); + Context("@Embedded content", () -> { + Context("given a entity with a null embedded content object", () -> { + It("should return null when content is fetched", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + assertThat(embeddedStore.getContent(entity, PropertyPath.from("content")), is(nullValue())); + }); + + It("should be successful when content is set", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + embeddedStore.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + try (InputStream is = embeddedStore.getContent(entity, PropertyPath.from("content"))) { + assertThat(IOUtils.contentEquals(is, new ByteArrayInputStream("Hello Spring Content World!".getBytes())), is(true)); + } + }); + + It("should return null when content is unset", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + EntityWithEmbeddedContent expected = new EntityWithEmbeddedContent(entity.getId(), entity.getContent()); + assertThat(embeddedStore.unsetContent(entity, PropertyPath.from("content")), is(expected)); + int i = 0; + }); + }); + }); }); }); } @@ -671,4 +695,33 @@ public interface SharedIdStore extends ContentStore {} // public interface SharedSpringIdStore extends ContentStore {} + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name="entity_with_embedded") + public static class EntityWithEmbeddedContent { + + @Id + private String id = UUID.randomUUID().toString(); + + @Embedded + private EmbeddedContent content; + } + + @Embeddable + @NoArgsConstructor + @Data + public static class EmbeddedContent { + + @ContentId + private String contentId; + + @ContentLength + private Long contentLen; + } + + public interface EmbeddedRepository extends JpaRepository {} + public interface EmbeddedStore extends ContentStore {} } From bd76373573071982e69d29153f8ffe4c98f8d5cf Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 13 Jul 2023 20:35:36 -0700 Subject: [PATCH 2/2] test: add @embedded content tests for jpa storage --- .../jpa/store/DefaultJpaStoreImpl.java | 23 ++++--- .../content/jpa/ContentStoreIT.java | 69 +++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java b/spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/store/DefaultJpaStoreImpl.java index eeadb6a7f..8bfd17822 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 @@ -260,7 +260,9 @@ public S setContent(S entity, PropertyPath propertyPath, InputStream content, Se @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? + if (property == null) { + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); + } SID contentId = getContentId(entity, propertyPath); if (contentId == null || params.getDisposition().equals(org.springframework.content.commons.store.SetContentParams.ContentDisposition.CreateNew)) { @@ -370,14 +372,12 @@ public S unsetContent(S entity, PropertyPath propertyPath, org.springframework.c public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams params) { ContentProperty property = this.mappingContext.getContentProperty(entity.getClass(), propertyPath.getName()); if (property == null) { - // TODO - } - Object id = property.getContentId(entity); - if (id == null) { - id = -1L; + throw new StoreAccessException(String.format("Content property %s does not exist", propertyPath.getName())); } - Resource resource = loader.getResource(id.toString()); - if (resource instanceof DeletableResource && params.getDisposition().equals(UnsetContentParams.Disposition.Remove)) { + + Resource resource = this.getResource(entity, propertyPath); + + if (resource != null && resource.exists() && resource instanceof DeletableResource && params.getDisposition().equals(UnsetContentParams.Disposition.Remove)) { try { ((DeletableResource) resource).delete(); } catch (Exception e) { @@ -385,8 +385,11 @@ public S unsetContent(S entity, PropertyPath propertyPath, UnsetContentParams pa throw new StoreAccessException(format("Unsetting content for entity %s", entity), e); } } - unassociate(entity, propertyPath); - property.setContentLength(entity, 0); + + if (resource != null) { + unassociate(entity, propertyPath); + property.setContentLength(entity, 0); + } return entity; } 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 6986ff150..a2317eeb8 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 @@ -13,15 +13,23 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.UUID; import java.util.function.Supplier; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.runner.RunWith; +import org.springframework.content.commons.store.ContentStore; +import org.springframework.content.commons.annotations.ContentId; +import org.springframework.content.commons.annotations.ContentLength; 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.data.jpa.repository.JpaRepository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @@ -40,6 +48,8 @@ import internal.org.springframework.content.jpa.testsupport.repositories.ClaimRepository; import internal.org.springframework.content.jpa.testsupport.stores.ClaimStore; +import jakarta.persistence.*; + @RunWith(Ginkgo4jRunner.class) @Ginkgo4jConfiguration(threads = 1) public class ContentStoreIT { @@ -61,6 +71,9 @@ public class ContentStoreIT { protected ClaimRepository claimRepo; protected ClaimStore claimFormStore; + private EmbeddedRepository embeddedRepo; + private EmbeddedStore embeddedStore; + protected Claim claim; protected Object id; @@ -81,6 +94,9 @@ public class ContentStoreIT { claimRepo = context.getBean(ClaimRepository.class); claimFormStore = context.getBean(ClaimStore.class); + embeddedRepo = context.getBean(EmbeddedRepository.class); + embeddedStore = context.getBean(EmbeddedStore.class); + if (ptm == null) { ptm = mock(PlatformTransactionManager.class); } @@ -285,6 +301,30 @@ public class ContentStoreIT { Assert.assertEquals(claim.getClaimForm().getContentLength(), 0); }); }); + + Context("@Embedded content", () -> { + Context("given a entity with a null embedded content object", () -> { + It("should return null when content is fetched", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + assertThat(embeddedStore.getContent(entity, PropertyPath.from("content")), is(nullValue())); + }); + + It("should be successful when content is set", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + embeddedStore.setContent(entity, PropertyPath.from("content"), new ByteArrayInputStream("Hello Spring Content World!".getBytes())); + try (InputStream is = embeddedStore.getContent(entity, PropertyPath.from("content"))) { + assertThat(IOUtils.contentEquals(is, new ByteArrayInputStream("Hello Spring Content World!".getBytes())), is(true)); + } + }); + + It("should return null when content is unset", () -> { + EntityWithEmbeddedContent entity = embeddedRepo.save(new EntityWithEmbeddedContent()); + EntityWithEmbeddedContent expected = new EntityWithEmbeddedContent(entity.getId(), entity.getContent()); + assertThat(embeddedStore.unsetContent(entity, PropertyPath.from("content")), is(expected)); + int i = 0; + }); + }); + }); }); }); } @@ -356,4 +396,33 @@ protected void deleteAllClaimFormsContent() { } } } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name="entity_with_embedded") + public static class EntityWithEmbeddedContent { + + @Id + private String id = UUID.randomUUID().toString(); + + @Embedded + private EmbeddedContent content; + } + + @Embeddable + @NoArgsConstructor + @Data + public static class EmbeddedContent { + + @ContentId + private String contentId; + + @ContentLength + private Long contentLen; + } + + public interface EmbeddedRepository extends JpaRepository {} + public interface EmbeddedStore extends ContentStore {} }