Getting Started with Spring Content and CMIS

What you'll build

You'll build a simple document management application using Spring Content and Spring Content CMIS.

What you'll need

  • About 30 minutes

  • A favorite text editor or IDE

  • JDK 1.8 or later

  • Maven 3.0+

You can also import the code from this guide as well as view the web page directly into Spring Tool Suite (STS) and work your way through it from there.

How to complete this guide

Like most Spring Getting Started guides, you can start form scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.

To start from scratch, move on to Build with Maven.

To skip the basics, do the following:

  • Download and unzip the source repository for this guide, or clone it using Git: git clone https://github.com/paulcwarren/spring-content-gettingstarted.git

  • cd into spring-content-gettingstarted/spring-content-with-cmis/initial

  • Jump ahead to Define a simple entity. When you’re finished, you can check your results against the code in spring-content-gettingstarted/spring-content-with-cmis/complete.

Build with Maven

First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with Maven is included here. If you’re not familiar with Maven, refer to Building Java Projects with Maven.

Create a directory structure

In a project directory of your choosing, create the following subdirectory structure; for example, with mkdir -p src/main/java/gettingstarted on *nix systems:

∟ src
   ∟ main
       ∟ java
           ∟ gettingstarted
       ∟ resources
           ∟ static

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <artifactId>spring-content-with-cmis</artifactId>

 	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.7</version>
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<ginkgo-version>1.0.12</ginkgo-version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>
		<dependency>
			<groupId>com.github.paulcwarren</groupId>
			<artifactId>spring-content-fs-boot-starter</artifactId>
			<version>2.3.0</version>
		</dependency>
        <dependency>
            <groupId>com.github.paulcwarren</groupId>
            <artifactId>spring-versions-jpa</artifactId>
            <version>2.3.0</version>
        </dependency>
 		<dependency>
			<groupId>com.github.paulcwarren</groupId>
			<artifactId>spring-content-cmis</artifactId>
			<version>2.3.0</version>
		</dependency>
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
        </dependency>

		<!-- Test dependencies -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
		</dependency>
		<dependency>
			<groupId>com.jayway.restassured</groupId>
			<artifactId>rest-assured</artifactId>
			<version>2.9.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.github.paulcwarren</groupId>
			<artifactId>ginkgo4j</artifactId>
			<version>${ginkgo-version}</version>
			<scope>test</scope>
		</dependency>
 	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>snapshots</id>
			<name>nexus</name>
			<url>https://oss.sonatype.org/content/repositories/snapshots</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
	</repositories>

</project>

We add several dependencies:-

  • Spring Boot Starter Web provides the web server framework

  • Spring Security provides the security framework

  • Spring Boot Starter Data JPA will provide a relational database to store the metadata of our documents. In this case we are using the H2 in-memory database

  • Spring Boot Starter Content FS will provide Filesystem-based storage for the content of each document and manage its association with a Document Entity

  • Spring Versions JPA adds the ability to create new versions of a Document

  • Spring Content CMIS provides the ability to export Document and Folder Entities via CMIS browser bindings.

Create an Application class

We need to initialize various aspects of our application. First we'll add the usual Spring Boot Application class.

src/main/java/gettingstarted/SpringContentApplication.java


package gettingstarted;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.content.cmis.EnableCmis;
import org.springframework.content.fs.config.EnableFilesystemStores;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.versions.jpa.config.JpaLockingAndVersioningConfig;

@SpringBootApplication
@EnableCmis(basePackages = "gettingstarted",
        id = "1",
        name = "spring-content-with-cmis",
        description = "Spring Content CMIS Getting Started Guide",
        vendorName = "Spring Content OSS",
        productName = "Spring Content CMIS Connector",
        productVersion = "1.0.0")
@Import(JpaLockingAndVersioningConfig.class)
@EnableJpaRepositories(
        basePackages={  "gettingstarted",
                        "org.springframework.versions"},
        considerNestedRepositories=true)
