© 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. Introduction

Traditional document management use cases often require content to be locked and versioned. The Spring Versions JPA module provides these capabilities.

Spring Versions JPA can be used with the following storage modules:

2. Getting Started

2.1. Introduction

Spring Versions are enhancements to Spring Data that extend the existing optimistic locking semantics of Entities to any associated Spring Content Resources and also adds user-facing pessimistic locking and versioning semantics to Entities and their Content through a new LockingAndVersioningRepository interface defined in the base module Spring Version Commons.

Spring Versions JPA is the JPA-based implementation of these enhancements.

2.2. Adding Spring Versions JPA to a Spring Boot project

Spring Versions JPA has both a Spring Boot starter and auto-configuration so the simplest way to get to started is with a Spring Boot application. Add the spring-versions-jpa-boot-starter to the classpath:

Example 1. Classpath configuration with Gradle
dependencies {
    ...
    compile("com.github.paulcwarren:spring-versions-jpa-boot-starter:${version}")
	...
}
Example 2. Classpath configuration with Maven
 <dependencies>
	...
    <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-versions-jpa-boot-starter</artifactId>
      <version>${version}</version>
    </dependency>
	...
  </dependencies>

2.3. Adding Spring Versions JPA to a Non-Spring Boot Project

Whilst it is the easiest way to get started Spring Versions JPA does not require Spring Boot however. To a add Spring Versions JPA to a non-Spring Boot project, add the spring-versions-jpa dependency to the classpath:

Example 3. Classpath configuration for Gradle
dependencies {
    ...
    compile("com.github.paulcwarren:spring-versions-jpa:${version}")
	...
}
Example 4. Classpath configuration for Maven
 <dependencies>
	...
    <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-versions-jpa</artifactId>
      <version>${version}</version>
    </dependency>
	...
  </dependencies>

2.4. Configuring Spring Versions JPA

Before they can be used, the locking and versioning capabilities must be configured. This is done by importing the @Configuration class org.springframework.versions.jpa.JpaLockingAndVersioningConfig into your application and initializing the database with the pre-defined sql scripts:

@Configuration
@Import(JpaLockingAndVersioningConfig.class)
public class LockingAndVersioningConfig {

    @Value("/org/springframework/versions/jpa/schema-drop-hsqldb.sql") // (1)
    private Resource dropVersionSchema;

    @Value("/org/springframework/versions/jpa/schema-hsqldb.sql")
    private Resource createVersionSchema;

    @Bean
    DataSourceInitializer datasourceInitializer(DataSource dataSource) {
        ResourceDatabasePopulator databasePopulator =
                new ResourceDatabasePopulator();

        databasePopulator.addScript(dropVersionSchema);
        databasePopulator.addScript(createVersionSchema);
        databasePopulator.setIgnoreFailedDrops(true);

        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource);
        initializer.setDatabasePopulator(databasePopulator);

        return initializer;
    }

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.HSQL).build();
    }
}
  1. Replace with schema resources for database. Supported databases; H2, HSQL, MySQL, Postgres and SQL Server.

Important
This step is unnecessary if you are using Spring Boot as it will automatically enable Spring Versions JPA when you place com.github.paulcwarren:spring-versions-jpa-boot-starter on your application’s classpath and your application is annotated as a @SpringBootApplication.

Configure the Spring Content Storage module by adding the package org.springframework.versions to your @Enable…​Repositories package context.

@SpringBootApplication
public class Application {

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

    @Configuration
    @EnableJpaRepositories(basePackages={"tests.versioning", "org.springframework.versions"})	// (1)
    @EnableFilesystemStores(basePackages = "tests.versioning")
    public static class ApplicationConfiguration {
    }
}
  1. This is required whether you are using Spring Boot, or not.

3. Locking And Versioning

3.1. Fundamentals

Locking and Versioning supports both optimistic and pessimistic locking strategies as well as versioning of Spring Data Entities and associated Spring Content Resources.

3.2. Optimistic Locking

Once configured (see Configuring Spring Versions JPA) optimistic locking semantics are automatically extended to Spring Content Resource’s associated with @Versioned Spring Data Entities.

As a result any ContentStore operation that is attempted on an out-of-date Entity, that with an out-of-date @Version value, will throw javax.persistence.OptimisticLockException.

In addition, any ContentStore operation on an up-to-date Entity’s content will cause the @Version value to be incremented rendering all other copies out-of-date and forcing those to be re-fetched.

3.3. Pessimistic Locking and Versioning

To use a pessimistic locking strategy and versioning, the repository should be made to extend LockingAndVersioningRepository. For example:

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

The LockingAndVersioningRepository interface adds (and overrides) the following methods to a Repository:

public interface LockingAndVersioningRepository<T, ID extends Serializable> {

    /**
     * Locks the entity and returns the updated entity (@Version and @LockOwner) attributes updated, otherwise
     * returns null.
     *
     * @param <S> the type of entity
     * @param entity the entity to be locked
     * @return the locked entity
     * @throws SecurityException if no authentication exists
     */
    <S extends T> S lock(S entity);

    /**
     * Unlocks the entity and returns the updated entity (@Version and @LockOwner) attributes updated, otherwise
     * returns null
     *
     * @param <S> the type of entity
     * @param entity the entity to unlock
     * @return the unlocked entity
     * @throws LockOwnerException if the current principal is not the lock owner
     * @throws SecurityException if no authentication exists
     */
    <S extends T> S unlock(S entity);

