Getting Started Spring Content with Versions
What you'll build
We'll build on the previous guide Getting Started with Spring Content REST API.
What you'll need
-
About 30 minutes
-
A favorite text editor or IDE
-
JDK 1.8 or later
-
Maven 3.0+
How to complete this guide
Before we begin let's set up our development environment:
-
Download and unzip the source repository for this guide, or clone it using Git:
git clone https://github.com/paulcwarren/spring-content-gettingstarted.git
-
We are going to start where Getting Started with Spring Content REST API leaves off so
cd
intospring-content-gettingstarted/spring-content-rest/complete
When you’re finished, you can check your results against the code in
spring-content-gettingstarted/spring-content-with-versions/complete
.
Update dependencies
Add the com.github.paulcwarren:spring-versions-boot-starter
and org.springframework.boot:spring-boot-starter-security
dependencies.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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-versions</artifactId>
<parent>
<groupId>com.github.paulcwarren</groupId>
<artifactId>gettingstarted-spring-content</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-fs-boot-starter</artifactId>
<version>${spring.content.version}</version>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-rest-boot-starter</artifactId>
<version>${spring.content.version}</version>
</dependency>
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-versions-jpa-boot-starter</artifactId>
<version>${spring.content.version}</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>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<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>
</project>
Update File Entity
Add the following annotations to our Entity.
src/main/java/gettingstarted/File.java
package gettingstarted;
import java.util.Date;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.annotations.ContentLength;
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
@Getter
@Setter
@NoArgsConstructor
public class File {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private Date created = new Date();
private String summary;
@ContentId private String contentId;
@ContentLength private long contentLength;
private String contentMimeType = "text/plain";
@LockOwner
private String lockOwner;
@AncestorId
private Long ancestorId;
@AncestorRootId
private Long ancestralRootId;
@SuccessorId
private Long successorId;
@VersionNumber
private String version;
@VersionLabel private String label;
public File(File f) {
this.name = f.getName();
this.summary = f.getSummary();
this.contentId = f.getContentId();
this.contentLength = f.getContentLength();
this.contentMimeType = f.getContentMimeType();
}
}
@LockOwner
; tracks the current lock owner. Optional
@AncestorId
; references the entity that came immediately before in the version series
@AncestorRootId
; references the entity that came first in the version series
@SuccessorId
; references the entity that came after in the version series
@VersionNumber
; version designation
@VersionLabel
; description of the version
Also note the copy constructor. An Entity can be complex. It is impossible for Spring Content to know exactly how to stamp out a new version when it needs to. This is where the copy constructor comes in. The copy constructor tells Spring Content how to create a new version. There is no need to copy the version attributes as they will be managed as part of the version creation process. Other than that, it is up to you.
Update FileRepository
So that we can perform version operations on an Entity make your FileRepository extend LockingAndVersioningRepository
.
src/main/java/gettingstarted/FileRepository.java
package gettingstarted;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.versions.LockingAndVersioningRepository;
public interface FileRepository extends JpaRepository<File, Long>, LockingAndVersioningRepository<File, Long> {
}
Update Configuratoin
Add a Store configuration to tell Spring Data JPA to look in the package org.springframework.versions
for the implementation of the LockingAndVersioningRepository
. Because of this you will also need to tell Spring Data to find FileRepository
in the gettingstarted
package.
SpringApplication.run(SpringContentApplication.class, args);
}
@Configuration
Enable web security and add an anonymous user principal.
public static class StoreConfig {
}
@Configuration
@EnableWebSecurity
public static class SpringSecurityConfig /*extends WebSecurityConfigurerAdapter*/ {
protected static String REALM = "SPRING_CONTENT";
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
// Enable if spring-doc apps supports user accounts in the future
auth.inMemoryAuthentication().
withUser(User.withDefaultPasswordEncoder().username("paul").password("warren").roles("ADMIN").
// withUser(User.withDefaultPasswordEncoder().username("john123").password("password").roles("USER").
build());
}
@Bean
public AuthenticationEntryPoint getBasicAuthEntryPoint() {
return new AuthenticationEntryPoint();
}
// @Override
// protected void configure(HttpSecurity http) throws Exception {
//
// http.csrf().disable()
// .authorizeRequests()
// .antMatchers("/admin/**").hasRole("ADMIN")
// .and().httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint())
// .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//
// }
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.and().httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint())
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
// @Override
Build an executable JAR
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-versions-0.0.1.jar
Create an Entity and version it
Create an entity:
curl -X POST -H 'Content-Type:application/hal+json' -d '{}' http://localhost:8080/files/
Associate content:
curl -X PUT -H 'Content-Type:text/plain' -d 'Hello Spring Content with Versions World!' http://localhost:8080/files/1
Lock the Entity to prevent others from editing it:
curl -X PUT -H 'Content-Type:application/hal+json' http://localhost:8080/files/1/lock
Create a new version of the entity:
curl -X PUT -H 'Content-Type:application/hal+json' -d '{"number":"1.1","label":"some minor changes"}' http://localhost:8080/files/1/version
Unlock the new version of the Entity:
curl -X DELETE -H 'Accept:application/hal+json' http://localhost:8080/files/2/lock
Fetch the version series:
curl -H 'Accept:application/hal+json' http://localhost:8080/files/
Verify you now see two entities that are in a version series with each other.
Summary
Congratulations! You've written a simple application that uses Spring Content with Versions to create a version series of entities with associated content.
Don't forget you can simply change the type of the spring-content bootstarter project on the classpath to switch from file storage to a different storage technology. The REST and Version Modules works seamlessly with all of the storage modules.
Spring Content supports the following implementations:-
-
Spring Content Filesystem; stores content as Files on the Filesystem (as used in this tutorial)
-
Spring Content S3; stores content as Objects in Amazon S3
-
Spring Content JPA; stores content as BLOBs in the database
-
Spring Content MongoDB; stores content as Resources in Mongo's GridFS