© 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 significantly reduce the amount of boilerplate code required to implement handling of rich-content resources and other media by providing content access, metadata association, search and transformation capabilities for various implementations of Content Store.

Important

This chapter explains the core concepts and interfaces for Spring Content stores. The information in this chapter is pulled from the Spring Content Commons. It uses the configuration and code samples for the S3 module. Adapt the Java configuration and the types to be extended to the equivalents of the particular module that you are using.

1.1. Core concepts

The central interface in the Spring Content is the ContentStore abstraction. This interface provides CRUD functionality and metadata association for content. It is typed to the Spring Data domain class to which the content is associated, and to the type of the content ID attribute 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 saves its metadata on entity

  2. Returns the content associated with entity

  3. Deletes content and unassociates it from entity

1.1.1. Events

There are 6 events that Spring Content emits whilst performing basic content access. Those are:

  • BeforeSetContent

  • AfterSetContent

  • BeforeGetContent

  • AfterGetContent

  • BeforeUnsetContent

  • AfterUnsetContent

Writing an ApplicationListener

There is an abstract class that you can subclass which listens for these kinds of events and calls the appropriate method based on the event type. You just override the methods for the events you’re interested in.

Example 2. 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(Object entity) {
		...logic to inspect and handle the entity and it's content before it is fetched
	}
}

This approach does not filter events based on domain type.

Writing an Annotated ContentRepository Handler

Another approach is to use an annotated handler, which does filter events based on domain type.

To declare a handler, create a POJO and annotate it as @StoreEventHandler. This tells the BeanPostProcessor 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 content event. There are 6 handler annotations. Those are:

  • HandleBeforeSetContent

  • HandleAfterSetContent

  • HandleBeforeGetContent

  • HandleAfterGetContent

  • HandleBeforeUnsetContent

  • HandleAfterUnsetContent

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

The type of interests you are interested in is determined from the type of the first parameter of each annotated method.

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 4. Handler registration
@Configuration
public class ContentStoreConfiguration {

  @Bean
  ExampeAnnotatedEventHandler exampleEventHandler() {
    return new ExampeAnnotatedEventHandler();
  }
}
Experimental API

Some Spring Content modules offer support for a new experimental Store API. This API offers lower-level management of content that can be used to satisfy a wider set of content-related use cases.

The base interface in this API is the Store, a generic version of a org.springframework.core.io.ResourceLoader that returns `org.springframework.content.commons.io.DeletableResource`s allowing them to be fully-managed.

Example 5. Store interface
public interface Store<ID extends Serializable> {

	Resource getResource(ID id);

}

Derived from the Store is a second interface AssociativeStore allowing Resources returned by `Store`s to be associated with Spring Data Entity objects.

Example 6. AssociativeStore interface
public interface AssociativeStore<S, SID extends Serializable> extends Store<SID> {

	void associate(S entity, SID id);
	void unassociate(S entity);

}

1.1.2. Search

Applications that handle files and other media usually have search capabilities allowing content to be found by looking inside of it.

Content stores can therefore optionally be made searchable by extending the Searchable<CID> interface.

Example 7. Searchable interface
public interface Searchable<CID> {

    Iterable<T> findKeyword(String term);

    Iterable<T> findAllKeywords(String...terms);

    Iterable<T> findAnyKeywords(String...terms);

    Iterable<T> findKeywordsNear(int proximity, String...terms);

    Iterable<T> findKeywordStartsWith(String term);

    Iterable<T> findKeywordStartsWithAndEndsWith(String prefix, String suffix);

    Iterable<T> findAllKeywordsWithWeights(String[] terms, double[] weights);
}

1.1.3. 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 8. 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
    @EnableS3ContentRepositories
    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. Filesystem Content Stores

2.1. Annotation-based Configuration

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

Example 9. Spring Content Filesystem Stores using Java Config
@Configuration
@EnableFilesystemStores
public static class ApplicationConfig {

    @Bean
    File filesystemRoot() {
        try {
            return Files.createTempDirectory("").toFile();
        } catch (IOException ioe) {}
        return null;
    }

    @Bean
    FileSystemResourceLoader fileSystemResourceLoader() {
        return new FileSystemResourceLoader(filesystemRoot().getAbsolutePath());
    }

}

By default content will be stored in a randomly chosen (at application startup) folder

2.2. Spring Boot Configuration

When using content-fs-spring-boot-starter the root for all file system stores will be a randomly chosen folder (at application start-up) under java.io.tmdir.

The following configuration properties (prefix content.fs) are supported.

Property Description

filesystemRoot

The root location where file system stores place their content (defaults to java.io.tmpdir/<random>/).

2.3. Accessing Content

2.3.1. Storage Customization

By default Spring Content Filesystem will store all content at the root of the content store location.

The Filesystem Store uses a dedicated ConversionService to convert the content entity’s ID into a resource path. By configuring your application to contribute one (or more) org.springframework.core.conmvert.converter.Converter it is possible to configure the Store to use other more sophisticated storage models.

For example Content IDs of type java.util.UUID or java.net.URI can both be mapped to a nested resource path that will have the effect of distributing content evenly over the Store.

Example 10. Configuring the Filesystem Store
@Configuration
public class FilesystemStoreConfiguration  {

   	public Converter<UUID,String> converter() {
		return new FilesystemStoreConverter<UUID,String>() {

			@Override
			public String convert(UUID source) {
				return String.format("/%s", source.toString().replaceAll("-", "/"));
			}
		};
	}

	@Bean
	public FilesystemStoreConfigurer configurer() {
		return new FilesystemStoreConfigurer() {

			@Override
			public void configureFilesystemStoreConverters(ConverterRegistry registry) {
				registry.addConverter(converter());
			}
		};
	}
}

2.3.2. Setting Content

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

If content has not yet been associated with this entity before and an ID has not been assigned by the application, one will be generated based on java.util.UUID and converted to the type of the @ContentId field.

The @ContentId and @ContentLength annotations will be updated on entity.

If content has been previously stored it will overwritten updating just the @ContentLength attribute, if appropriate.

2.3.3. Getting Content

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

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