© 2008-2017 The original authors.
Note
|
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. |
Preface
Project metadata
-
Version control - http://github.com/paulcwarren/spring-content/
-
Bugtracker - http://github.com/paulcwarren/spring-content/issues
-
Release repository - https://repo1.maven.org/maven2/
-
Snapshots repository - https://oss.sonatype.org/content/repositories/snapshots
1. Working with Spring Stores
The goal of the Spring Content is to make it easy to create applications that manage content such as documents, images and video by significantly reducing the amount of boilerplate code that the Developer must create for themselves. Instead, the Developer provides interfaces only that declare the intent for the content-related functionality. Based on these, and on class-path dependencies, Spring Content is then able to inject storage-specific implementations.
Important
|
This chapter explains the core concepts and interfaces for Spring Content. The examples in this chapter use Java configuration and code samples for the Spring Content S3 module. Adapt the Java configuration and the types to be extended to the equivalents for the particular Spring Content module that you are using. |
1.1. Core concepts
The central interface in the Spring Content is the ContentStore
. This interface provides CRUD functionality and entity association for content. As such a ContentStore is typed to the Spring Data Entity to which content is associated, and to the type of a @ContentId
field on that domain class.
public interface ContentStore<E, CID extends Serializable> {
void setContent(E entity, InputStream content); (1)
InputStream getContent(E entity); (2)
void unsetContent(E entity); (3)
}
-
Stores content and associates it with
entity
-
Returns the content associated with
entity
-
Deletes content and unassociates it from
entity
For example, given an Entity User
, a UserRepository
and a ProfilePictureStore
it
is possible to associate and store a profile picture for each user.
@Entity
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String username;
@ContentId
private String contentId;
@ContentLength
private Long contentLength
}
public interface UserRepository extends JpaRepository<User, Long> {
}
public interface ProfilePictureStore extends ContentStore<User, String> {
}
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner demo(UserRepository repository, ProfilePictureStore store) {
return (args) -> {
// create a new user
User jbauer = new User("jbauer");
// store profile picture
store.setContent(jbauer, new FileInputStream("/tmp/jbauer.jpg"));
// save the user
repository.save(jbauer);
};
}
}
1.1.1. Experimental API
All Spring Content storage modules also now support an experimental Store API based on the
Spring Resource API (i.e. org.springframework.core.io.ResourceLoader
and org.springframework.core.io.Resource
).
The base interface in this API, Store
, is a generic org.springframework.core.io.ResourceLoader
. Unlike
the conventional Spring ResourceLoader this more generalized ResourceLoader can handle Resources with different types
of Identifer, not just string-based locators, and returns Spring Resource’s implementing
org.springframework.content.commons.io.DeletableResource
providing a set of methods to control the entire lifecycle
from creation through to deletion.
public interface Store<SID extends Serializable> {
Resource getResource(SID id);
}
This interface is supported by Spring Content REST.
Derived from the Store
is a second interface AssociativeStore
that allows Spring `Resource`s to also be associated
with Spring Data Entities.
public interface AssociativeStore<S, SID extends Serializable> extends Store<SID> {
Resource getResource(S entity);
void associate(S entity, SID id);
void unassociate(S entity);
}
This interface is not yet supported by Spring Content REST.
1.1.2. Events
Spring Content emits twelve events. Roughly speaking one for each Store method. They are:
-
BeforeGetResourceEvent
-
AfterGetResourceEvent
-
BeforeAssociateEvent
-
AfterAssociateEvent
-
BeforeUnassociateEvent
-
AfterUnassociateEvent
-
BeforeSetContent
-
AfterSetContent
-
BeforeGetContent
-
AfterGetContent
-
BeforeUnsetContent
-
AfterUnsetContent
Writing an ApplicationListener
If you wish to extend Spring Content’s functionality you can subclass the abstract class AbstractStoreEventListener
and
override the methods that you are interested in. When these events occur your handlers will be called.
There are two variants of each event handler. The first takes the entity with with the content is associated and is the source of the event. The second takes the event object. The latter can be useful, especially for events related to Store methods that return results to the caller.
public class ExampleEventListener extends AbstractContentRepositoryEventListener {
@Override
public void onAfterSetContent(Object entity) {
...logic to inspect and handle the entity and it's content after it is stored
}
@Override
public void onBeforeGetContent(BeforeGetContentEvent event) {
...logic to inspect and handle the entity and it's content before it is fetched
}
}
The down-side of this approach is that it does not filter events based on Entity.
Writing an Annotated StoreEventHandler
Another approach is to use an annotated handler, which does filter events based on Entity.
To declare a handler, create a POJO and annotate it as @StoreEventHandler
. This tells
Spring Content that this class needs to be inspected for handler methods. It
iterates over the class’s methods and looks for annotations that correspond to the
event. There are twelve handler annotations:
-
HandleBeforeGetResource
-
HandleAfterGetResource
-
HandleBeforeAssociate
-
HandleAfterAssociate
-
HandleBeforeUnassociate
-
HandleAfterUnassociate
-
HandleBeforeSetContent
-
HandleAfterSetContent
-
HandleBeforeGetContent
-
HandleAfterGetContent
-
HandleBeforeUnsetContent
-
HandleAfterUnsetContent
@StoreEventHandler
public class ExampleAnnotatedEventListener {
@HandleAfterSetContent
public void handleAfterSetContent(SopDocument doc) {
...type-safe handling logic for SopDocument's and their content after it is stored
}
@HandleBeforeGetContent
public void onBeforeGetContent(Product product) {
...type-safe handling logic for Product's and their content before it is fetched
}
}
These handlers will be called only when the event originates from a matching entity.
As with the ApplicationListener event handler in some cases it is useful to handle the event. For example, when Store methods returns results to the caller.
@StoreEventHandler
public class ExampleAnnotatedEventListener {
@HandleAfterSetContent
public void handleAfterGetResource(AfterGetResourceEvent event) {
SopDocument doc = event.getSource();
Resource resourceToBeReturned = event.getResult();
...code that manipulates the resource being returned...
}
}
To register your event handler, either mark the class with one of Spring’s @Component stereotypes so it can be picked up by @SpringBootApplication or @ComponentScan. Or declare an instance of your annotated bean in your ApplicationContext.
@Configuration
public class ContentStoreConfiguration {
@Bean
ExampeAnnotatedEventHandler exampleEventHandler() {
return new ExampeAnnotatedEventHandler();
}
}
1.1.3. Searchable Stores
Applications that handle documents and other media usually have search capabilities allowing relevant content to be found by looking inside of it for keywords or phrases, so called full-text search.
Spring Content is able to support this capability with it’s Searchable<CID>
interface.
public interface Searchable<CID> {
Iterable<T> search(String queryString);
}
Any Store interface can be made to extend Searchable<CID>
in order to extend its capabilities to include the
search(String queryString)
method. For example:
public interface DocumentContentStore extends ContentStore<Document, UUID>, Searchable<UUID> {
}
...
@Autowired
private DocumentContentStore store;
Iterable<UUID> = store.search("to be or not to be");
For search
to return actual results full-text indexing must be enabled. See Fulltext Indexing and Searching
for more information on how to do this.
1.1.4. Renditions
Applications that handle files and other media usually also have rendition capabilities allowing content to be transformed from one format to another.
Content stores can therefore optionally also be given rendition capabilities by extending the Renderable<E>
interface.
public interface Renderable<E> {
InputStream getRendition(E entity, String mimeType);
}
Returns a mimeType
rendition of the content associated with entity
.
1.2. Creating Content Store Instances
To use these core concepts:
-
Define a Spring Data entity and give it’s instances the ability to be associated with content by adding
@ContentId
and@ContentLength
annotations@Entity public class SopDocument { private @Id @GeneratedValue Long id; private String title; private String[] authors, keywords; // Spring Content managed attribute private @ContentId UUID contentId; private @ContentLength Long contentLen; }
-
Define an interface extending Spring Data’s
CrudRepository
and type it to the domain and ID classes.public interface SopDocumentRepository extends CrudRepository<SopDocument, Long> { }
-
Define another interface extending
ContentStore
and type it to the domain and@ContentId
class.public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID> { }
-
Optionally, make it extend
Searchable
public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID>, Searchable<UUID> { }
-
Optionally, make it extend
Renderable
public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID>, Renderable<SopDocument> { }
-
Set up Spring to create proxy instances for these two interfaces using JavaConfig:
@EnableJpaRepositories @EnableS3Stores class Config {}
NoteThe JPA and S3 namespaces are used in this example. If you are using the repository and content store abstractions for other databases and stores, you need to change this to the appropriate namespace declaration for your store module. -
Inject the repositories and use them
@Component public class SomeClass { @Autowired private SopDocumentRepository repo; @Autowired private SopDocumentContentStore contentStore; public void doSomething() { SopDocument doc = new SopDocument(); doc.setTitle("example"); contentStore.setContent(doc, new ByteArrayInputStream("some interesting content".getBytes())); (1) doc.save(); ... InputStream content = contentStore.getContent(sopDocument); ... List<SopDocument> docs = doc.findAllByContentId(contentStore.findKeyword("interesting")); ... } }
-
Spring Content will update the
@ContentId
and@ContentLength
fields
-
1.3. Patterns of Content Association
Content can be associated with a Spring Data Entity in several ways.
1.3.1. Entity Association
The simplest, allowing you to associate one Entity with one Resource, is to decorate your Spring Data Entity with the Spring Content attributes.
The following example shows a Resource associated with an Entity Dvd
.
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
...
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface DvdStore extends ContentStore<Dvd, UUID> {}
1.3.2. Property Association
Sometimes you might want to associate multiple different Resources with an Entity. To do this it is also possible to associate Resources with one or more Entity properties.
The following example shows two Resources associated with a Dvd
entity. The first Resource is the Dvd’s cover Image and the second is the Dvd’s Stream.
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "image_id")
private Image image;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "stream_id")
private Stream stream;
...
}
@Entity
public class Image {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
@OneToOne
private Dvd dvd;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
@Entity
public class Stream {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
@OneToOne
private Dvd dvd;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface ImageStore extends ContentStore<Image, UUID> {}
public interface StreamStore extends ContentStore<Stream, UUID> {}
Note how the Content attributes are placed on each property object of on the Entity itself.
When using JPA with a relational database these are typically (but not always) also Entity associations. However when using NoSQL databases like MongoDB that are capable of storing hierarchical data they are true property associations.
Property Collection Associations
In addition to associating many different types of Resource with a single Entity. It is also possible to associate one Entity with many Resources using a java.util.Collection
property, as the following example shows.
@Entity
public class Dvd {
private @Id @GeneratedValue Long id;
private String title;
@OneToMany
@JoinColumn(name = "chapter_id")
private List<Chapter> chapters;
...
}
@Entity
public class Chapter {
// Spring Data managed attribute
private @Id @GeneratedValue Long id;
// Spring Content managed attributes
private @ContentId UUID contentId;
private @ContentLength Long contentLen;
}
public interface DvdRepository extends CrudRepository<Dvd, Long> {}
public interface ChapterStore extends ContentStore<Chapter, UUID> {}
2. S3 Content Stores
2.1. Annotation based configuration
Spring Content S3 is enabled with the following Java Config.
@Configuration
@EnableS3Stores
public static class ApplicationConfig {
@Bean
public AmazonS3 client() { (1)
BasicAWSCredentials creds = new BasicAWSCredentials(...)
AmazonS3Client amazonS3Client = new AmazonS3Client(creds);
return amazonS3Client;
}
}
-
The Amazon S3 client used by the S3 ResourceLoader
-
A default bucket also needs to be set (see Configuring below)
2.2. Configuring
The following configuration properties (prefix spring.content.s3
) are supported.
Property | Required | Description |
---|---|---|
bucket |
No |
Content store location. If not set as an application property Spring Content S3 will look for the environment variable AWS_BUCKET |
2.3. Accessing Content
2.3.1. Storage Model
In Amazon S3, buckets and objects are the two main primitives, where objects are stored in buckets. Amazon S3 has a flat structure with no hierarchy like you would see in a typical file system. There are also no limits to the number of buckets or objects in buckets. However, for the sake of organizational simplicity, the Amazon S3 console supports the folder concept as a means of grouping objects. Amazon S3 does this by using key name prefixes for objects.
2.3.2. S3ObjectId Resolution
Before content is stored or retrieved the store uses the following rules to determine the bucket and
key to create an S3ObjectId
.
For Store
:
-
if the ID is already an
S3ObjectId
this will be used as-is -
otherwise, if a registered Spring Converter exists that converts the ID to an
S3ObjectId
this will be used to create anS3ObjectId
-
otherwise, the
@ContentId
field and default bucket will be used as key and bucket respectively to create anS3Object
For AssociativeStore
or ContentStore
:
-
if a registered Spring Converter exists that converts the entity to an
S3ObjectId
this will be used -
otherwise, if one of the Entity’s field is annotated with
@Bucket
, the@ContentId
and the@Bucket
will be used as key and bucket respectively to create a newS3ObjectId
-
otherwise, the
@ContentId
field and default bucket will be used as key and bucket respectively to create anS3Object
Configuring a Spring Converter
To configure Spring Content S3 with one or more Spring Converters that convert your Entities, or IDs, to an instance of S3ObjectId the following approach can be used:
@Configuration
public class S3StoreConfiguration {
@Bean
public S3StoreConfigurer configurer() {
return new S3StoreConfigurer() {
@Override
public void configureS3StoreConverters(ConverterRegistry registry) {
registry.addConverter(new Converter<TestEntity, S3ObjectId>() {
@Override
public S3ObjectId convert(TestEntity entity) {
return new S3ObjectId(entity.getCustomBucketField(), entity.getCustomContentIdField());
}
});
}
@Override
public void configureS3ObjectIdResolvers(S3ObjectIdResolvers resolvers) {
// deprecated
}
}
}
}
Key Resolution
By default, Spring Content S3 will store all content using simple keys. This maps well onto primitive datatypes and
java.util.UUID
.
The S3 Store uses a PlacementService
to convert the Entity’s @ContentId
into a resource path string. By
configuring your application to contribute one (or more) Spring Converters that convert from your Entity’s ID to String
it is possible to configure the Store to take advantage of S3’s console folder concepts.
For example, @ContentId
fields of type java.util.UUID
or java.net.URI
can both be mapped to a nested resource path
that will have the effect, in the Amazon S3 console, of organizing content into a distributed set of folders.
@Configuration
public class S3StoreConfiguration {
@Bean
public S3StoreConfigurer configurer() {
return new S3StoreConfigurer() {
@Override
public void configureS3StoreConverters(ConverterRegistry registry) {
registry.addConverter(new Converter<UUID,String>() {
@Override
public String convert(UUID source) {
return String.format("/%s", source.toString().replaceAll("-", "/"));
}
});
}
@Override
public void configureS3ObjectIdResolvers(S3ObjectIdResolvers resolvers) {
// deprecated
}
}
}
}
2.3.3. Setting Content
Storing content is achieved using the ContentStore.setContent(entity, InputStream)
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 @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.
2.3.4. Getting Content
Content can be accessed using the ContentStore.getContent(entity)
method.
2.3.5. Unsetting Content
Content can be removed using the ContentStore.unsetContent(entity)
method.
3. Indexing & Searching with Solr
3.1. Overview
When enabled the Solr integration will forward all content to Solr for fulltext indexing which can then be searched by adding the optional Searchable<CID>
interface to the Content Repositories.
3.2. Dependencies
Add the solrj to the classpath.
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>5.5.3</version> (1)
<exclusions>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>wstx-asl</artifactId>
</exclusion>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
-
If using Spring Boot the version may be omitted
3.3. Enabling
-
Specify the
@EnableFullTextSolrIndexing
annotation to your@Configuration
Spring Application block. -
Ensure a
(SolrJ) SolrClient
@Bean
is instantiated somewhere within your@Configuration
Spring Application block.
3.4. Configuring
By default when the Solr module is enabled Spring-Content looks for a http://localhost:8983/solr/solr
solr server with no username or password.
To change this behavior the following variables need to be set via the Externalized Configuration method.
Property | Description |
---|---|
solr.url |
Url of the Solr host (including port and core) |
solr.username |
Solr user |
solr.password |
Solr user’s password |