@EnableFilesystemStores
public class SpringContentApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringContentApplication.class, args);
	}

	@Configuration
	@EnableWebSecurity
	@EnableJpaAuditing
	public static class SecurityConfig extends WebSecurityConfigurerAdapter {

	    @Autowired
	    public void configureGlobal(AuthenticationManagerBuilder auth)
	            throws Exception {
	        auth.
	                inMemoryAuthentication()
	                    .withUser("test")
	                    .password("{noop}test")
	                        .roles("USER");
	    }

	    @Override
	    protected void configure(HttpSecurity http) throws Exception {
	        http
	                .csrf()
	                    .disable()
	                .authorizeRequests()
	                    .anyRequest()
	                        .authenticated()
	                    .and()
	                        .httpBasic();
	    }

	    @Bean
	    public AuditorAware<String> objectAuditor() {
	        return new AuditorAware<String>() {
	            @Override
	            public Optional<String> getCurrentAuditor() {
	                return Optional.ofNullable(SecurityContextHolder.getContext())
	                        .map(SecurityContext::getAuthentication)
	                        .filter(Authentication::isAuthenticated)
	                        .map(Authentication::getPrincipal)
	                        .map((u) -> ((User)u).getUsername());
	            }
	        };
	    }
	}
}

Let's talk through the annotations:

  • @SpringBootApplication needs no explanation

  • @EnableCmis enables the cmis bindings. The most important attribute is basePackages. This tells the Spring Content CMIS module which packages to scan for Entities with @Cmis annotations. We'll see these presently.

  • @Import imports the Spring Versions JPA standard configuration

  • @EnableJpaRepositories tells Spring Data which packages to scan for Spring Data JPA Entities. We need to add this annotation in order to instruct Spring Data to scan the org.springframework.versions package (where it will find the LockingAndVersioningRepository fragment implementation) in addition to the gettingstarted package.

  • @EnableFilesystemStores enables Spring Content Filesystem Storage. Technically, this is not required but added for clarity.

You will also see that this application enables Spring Security setting up a single user test with a password of test. As well as entity auditing that we use to timestamp entity creation and modification.

Define a BaseObject

We are going to model both Documents and Folders and they share some common characteristics so let's define a BaseObject that they can both inherit from:

src/main/java/gettingstarted/BaseObject.java


package gettingstarted;


import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.ManyToOne;
import javax.persistence.Version;

import org.springframework.content.cmis.CmisDescription;
import org.springframework.content.cmis.CmisName;
import org.springframework.content.cmis.CmisReference;
import org.springframework.content.cmis.CmisReferenceType;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@EntityListeners(AuditingEntityListener.class)
@Inheritance(strategy = InheritanceType.JOINED)
@NoArgsConstructor
@Getter
@Setter
public class BaseObject {

	@javax.persistence.Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long Id;

	@CmisName
	private String name;

	@CmisDescription
	private String description;

	@CreatedBy
	private String createdBy;

	@CreatedDate
	private Long createdDate;

	@LastModifiedBy
	private String lastModifiedBy;

	@LastModifiedDate
	private Long lastModifiedDate;

	@Version
	private Long vstamp;

	@CmisReference(type = CmisReferenceType.Parent)
	@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	private Folder parent;

	public BaseObject(String name) {
		this.name = name;
	}
}

As you would expect we created a standard JPA Entity to capture some common metadata; name and description. Standard audit metadata; createdBy, createdDate, modifiedBy, modifiedDate, and a revision stamp; vstamp. Lastly, we create a many-to-one relationship to a parent folder.

You will notice that several of these fields are annotated with @Cmis annotations. @CmisName and @CmisDescription map these fields to their respective cmis fields. @CmisReference is a special reference that instructs Spring Content CMIS that this is part of a parent/child relationship.

Create a Folder Entity

Next, we'll create the Folder entity. As you would expect a Folder is a containing object allowing users to arrange their documents hierarchically.

src/main/java/gettingstarted/Folder.java


package gettingstarted;


import java.util.Collection;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;

import org.springframework.content.cmis.CmisFolder;
import org.springframework.content.cmis.CmisReference;
import org.springframework.content.cmis.CmisReferenceType;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@Getter
@Setter
@CmisFolder
public class Folder extends BaseObject {

	@CmisReference(type= CmisReferenceType.Child)
	@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL)
	private Collection<BaseObject> children;

	public Folder(String name) {
		super(name);
	}
}

Note the @CmisFolder annotation that maps this entity to the cmis:folder object type. Also note the @CmisReference annotation instructing Spring Content CMIS that this is the other end of the parent/child relationship.

