diff --git a/pom.xml b/pom.xml index 102668588..e7fd2e4b3 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,7 @@ state strategy template-method + version-number visitor double-checked-locking servant diff --git a/version-number/README.md b/version-number/README.md new file mode 100644 index 000000000..9a7468a85 --- /dev/null +++ b/version-number/README.md @@ -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 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 + +![alt text](etc/version-number.urm.png "Version Number pattern 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) diff --git a/version-number/etc/version-number.urm.png b/version-number/etc/version-number.urm.png new file mode 100644 index 000000000..95a5819b4 Binary files /dev/null and b/version-number/etc/version-number.urm.png differ diff --git a/version-number/etc/version-number.urm.puml b/version-number/etc/version-number.urm.puml new file mode 100644 index 000000000..6f3c3364e --- /dev/null +++ b/version-number/etc/version-number.urm.puml @@ -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 + + BookRepository() + + add(book : Book) + + get(bookId : long) : Book + + update(book : Book) + } +} +@enduml \ No newline at end of file diff --git a/version-number/pom.xml b/version-number/pom.xml new file mode 100644 index 000000000..b5748c93b --- /dev/null +++ b/version-number/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.24.0-SNAPSHOT + + version-number + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.versionnumber.App + + + + + + + + + diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/App.java b/version-number/src/main/java/com/iluwatar/versionnumber/App.java new file mode 100644 index 000000000..cffba50d4 --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/App.java @@ -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. + * + *

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. + * + *

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 + } + } +} diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/Book.java b/version-number/src/main/java/com/iluwatar/versionnumber/Book.java new file mode 100644 index 000000000..93b35880b --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/Book.java @@ -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; + } +} diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/BookDuplicateException.java b/version-number/src/main/java/com/iluwatar/versionnumber/BookDuplicateException.java new file mode 100644 index 000000000..cd993b147 --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/BookDuplicateException.java @@ -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); + } +} diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/BookNotFoundException.java b/version-number/src/main/java/com/iluwatar/versionnumber/BookNotFoundException.java new file mode 100644 index 000000000..f832f350f --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/BookNotFoundException.java @@ -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); + } +} diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java b/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java new file mode 100644 index 000000000..ef41e79ac --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java @@ -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 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)); + } +} diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/VersionMismatchException.java b/version-number/src/main/java/com/iluwatar/versionnumber/VersionMismatchException.java new file mode 100644 index 000000000..94ea0fd9e --- /dev/null +++ b/version-number/src/main/java/com/iluwatar/versionnumber/VersionMismatchException.java @@ -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); + } +} diff --git a/version-number/src/test/java/com/iluwatar/versionnumber/AppTest.java b/version-number/src/test/java/com/iluwatar/versionnumber/AppTest.java new file mode 100644 index 000000000..7b4984901 --- /dev/null +++ b/version-number/src/test/java/com/iluwatar/versionnumber/AppTest.java @@ -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[]{})); + } +} diff --git a/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java new file mode 100644 index 000000000..6b7b2b39a --- /dev/null +++ b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java @@ -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()); + } + } +}