    /**
     * Overridden implementation of save that enforces locking semantics
     *
     * @param <S> the type of entity
     * @param entity the entity to save
     * @return the saved entity
     * @throws LockOwnerException if the current principal is not the lock owner
     * @throws SecurityException if no authentication exists
     */
    <S extends T> S save(S entity);

    /**
     * Creates and returns a new version of the entity.  This new version becomes the latest version in the version
     * list.
     *
     * This method requires the entity class to have a copy constructor used for cloning the new version instance.
     *
     * @param <S> the type of entity
     * @param entity the entity to base the new versionWithEntity on
     * @param info the version info
     * @return the new versionWithEntity
     * @throws LockingAndVersioningException if entity is not the latest
     * @throws LockOwnerException if the current principal is not the lock owner
     * @throws SecurityException if no authentication exists
     */
    <S extends T> S version(S entity, VersionInfo info);

    /**
     * Returns the latest version of all entities.  When extending LockingAndVersioningRepository this
     * method would usually be preferred over CrudRepository's findAll that would find all versions
     * of all entities.
     *
     * @param <S> the type of entity
     * @return list of latest versionWithEntity entities
     */
    <S extends T> List<S> findAllLatestVersion();

    /**
     * Returns a list of all versions for the given entity.
     *
     * @param <S> the type of entity
     * @param entity the entity to find versions for
     * @return list of entity versions
     */
    <S extends T> List<S> findAllVersions(@Param("entity") S entity);

    /**
     * Deletes a given entity version.  The entity must be the head of the version list.
     *
     * If the entity is locked the lock will be carried over to the previous version when
     * it becomes the new head.
     *
     * @param <S> the type of entity
     * @param entity the entity to delete
     * @throws LockingAndVersioningException if entity is not the latest
     * @throws LockOwnerException if the current principal is not the lock owner
     * @throws SecurityException if no authentication exists
     */
    <S extends T> void delete(S entity);
}

Lock and version information is recorded on each Entity instance by adding the following attribute annotations to the Entity class that is the subject of LockingAndVersioningRepository:

Annotation Type Required Description

AncestorId

@Id

Yes

The previous version in the set. The type of this field will be dictated by the entity’s @Id field.

AncestorRootId

@Id

Yes

The first version in the set. The type of this field will be dictated by the entity’s @Id field.

SuccessorId

@Id

Yes

The next version in the set. The type of this field will be dictated by the entity’s @Id field.

LockOwner

String

No

The name of the lock owner.

VersionNumber

String

No

The entity’s version number.

VersionLabel

String

No

The entity’s version label.

public class Document {

    @Id
    @GeneratedValue
    private Long id;

    @Version
    private Long vstamp;

    @ContentId
    private UUID contentId;

    @ContentLength
    private int contentLen;

    @MimeType
    private String mimeType;

    @LockOwner
    private String lockOwner;

    @AncestorId
    private Long ancestorId;

    @AncestorRootId
    private Long ancestralRootId;

    @SuccessorId
    private Long successorId;

    @VersionNumber
    private String version;

    @VersionLabel
    private String label;
}

4. REST API

4.1. The Lock Resource

When a repository extending LockingAndVersioningRepository is exported a lock endpoint will be available at the /{repository}/{id}/lock URI.

  curl -X PUT http://localhost:8080/docs/1234/lock
  curl -X DELETE http://localhost:8080/docs/1234/lock

4.1.1. Supported HTTP Methods

The Lock resource supports PUT and DELETE. All other HTTP methods will cause a 405 Method Not Allowed.

PUT

Acquires a pessimistic lock on the resource.

DELETE

If held, releases the pessimistic lock on the resource.

Supported media types

Not applicable.

4.2. The Version Resource

When a repository extending LockingAndVersioningRepository is exported a version endpoint will be available at the /{repository}/{id}/version URI.

  curl -X PUT http://localhost:8080/docs/1234/version -d '{"number": "1.1", "label": "a minor change"}'

4.2.1. Supported HTTP Methods

The version resource supports PUT. All other HTTP methods will cause a 405 Method Not Allowed.

PUT

Creates a new version of the entity.

Supported media types

application/json

4.3. The FindAllVersionsLatest Resource

When a repository extending LockingAndVersioningRepository is exported a findAllVersionsLatest endpoint will be available at the /{repository}/findAllVersionsLatest URI.

  curl -X GET http://localhost:8080/docs/findAllVersionsLatest

4.3.1. Supported HTTP Methods

The version resource supports GET. All other HTTP methods will cause a 405 Method Not Allowed.

GET

Returns the latest version of all entities.

Supported media types

Not applicable.

4.4. The FindAllVersions Resource

When a repository extending LockingAndVersioningRepository is exported a findAllLatestVersions endpoint will be available at the /{repository}/{id}/findAllVersions URI.

  curl -X GET http://localhost:8080/docs/1234/findAllVersions

4.4.1. Supported HTTP Methods

The version resource supports GET. All other HTTP methods will cause a 405 Method Not Allowed.

GET

Returns all versions of the given entity.

Supported media types

Not applicable.