Merge pull request #1563 from manannikov/Issue#1284
#1284 Implement Version Number pattern
This commit is contained in:
commit
0c44b53909
1
pom.xml
1
pom.xml
@ -89,6 +89,7 @@
|
|||||||
<module>state</module>
|
<module>state</module>
|
||||||
<module>strategy</module>
|
<module>strategy</module>
|
||||||
<module>template-method</module>
|
<module>template-method</module>
|
||||||
|
<module>version-number</module>
|
||||||
<module>visitor</module>
|
<module>visitor</module>
|
||||||
<module>double-checked-locking</module>
|
<module>double-checked-locking</module>
|
||||||
<module>servant</module>
|
<module>servant</module>
|
||||||
|
169
version-number/README.md
Normal file
169
version-number/README.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Version Number
|
||||||
|
folder: versionnumber
|
||||||
|
permalink: /patterns/versionnumber/
|
||||||
|
description: Entity versioning with version number
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- Concurrency
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- Data access
|
||||||
|
- Microservices
|
||||||
|
---
|
||||||
|
|
||||||
|
## Name / classification
|
||||||
|
|
||||||
|
Version Number.
|
||||||
|
|
||||||
|
## Also known as
|
||||||
|
|
||||||
|
Entity Versioning, Optimistic Locking.
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
Resolve concurrency conflicts when multiple clients are trying to update same entity simultaneously.
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
Real world example
|
||||||
|
|
||||||
|
> Alice and Bob are working on the book, which stored in the database. Our heroes are making
|
||||||
|
> changes simultaneously, and we need some mechanism to prevent them from overwriting each other.
|
||||||
|
|
||||||
|
In plain words
|
||||||
|
|
||||||
|
> Version Number pattern grants protection against concurrent updates to same entity.
|
||||||
|
|
||||||
|
Wikipedia says
|
||||||
|
|
||||||
|
> Optimistic concurrency control assumes that multiple transactions can frequently complete
|
||||||
|
> without interfering with each other. While running, transactions use data resources without
|
||||||
|
> acquiring locks on those resources. Before committing, each transaction verifies that no other
|
||||||
|
> transaction has modified the data it has read. If the check reveals conflicting modifications,
|
||||||
|
> the committing transaction rolls back and can be restarted.
|
||||||
|
|
||||||
|
**Programmatic Example**
|
||||||
|
|
||||||
|
We have a `Book` entity, which is versioned, and has a copy-constructor:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Book {
|
||||||
|
private long id;
|
||||||
|
private String title = "";
|
||||||
|
private String author = "";
|
||||||
|
|
||||||
|
private long version = 0; // version number
|
||||||
|
|
||||||
|
public Book(Book book) {
|
||||||
|
this.id = book.id;
|
||||||
|
this.title = book.title;
|
||||||
|
this.author = book.author;
|
||||||
|
this.version = book.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters and setters are omitted here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We also have `BookRepository`, which implements concurrency control:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class BookRepository {
|
||||||
|
private final Map<Long, Book> collection = new HashMap<>();
|
||||||
|
|
||||||
|
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||||
|
if (!collection.containsKey(book.getId())) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestBook = collection.get(book.getId());
|
||||||
|
if (book.getVersion() != latestBook.getVersion()) {
|
||||||
|
throw new VersionMismatchException(
|
||||||
|
"Tried to update stale version " + book.getVersion()
|
||||||
|
+ " while actual version is " + latestBook.getVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update version, including client representation - modify by reference here
|
||||||
|
book.setVersion(book.getVersion() + 1);
|
||||||
|
|
||||||
|
// save book copy to repository
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book get(long bookId) throws BookNotFoundException {
|
||||||
|
if (!collection.containsKey(bookId)) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return copy of the book
|
||||||
|
return new Book(collection.get(bookId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's the concurrency control in action:
|
||||||
|
|
||||||
|
```java
|
||||||
|
var bookId = 1;
|
||||||
|
// Alice and Bob took the book concurrently
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||||
|
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||||
|
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||||
|
|
||||||
|
// now Bob has the stale version of the book with empty title and version = 0
|
||||||
|
// while actual book in database has filled title and version = 1
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||||
|
try {
|
||||||
|
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||||
|
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
// Bob update fails, and book in repository remained untouchable
|
||||||
|
LOGGER.info("Exception: {}", e.getMessage());
|
||||||
|
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Program output:
|
||||||
|
|
||||||
|
```java
|
||||||
|
Alice updates the book with new version 1
|
||||||
|
Bob tries to update the book with his version 0
|
||||||
|
Exception: Tried to update stale version 0 while actual version is 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Applicability
|
||||||
|
|
||||||
|
Use Version Number for:
|
||||||
|
|
||||||
|
* resolving concurrent write-access to the data
|
||||||
|
* strong data consistency
|
||||||
|
|
||||||
|
## Tutorials
|
||||||
|
* [Version Number Pattern Tutorial](http://www.java2s.com/Tutorial/Java/0355__JPA/VersioningEntity.htm)
|
||||||
|
|
||||||
|
## Known uses
|
||||||
|
* [Hibernate](https://vladmihalcea.com/jpa-entity-version-property-hibernate/)
|
||||||
|
* [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning)
|
||||||
|
* [Apache Solr](https://lucene.apache.org/solr/guide/6_6/updating-parts-of-documents.html)
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
Version Number pattern allows to implement a concurrency control, which is usually done
|
||||||
|
via Optimistic Offline Lock pattern.
|
||||||
|
|
||||||
|
## Related patterns
|
||||||
|
* [Optimistic Offline Lock](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
* [Optimistic Locking in JPA](https://www.baeldung.com/jpa-optimistic-locking)
|
||||||
|
* [JPA entity versioning](https://www.byteslounge.com/tutorials/jpa-entity-versioning-version-and-optimistic-locking)
|
||||||
|
* [J2EE Design Patterns](http://ommolketab.ir/aaf-lib/axkwht7wxrhvgs2aqkxse8hihyu9zv.pdf)
|
BIN
version-number/etc/version-number.urm.png
Normal file
BIN
version-number/etc/version-number.urm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
32
version-number/etc/version-number.urm.puml
Normal file
32
version-number/etc/version-number.urm.puml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@startuml
|
||||||
|
package com.iluwatar.versionnumber {
|
||||||
|
class App {
|
||||||
|
- LOGGER : Logger {static}
|
||||||
|
+ App()
|
||||||
|
+ main(args : String[]) {static}
|
||||||
|
}
|
||||||
|
class Book {
|
||||||
|
- author : String
|
||||||
|
- id : long
|
||||||
|
- title : String
|
||||||
|
- version : long
|
||||||
|
+ Book()
|
||||||
|
+ Book(book : Book)
|
||||||
|
+ getAuthor() : String
|
||||||
|
+ getId() : long
|
||||||
|
+ getTitle() : String
|
||||||
|
+ getVersion() : long
|
||||||
|
+ setAuthor(author : String)
|
||||||
|
+ setId(id : long)
|
||||||
|
+ setTitle(title : String)
|
||||||
|
+ setVersion(version : long)
|
||||||
|
}
|
||||||
|
class BookRepository {
|
||||||
|
- collection : Map<Long, Book>
|
||||||
|
+ BookRepository()
|
||||||
|
+ add(book : Book)
|
||||||
|
+ get(bookId : long) : Book
|
||||||
|
+ update(book : Book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@enduml
|
66
version-number/pom.xml
Normal file
66
version-number/pom.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.24.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>version-number</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.iluwatar.versionnumber.App</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Version Number pattern helps to resolve concurrency conflicts in applications.
|
||||||
|
* Usually these conflicts arise in database operations, when multiple clients are trying
|
||||||
|
* to update the same record simultaneously.
|
||||||
|
* Resolving such conflicts requires determining whether an object has changed.
|
||||||
|
* For this reason we need a version number that is incremented with each change
|
||||||
|
* to the underlying data, e.g. database. The version number can be used by repositories
|
||||||
|
* to check for external changes and to report concurrency issues to the users.
|
||||||
|
*
|
||||||
|
* <p>In this example we show how Alice and Bob will try to update the {@link Book}
|
||||||
|
* and save it simultaneously to {@link BookRepository}, which represents a typical database.
|
||||||
|
*
|
||||||
|
* <p>As in real databases, each client operates with copy of the data instead of original data
|
||||||
|
* passed by reference, that's why we are using {@link Book} copy-constructor here.
|
||||||
|
*/
|
||||||
|
public class App {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Program entry point.
|
||||||
|
*
|
||||||
|
* @param args command line args
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws
|
||||||
|
BookDuplicateException,
|
||||||
|
BookNotFoundException,
|
||||||
|
VersionMismatchException {
|
||||||
|
var bookId = 1;
|
||||||
|
|
||||||
|
var bookRepository = new BookRepository();
|
||||||
|
var book = new Book();
|
||||||
|
book.setId(bookId);
|
||||||
|
bookRepository.add(book); // adding a book with empty title and author
|
||||||
|
LOGGER.info("An empty book with version {} was added to repository", book.getVersion());
|
||||||
|
|
||||||
|
// Alice and Bob took the book concurrently
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||||
|
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||||
|
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||||
|
|
||||||
|
// now Bob has the stale version of the book with empty title and version = 0
|
||||||
|
// while actual book in database has filled title and version = 1
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||||
|
try {
|
||||||
|
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||||
|
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
// Bob update fails, and book in repository remained untouchable
|
||||||
|
LOGGER.info("Exception: {}", e.getMessage());
|
||||||
|
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
public class Book {
|
||||||
|
private long id;
|
||||||
|
private String title = "";
|
||||||
|
private String author = "";
|
||||||
|
|
||||||
|
private long version = 0; // version number
|
||||||
|
|
||||||
|
public Book() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need this copy constructor to copy book representation in {@link BookRepository}.
|
||||||
|
*/
|
||||||
|
public Book(Book book) {
|
||||||
|
this.id = book.id;
|
||||||
|
this.title = book.title;
|
||||||
|
this.author = book.author;
|
||||||
|
this.version = book.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthor(String author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(long version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When someone has tried to add a book which repository already have.
|
||||||
|
*/
|
||||||
|
public class BookDuplicateException extends Exception {
|
||||||
|
public BookDuplicateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client has tried to make an operation with book which repository does not have.
|
||||||
|
*/
|
||||||
|
public class BookNotFoundException extends Exception {
|
||||||
|
public BookNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This repository represents simplified database.
|
||||||
|
* As a typical database do, repository operates with copies of object.
|
||||||
|
* So client and repo has different copies of book, which can lead to concurrency conflicts
|
||||||
|
* as much as in real databases.
|
||||||
|
*/
|
||||||
|
public class BookRepository {
|
||||||
|
private final Map<Long, Book> collection = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds book to collection.
|
||||||
|
* Actually we are putting copy of book (saving a book by value, not by reference);
|
||||||
|
*/
|
||||||
|
public void add(Book book) throws BookDuplicateException {
|
||||||
|
if (collection.containsKey(book.getId())) {
|
||||||
|
throw new BookDuplicateException("Duplicated book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// add copy of the book
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates book in collection only if client has modified the latest version of the book.
|
||||||
|
*/
|
||||||
|
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||||
|
if (!collection.containsKey(book.getId())) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestBook = collection.get(book.getId());
|
||||||
|
if (book.getVersion() != latestBook.getVersion()) {
|
||||||
|
throw new VersionMismatchException(
|
||||||
|
"Tried to update stale version " + book.getVersion()
|
||||||
|
+ " while actual version is " + latestBook.getVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update version, including client representation - modify by reference here
|
||||||
|
book.setVersion(book.getVersion() + 1);
|
||||||
|
|
||||||
|
// save book copy to repository
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns book representation to the client.
|
||||||
|
* Representation means we are returning copy of the book.
|
||||||
|
*/
|
||||||
|
public Book get(long bookId) throws BookNotFoundException {
|
||||||
|
if (!collection.containsKey(bookId)) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return copy of the book
|
||||||
|
return new Book(collection.get(bookId));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client has tried to update a stale version of the book.
|
||||||
|
*/
|
||||||
|
public class VersionMismatchException extends Exception {
|
||||||
|
public VersionMismatchException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application test
|
||||||
|
*/
|
||||||
|
class AppTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue: Add at least one assertion to this test case.
|
||||||
|
*
|
||||||
|
* Solution: Inserted assertion to check whether the execution of the main method in {@link App#main(String[])}
|
||||||
|
* throws an exception.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExecuteApplicationWithoutException() {
|
||||||
|
assertDoesNotThrow(() -> App.main(new String[]{}));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link BookRepository}
|
||||||
|
*/
|
||||||
|
class BookRepositoryTest {
|
||||||
|
private final long bookId = 1;
|
||||||
|
private final BookRepository bookRepository = new BookRepository();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws BookDuplicateException {
|
||||||
|
var book = new Book();
|
||||||
|
book.setId(bookId);
|
||||||
|
bookRepository.add(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultVersionRemainsZeroAfterAdd() throws BookNotFoundException {
|
||||||
|
var book = bookRepository.get(bookId);
|
||||||
|
assertEquals(0, book.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAliceAndBobHaveDifferentVersionsAfterAliceUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra");
|
||||||
|
bookRepository.update(aliceBook);
|
||||||
|
|
||||||
|
assertEquals(1, aliceBook.getVersion());
|
||||||
|
assertEquals(0, bobBook.getVersion());
|
||||||
|
var actualBook = bookRepository.get(bookId);
|
||||||
|
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||||
|
assertEquals(aliceBook.getTitle(), actualBook.getTitle());
|
||||||
|
assertNotEquals(aliceBook.getTitle(), bobBook.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldThrowVersionMismatchExceptionOnStaleUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra");
|
||||||
|
bookRepository.update(aliceBook);
|
||||||
|
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga");
|
||||||
|
try {
|
||||||
|
bookRepository.update(bobBook);
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
assertEquals(0, bobBook.getVersion());
|
||||||
|
var actualBook = bookRepository.get(bookId);
|
||||||
|
assertEquals(1, actualBook.getVersion());
|
||||||
|
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||||
|
assertEquals("", bobBook.getTitle());
|
||||||
|
assertNotEquals(aliceBook.getAuthor(), bobBook.getAuthor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user