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 into spring-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