© 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

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.

Example 1. ContentStore interface
public interface ContentStore<E, CID extends Serializable> {

	void setContent(E entity, InputStream content); 	(1)

	InputStream getContent(E entity);			(2)

	void unsetContent(E entity);				(3)
}
  1. Stores content and associates it with entity

  2. Returns the content associated with entity

  3. 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.

Example 2. ContentStore interface
@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.

Example 3. Store interface
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.

Example 4. AssociativeStore interface
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.

Example 5. Entity-based AbstractContentRepositoryEventListener
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

Example 6. Entity-based annotated event handler
@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.

Example 7. Event-based annotated event handler
@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.

Example 8. Handler registration
@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.

Example 9. Searchable 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.

Example 10. Renderable 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:

  1. 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;
    }
  2. Define an interface extending Spring Data’s CrudRepository and type it to the domain and ID classes.

    public interface SopDocumentRepository extends CrudRepository<SopDocument, Long> {
    }
  3. Define another interface extending ContentStore and type it to the domain and @ContentId class.

    public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID> {
    }
  4. Optionally, make it extend Searchable

    public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID>, Searchable<UUID> {
    }
  5. Optionally, make it extend Renderable

    public interface SopDocumentContentStore extends ContentStore<SopDocument, UUID>, Renderable<SopDocument> {
    }
  6. Set up Spring to create proxy instances for these two interfaces using JavaConfig:

    @EnableJpaRepositories
    @EnableS3Stores
    class Config {}
    Note
    The 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.
  7. 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"));
    		...
    
    	}
    }
    1. 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. JPA Content Stores

2.1. Annotation based configuration

Spring Content JPA Stores are enabled with the following Java Config.

Example 11. Spring Content JPA Stores using Java Config
@Configuration
@EnableJpaRepositories
@EnableJpaStores
@EnableTransactionManagement
public static class ApplicationConfig {

  @Bean
  public DataSource dataSource() {

    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    return builder.setType(EmbeddedDatabaseType.HSQL).build();
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setGenerateDdl(true);

    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan("com.acme.domain");
    factory.setDataSource(dataSource());
    return factory;
  }

  @Bean
  public PlatformTransactionManager transactionManager() {

    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory());
    return txManager;
  }

}

This configuration class sets up an embedded HSQL database using the EmbeddedDatabaseBuilder API from spring-jdbc. We then set up an EntityManagerFactory and use Hibernate as the persistence provider. The last infrastructure component declared here is the JpaTransactionManager.

We activate Spring Data JPA repositories using the @EnableJpaRepositories annotation. We activate Spring Content JPA stores using @EnableJpaStores and enable transaction management with the @EnableTransactionManagement annotation.

If no base packages are configured both Spring Data JPA and Spring Content JPA will use the package that the configuration class resides in as the base package.

2.1.1. Content Streaming

Spring Content JPA attempts to provide streams that chunk BLOBs through the memory space of the JVM rather that loading the entire BLOB into memory. In order to do this the following database-specific implementation are used.

MySQL

Despite the JDBC API that returns InputStream's for streaming BLOBs. By default the MySQL JConnector JDBC Driver will load the entire BLOB into the memory-space of the JVM running the JDBC client. This risks OutOfMemoryException's being thrown, the likelihood of which increase as the size of the BLOBs being stored increase.

However, a feature called "locator emulation" switches the implementation that is used for the getBlob() JDBC API to one that actually does stream the content properly (i.e. in reasonable size chunks) through the memory-space of the JVM.

Spring Content JPA is designed to be used with this feature and therefore you must add the property emulateLocators=true to your JDBC Connection string URL

2.1.2. Postgresql

The Postgresql implementation uses the OID field type and the Large Object API. Whilst this provides properly chuncked streams it does comes with a significant performance disadvantage over bytea fields.

Large Objects must be accessed within a transaction. Therefore, Store API access must also be within a transaction. For convenience ContentStore modification methods are already marked as @Transactional. Note, however query methods are not and therefore the transaction must be handled by the consumer.

2.1.3. SQL Server

The SQL Server implementation uses adaptive buffering to serve BLOBs in a memory efficient way.

2.2. Persisting Content

2.2.1. Setting Content

Storing content is achieved using the ContentStore.setContent(entity, InputStream) method.

The entity’s @ContentId and @ContentLength fields will be updated.

If content has been previously stored this will overwritten with the new content updating just the @ContentLength field, if appropriate.

How the @ContentId field is handled

Spring Data JPA requires that content entities have an @ContentId field for identity that will be generated when content is initially set.

2.2.2. Getting Content

Content can be accessed using the ContentStore.getContent(entity) method.

2.2.3. Unsetting Content

Content can be removed using the ContentStore.unsetContent(entity) method.

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>
  1. 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