Skip to content

Commit

Permalink
Merge pull request #1059 from paulcwarren/fix/issue-1049
Browse files Browse the repository at this point in the history
ContentLinksResourceProcessor generates unique links for each content…
  • Loading branch information
paulcwarren authored Sep 7, 2022
2 parents e31347e + 7605a52 commit f4ee858
Show file tree
Hide file tree
Showing 36 changed files with 1,478 additions and 240 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.commons.mappingcontext.MappingContext;
import org.springframework.content.commons.repository.factory.AbstractStoreFactoryBean;
import org.springframework.content.commons.utils.PlacementService;
import org.springframework.context.ApplicationContext;
Expand Down Expand Up @@ -31,6 +32,9 @@ public class AzureStorageFactoryBean extends AbstractStoreFactoryBean {
// @Autowired(required=false)
// private MultiTenantS3ClientProvider s3Provider = null;

@Autowired(required=false)
private MappingContext mappingContext;

@Autowired(required=false)
private LockingAndVersioningProxyFactory versioning;

Expand All @@ -57,6 +61,6 @@ protected Object getContentStoreImpl() {
DefaultResourceLoader loader = new DefaultResourceLoader();
loader.addProtocolResolver(resolver);

return new DefaultAzureStorageImpl(context, loader, storePlacementService, client);
return new DefaultAzureStorageImpl(context, loader, mappingContext, storePlacementService, client);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.lang.reflect.Field;
import java.util.UUID;

import internal.org.springframework.content.commons.utils.ContentPropertyInfoTypeDescriptor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -40,6 +39,7 @@
import com.azure.storage.blob.BlobServiceClient;

import internal.org.springframework.content.azure.io.AzureBlobResource;
import internal.org.springframework.content.commons.utils.ContentPropertyInfoTypeDescriptor;

@Transactional
public class DefaultAzureStorageImpl<S, SID extends Serializable>
Expand All @@ -53,9 +53,9 @@ public class DefaultAzureStorageImpl<S, SID extends Serializable>
private BlobServiceClient client;
// private MultiTenantS3ClientProvider clientProvider;

private MappingContext mappingContext = new MappingContext(".", ".");
private MappingContext mappingContext/* = new MappingContext("/", ".")*/;

public DefaultAzureStorageImpl(ApplicationContext context, ResourceLoader loader, PlacementService placementService, BlobServiceClient client) {
public DefaultAzureStorageImpl(ApplicationContext context, ResourceLoader loader, MappingContext mappingContext, PlacementService placementService, BlobServiceClient client) {
Assert.notNull(context, "context must be specified");
Assert.notNull(loader, "loader must be specified");
Assert.notNull(placementService, "placementService must be specified");
Expand All @@ -65,6 +65,10 @@ public DefaultAzureStorageImpl(ApplicationContext context, ResourceLoader loader
this.placementService = placementService;
this.client = client;
// this.clientProvider = provider;
this.mappingContext = mappingContext;
if (this.mappingContext == null) {
this.mappingContext = new MappingContext("/", ".");
}
}

@Override
Expand Down
112 changes: 91 additions & 21 deletions spring-content-commons/src/main/asciidoc/content-repositories.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ public class User {
private String username;
@ContentId
private String contentId;
private String profilePictureId;
@ContentLength
private Long contentLength
private Long profilePictureLength
}
public interface UserRepository extends JpaRepository<User, Long> {
Expand Down Expand Up @@ -109,7 +109,7 @@ public class Application {
}
// associate the Resource with the Entity
store.associate(jbauer, PropertyPath("content"), "/some/picture.jpeg");
store.associate(jbauer, PropertyPath("profilePicture"), "/some/picture.jpeg");
// save the user
repository.save(jbauer);
Expand All @@ -121,7 +121,7 @@ public class Application {

== ContentStore

`ContenStore` extends AssociativeStore and provides a more convenient API for managing associated content based on java `Stream`, rather than `Resource`.
`ContentStore` extends AssociativeStore and provides a more convenient API for managing associated content based on java `Stream`, rather than `Resource`.

[[content-repositories.contentstore]]
.ContentStore interface
Expand Down Expand Up @@ -156,10 +156,10 @@ public class User {
private String username;
@ContentId
private String contentId;
private String profilePictureId;
@ContentLength
private Long contentLength
private Long profilePictureLength
}
public interface UserRepository extends JpaRepository<User, Long> {
Expand All @@ -181,7 +181,7 @@ public class Application {
User jbauer = new User("jbauer");
// store profile picture
store.setContent(jbauer, new FileInputStream("/tmp/jbauer.jpg"));
store.setContent(jbauer, PropertyPath.from("profilePicture"), new FileInputStream("/tmp/jbauer.jpg"));
// save the user
repository.save(jbauer);
Expand All @@ -193,7 +193,7 @@ public class Application {

== ReactiveContentStore

`ReactiveContenStore` is an experimental Store that provides a reactive API for managing associated content based on
`ReactiveContentStore` is an experimental Store that provides a reactive API for managing associated content based on
Mono and Flux reactive API.

[[content-repositories.reactivecontentstore]]
Expand Down Expand Up @@ -229,10 +229,10 @@ public class User {
private String username;
@ContentId
private String contentId;
private String profilePictureId;
@ContentLength
private Long contentLength
private Long profilePictureLength
}
public interface UserRepository extends JpaRepository<User, Long> {
Expand All @@ -259,7 +259,7 @@ public class Application {
ByteBuffer byteBuffer = ByteBuffer.allocate(len);
Channels.newChannel(fis).read(byteBuffer);
store.setContent(jbauer, PropertyPath.from("content"), len, Flux.just(byteBuffer)))
store.setContent(jbauer, PropertyPath.from("profilePicture"), len, Flux.just(byteBuffer)))
.doOnSuccess(updatedJbauer -> {
// save the user
repository.save(updatedJbauer).block(Duration.ofSeconds(10));
Expand All @@ -274,7 +274,7 @@ Currently, S3 is the only storage module that supports this experimental API.

=== Content Properties

As we can see above content is "associated" by adding additional metadata about the content to the JPA Entity. This additional metadata is denoted with Spring Content annotations. There are several. The only mandatory annotation is `@ContentId`. Other optional annotations include `@ContentLength`, `@MimeType` and `@OriginalFileName`. These may be added to your entities when you need to capture this additional infomation about your associated content.
As we can see above content is "associated" by adding additional metadata about the content to the Entity. This additional metadata is annotated with Spring Content annotations. There are several. The only mandatory annotation is `@ContentId`. Other optional annotations include `@ContentLength`, `@MimeType` and `@OriginalFileName`. These may be added to your entities when you need to capture this additional infomation about your associated content.

When adding these optional annotations it is highly recommended that you correlate the field's name creating a "content property". This allows for multiple pieces of content to be associated with the same entity, as shown in the following example. When associating a single piece of content this is not necessary but still recommended.

Expand All @@ -293,29 +293,99 @@ public class User {
private String username;
@ContentId
private String contentId; <1>
private String profilePictureId; <1>
@ContentLength
private Long contentLength
private Long profilePictureLength
@MimeType
private String contentType;
private String profilePictureType;
@OriginalFileName
private String contentName;
private String profilePictureName;
@ContentId
private String renditionId; <2>
private String avatarId; <2>
@ContentLength
private Long renditionLength
private Long avatarLength
@MimeType
private String renditionType;
private String avatarType;
}
----
<1> Content property "content" with id, length, type and original filename
<2> Content property "rendition" with id, length and type
<1> Content property "profilePicture" with id, length, type and original filename
<2> Content property "avatar" with id, length and type
====

When modeled thus these can then be managed as follows:

====
[source, java]
----
InputStream profilePicture = store.getContent(user, PropertyPath.from("profilePicture"));
store.setContent(user, PropertyPath.from("avatar"), avatarStream);
----
====

=== Nested Content Properties

If desired content properties can also be nested, as the following JPA example shows:

[[content-repositories.nestedcontentproperty]]
.Nested Content Properties
====
[source, java]
----
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String username;
private @Embedded Images images = new Images();
}
@Embeddable
public class Images {
@ContentId
private String profilePictureId;
@ContentLength
private Long profilePictureLength
@MimeType
private String profilePictureType;
@OriginalFileName
private String profileName;
@ContentId
private String avatarId;
@ContentLength
private Long avatarLength
@MimeType
private String avatarType;
}
----
====

These can then be managed with forward slash (`/`) separated property paths:

====
[source, java]
----
InputStream profilePicture = store.getContent(user, PropertyPath.from("images/profilePicture"));
store.setContent(user, PropertyPath.from("images/avatar"), avatarStream);
----
====

[[content-repositories.multimodule]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class RenderableImpl implements Renderable, ContentStoreAware {
private List<RenditionProvider> providers = new ArrayList<>();

public RenderableImpl() {
this.mappingContext = new MappingContext(".", ".");
this.mappingContext = new MappingContext("/", ".");
}

@Autowired(required=false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import java.util.HashMap;
import java.util.Map;

import org.springframework.content.commons.storeservice.StoreInfo;
import org.springframework.content.commons.storeservice.Stores;

public class MappingContext {

private Map<Class<?>, Map<String, ContentProperty>> context = new HashMap<>();
Expand All @@ -19,13 +16,13 @@ public MappingContext(CharSequence keySeparator, CharSequence contentPropertySep
this.contentPropertySeparator = contentPropertySeparator;
}

public MappingContext(Stores stores) {
for (StoreInfo info : stores.getStores(Stores.MATCH_ALL)) {
if (info.getDomainObjectClass() != null) {
context.put(info.getDomainObjectClass(), resolveProperties(info.getDomainObjectClass()));
}
}
}
// public MappingContext(Stores stores) {
// for (StoreInfo info : stores.getStores(Stores.MATCH_ALL)) {
// if (info.getDomainObjectClass() != null) {
// context.put(info.getDomainObjectClass(), resolveProperties(info.getDomainObjectClass()));
// }
// }
// }

public boolean hasMapping(Class<?> domainClass, String path) {
Map<String, ContentProperty> properties = context.get(domainClass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,54 @@ public class ClassWalkerTest {
assertThat(visitor.getProperties(), hasEntry("child2", expectedProperty3));
});
});

Context("given a class with a child with multiple content property objects", () -> {
It("should return two content properties", () -> {
ContentPropertyBuilderVisitor visitor = new ContentPropertyBuilderVisitor("/", ".", new ContentPropertyBuilderVisitor.CanonicalName());
ClassWalker walker = new ClassWalker(TestClass6.class);
walker.accept(visitor);

ContentProperty expectedProperty = new ContentProperty();
expectedProperty.setContentPropertyPath("child.content");
expectedProperty.setContentIdPropertyPath("child.contentId");
expectedProperty.setContentLengthPropertyPath("child.contentLen");
expectedProperty.setMimeTypePropertyPath("child.contentMimeType");
expectedProperty.setOriginalFileNamePropertyPath("child.contentOriginalFileName");
assertThat(visitor.getProperties(), hasEntry("child/content", expectedProperty));

ContentProperty expectedProperty3 = new ContentProperty();
expectedProperty3.setContentPropertyPath("child.preview");
expectedProperty3.setContentIdPropertyPath("child.previewId");
expectedProperty3.setContentLengthPropertyPath("child.previewLen");
expectedProperty3.setMimeTypePropertyPath("child.previewMimeType");
expectedProperty3.setOriginalFileNamePropertyPath("child.previewOriginalFileName");
assertThat(visitor.getProperties(), hasEntry("child/preview", expectedProperty3));
});
});

Context("given a class with a child with a child with multiple content property objects", () -> {
It("should return two content properties", () -> {
ContentPropertyBuilderVisitor visitor = new ContentPropertyBuilderVisitor("/", ".", new ContentPropertyBuilderVisitor.CanonicalName());
ClassWalker walker = new ClassWalker(TestClass7.class);
walker.accept(visitor);

ContentProperty expectedProperty = new ContentProperty();
expectedProperty.setContentPropertyPath("child.child.content");
expectedProperty.setContentIdPropertyPath("child.child.contentId");
expectedProperty.setContentLengthPropertyPath("child.child.contentLen");
expectedProperty.setMimeTypePropertyPath("child.child.contentMimeType");
expectedProperty.setOriginalFileNamePropertyPath("child.child.contentOriginalFileName");
assertThat(visitor.getProperties(), hasEntry("child/child/content", expectedProperty));

ContentProperty expectedProperty2 = new ContentProperty();
expectedProperty2.setContentPropertyPath("child.child.preview");
expectedProperty2.setContentIdPropertyPath("child.child.previewId");
expectedProperty2.setContentLengthPropertyPath("child.child.previewLen");
expectedProperty2.setMimeTypePropertyPath("child.child.previewMimeType");
expectedProperty2.setOriginalFileNamePropertyPath("child.child.previewOriginalFileName");
assertThat(visitor.getProperties(), hasEntry("child/child/preview", expectedProperty2));
});
});
}

public static class TestClass {
Expand Down Expand Up @@ -259,6 +307,26 @@ public static class TestClass5 {
private UncorrelatedAttrClass child2;
}

public static class TestClass6 {
private TestMultipleAttrs child;
}

public static class TestMultipleAttrs {
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
private @MimeType String contentMimeType;
private @OriginalFileName String contentOriginalFileName;

private @ContentId UUID previewId;
private @ContentLength Long previewLen;
private @MimeType String previewMimeType;
private @OriginalFileName String previewOriginalFileName;
}

public static class TestClass7 {
private TestClass6 child;
}

public static enum TestEnum {
A, B, C, D
}
Expand Down
Loading

0 comments on commit f4ee858

Please sign in to comment.