Getting Started with Spring Content and CMIS (Spring Content 2.x only - Deprecated)
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
intospring-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 inspring-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.7.4</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>3.0.0</version>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-versions-jpa</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-cmis</artifactId>
<version>3.0.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 theorg.springframework.versions
package (where it will find theLockingAndVersioningRepository
fragment implementation) in addition to thegettingstarted
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
.
Click Load Repositores
and you should see this:
This information should match the information provided in the Application class.
Click Login
and you should see something like this:
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.
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
.
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.
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.