Create a Document entity

Lastly, we'll create the Document entity.

src/main/java/gettingstarted/Document.java


package gettingstarted;


import java.util.UUID;

import javax.persistence.Entity;

import org.springframework.content.cmis.CmisDocument;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.annotations.ContentLength;
import org.springframework.content.commons.annotations.MimeType;
import org.springframework.versions.AncestorId;
import org.springframework.versions.AncestorRootId;
import org.springframework.versions.LockOwner;
import org.springframework.versions.SuccessorId;
import org.springframework.versions.VersionLabel;
import org.springframework.versions.VersionNumber;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@Getter
@Setter
@CmisDocument
public class Document extends BaseObject {

	@ContentId
	private UUID contentId;

	@ContentLength
	private Long contentLen;

	@MimeType
	private String contentMimeType;

	@LockOwner
	private String lockOwner;

	@AncestorId
	private Long ancestorId;

	@AncestorRootId
	private Long ancestralRootId;

	@SuccessorId
	private Long successorId;

	@VersionNumber
	private String versionNumber = "0.0";

	@VersionLabel
	private String versionLabel;

	public Document(String name) {
		super(name);
	}

	public Document(Document doc) {
		this.setName(doc.getName());
		this.setDescription(doc.getDescription());
		this.setParent(doc.getParent());
	}
}

Again, note the @CmisDocument that maps this entity to the cmis:document object type. We also see the standard Spring Versions JPA annotations for capturing version metadata as Documents are versioned.

Create Repositories and Storage

For both Folder and Document we create Repository interfaces:

public interface FolderRepository extends JpaRepository<Folder, Long> {
}

public interface DocumentRepository extends JpaRepository<Document, Long>, LockingAndVersioningRepository<Document, Long> {
}

The DocumentRepository extends LockingAndVersioningRepository making Documents versionable.

And, of course, we create a Storage interface for Documents:

public interface DocumentStorage extends ContentStore<Document, UUID> {

Build an executable JAR

That's it. That's all you need. So let's take our application for a spin. If you are using Maven, you can run the application using mvn spring-boot:run. Or you can build the JAR file with mvn clean package and run the JAR by typing:

java -jar target/gettingstarted-spring-content-with-cmis-0.0.1.jar

Use the CMIS Workbench to test the application

We'll use the CMIS Workbench from the Apache Chemistry project to test the application.

The workbench can be downloaded from here.

Due to this bug, Java 8 is required.

Download and run the workbench; i.e. ./workbench.sh.

Spring Content CMIS exports the CMIS browser bindings to the /browser endpoint therefore the URL is http://localhost:8080/browser and the binding type is Browser.

The username and password, as per the SecurityConfig, is test/test.

Spring Content With CMIS Workbench Login

Click Load Repositores and you should see this:

Spring Content With CMIS Workbench Repositories

This information should match the information provided in the Application class.

Click Login and you should see something like this:

Spring Content With CMIS Workbench

Let's create a folder. Click Create Object -> Folder. Enter test-folder as the name and click Create Folder.

You'll see the folder in the navigation pane. Double-click on the folder to navigate into it. Select the Properties tab to view its properties.

Spring Content With CMIS Folder

Now, let's create a Document in this Folder. Click Create Object -> Document. Enter test-document as the name and generate 100 bytes of content. Click Create Document.

Spring Content With CMIS Document

Let's take a look at its content. Double-click on the document in the navigator to open the content in a new window.

Earlier we made our Entity's versionable by having our DocumentRepository extend LockingAndVersioningRepository so let's create a new version of this document. Make sure the document is selected in the navigator. Select the Actions tab and Check-out Object. Now you should see two documents in your navigator. One blue, the original. One green, a private working copy of the new version.

Spring Content With CMIS Private Working Copy

Usually, a user would iterate on their content and save it by setting the content stream. All of which is private to them. For simplicity we'll skip this step and go straight to checking in. In the Check-in Object panel select a local file and Check-in.

Investigate the properties of both versions to see how they are related. You should see that they share the same version series id and in fact one of them, the first we created, is that document. Also note, the content streams IDs are different and when viewed you, in fact, do see different content.

Summary

Congratulations! You’ve written a simple application that uses Spring Content CMIS.