From 2332520d678884357de5ed7adbaa4ebec624eb9f Mon Sep 17 00:00:00 2001 From: Pavel Manannikov Date: Tue, 13 Oct 2020 20:17:25 +0300 Subject: [PATCH 1/3] #1284 Implement Version Number pattern --- pom.xml | 1 + version-number/README.md | 169 ++++++++++++++++++ version-number/etc/version-number.urm.png | Bin 0 -> 22183 bytes version-number/etc/version-number.urm.puml | 32 ++++ version-number/pom.xml | 66 +++++++ .../java/com/iluwatar/versionnumber/App.java | 84 +++++++++ .../java/com/iluwatar/versionnumber/Book.java | 78 ++++++++ .../versionnumber/BookDuplicateException.java | 33 ++++ .../versionnumber/BookNotFoundException.java | 33 ++++ .../versionnumber/BookRepository.java | 86 +++++++++ .../VersionMismatchException.java | 33 ++++ .../com/iluwatar/versionnumber/AppTest.java | 46 +++++ .../versionnumber/BookRepositoryTest.java | 72 ++++++++ 13 files changed, 733 insertions(+) create mode 100644 version-number/README.md create mode 100644 version-number/etc/version-number.urm.png create mode 100644 version-number/etc/version-number.urm.puml create mode 100644 version-number/pom.xml create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/App.java create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/Book.java create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/BookDuplicateException.java create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/BookNotFoundException.java create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java create mode 100644 version-number/src/main/java/com/iluwatar/versionnumber/VersionMismatchException.java create mode 100644 version-number/src/test/java/com/iluwatar/versionnumber/AppTest.java create mode 100644 version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java 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..f95b1bcc7 --- /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()); + } + + Book 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 +long bookId = 1; +// Alice and Bob took the book concurrently +final Book aliceBook = bookRepository.get(bookId); +final Book 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 0000000000000000000000000000000000000000..95a5819b4b6e8b72335eef595efcfaf3413c5bb0 GIT binary patch literal 22183 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 literal 0 HcmV?d00001 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..8049f1352 --- /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 { + long bookId = 1; + + BookRepository bookRepository = new BookRepository(); + Book 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 Book aliceBook = bookRepository.get(bookId); + final Book 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..a0f46c30f --- /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()); + } + + Book 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..869bd4b44 --- /dev/null +++ b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java @@ -0,0 +1,72 @@ +/* + * 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.*; + +/** + * Tests for {@link BookRepository} + */ +class BookRepositoryTest { + @Test + void testBookRepository() throws BookDuplicateException, BookNotFoundException, VersionMismatchException { + final long bookId = 1; + + BookRepository bookRepository = new BookRepository(); + Book book = new Book(); + book.setId(bookId); + bookRepository.add(book); + + assertEquals(0, book.getVersion()); + + final Book aliceBook = bookRepository.get(bookId); + final Book bobBook = bookRepository.get(bookId); + + assertEquals(aliceBook.getTitle(), bobBook.getTitle()); + assertEquals(aliceBook.getAuthor(), bobBook.getAuthor()); + assertEquals(aliceBook.getVersion(), bobBook.getVersion()); + + aliceBook.setTitle("Kama Sutra"); + bookRepository.update(aliceBook); + + assertEquals(1, aliceBook.getVersion()); + assertEquals(0, bobBook.getVersion()); + assertEquals(aliceBook.getVersion(), bookRepository.get(bookId).getVersion()); + assertEquals(aliceBook.getTitle(), bookRepository.get(bookId).getTitle()); + assertNotEquals(aliceBook.getTitle(), bobBook.getTitle()); + + bobBook.setAuthor("Vatsyayana Mallanaga"); + try { + bookRepository.update(bobBook); + } catch (VersionMismatchException e) { + assertEquals(0, bobBook.getVersion()); + assertEquals(1, bookRepository.get(bookId).getVersion()); + assertEquals(aliceBook.getVersion(), bookRepository.get(bookId).getVersion()); + assertEquals("", bobBook.getTitle()); + assertNotEquals(aliceBook.getAuthor(), bobBook.getAuthor()); + } + } +} From 97e3a3debcb6f09ed5738e8cca94234a68e9c56c Mon Sep 17 00:00:00 2001 From: Pavel Manannikov Date: Mon, 23 Nov 2020 19:42:50 +0200 Subject: [PATCH 2/3] #1284 Use local variable inference --- version-number/README.md | 8 ++++---- .../src/main/java/com/iluwatar/versionnumber/App.java | 10 +++++----- .../com/iluwatar/versionnumber/BookRepository.java | 2 +- .../com/iluwatar/versionnumber/BookRepositoryTest.java | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/version-number/README.md b/version-number/README.md index f95b1bcc7..9a7468a85 100644 --- a/version-number/README.md +++ b/version-number/README.md @@ -78,7 +78,7 @@ public class BookRepository { throw new BookNotFoundException("Not found book with id: " + book.getId()); } - Book latestBook = collection.get(book.getId()); + var latestBook = collection.get(book.getId()); if (book.getVersion() != latestBook.getVersion()) { throw new VersionMismatchException( "Tried to update stale version " + book.getVersion() @@ -107,10 +107,10 @@ public class BookRepository { Here's the concurrency control in action: ```java -long bookId = 1; +var bookId = 1; // Alice and Bob took the book concurrently -final Book aliceBook = bookRepository.get(bookId); -final Book bobBook = bookRepository.get(bookId); +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 diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/App.java b/version-number/src/main/java/com/iluwatar/versionnumber/App.java index 8049f1352..cffba50d4 100644 --- a/version-number/src/main/java/com/iluwatar/versionnumber/App.java +++ b/version-number/src/main/java/com/iluwatar/versionnumber/App.java @@ -53,17 +53,17 @@ public class App { BookDuplicateException, BookNotFoundException, VersionMismatchException { - long bookId = 1; + var bookId = 1; - BookRepository bookRepository = new BookRepository(); - Book book = new Book(); + 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 Book aliceBook = bookRepository.get(bookId); - final Book bobBook = bookRepository.get(bookId); + 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 diff --git a/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java b/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java index a0f46c30f..ef41e79ac 100644 --- a/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java +++ b/version-number/src/main/java/com/iluwatar/versionnumber/BookRepository.java @@ -56,7 +56,7 @@ public class BookRepository { throw new BookNotFoundException("Not found book with id: " + book.getId()); } - Book latestBook = collection.get(book.getId()); + var latestBook = collection.get(book.getId()); if (book.getVersion() != latestBook.getVersion()) { throw new VersionMismatchException( "Tried to update stale version " + book.getVersion() diff --git a/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java index 869bd4b44..325dea87b 100644 --- a/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java +++ b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java @@ -33,17 +33,17 @@ import static org.junit.jupiter.api.Assertions.*; class BookRepositoryTest { @Test void testBookRepository() throws BookDuplicateException, BookNotFoundException, VersionMismatchException { - final long bookId = 1; + final var bookId = 1; - BookRepository bookRepository = new BookRepository(); - Book book = new Book(); + var bookRepository = new BookRepository(); + var book = new Book(); book.setId(bookId); bookRepository.add(book); assertEquals(0, book.getVersion()); - final Book aliceBook = bookRepository.get(bookId); - final Book bobBook = bookRepository.get(bookId); + final var aliceBook = bookRepository.get(bookId); + final var bobBook = bookRepository.get(bookId); assertEquals(aliceBook.getTitle(), bobBook.getTitle()); assertEquals(aliceBook.getAuthor(), bobBook.getAuthor()); From 9ead3adf735d654d8f81081335d117487692eda7 Mon Sep 17 00:00:00 2001 From: Pavel Manannikov Date: Mon, 23 Nov 2020 20:59:00 +0200 Subject: [PATCH 3/3] #1284 Divide tests --- .../versionnumber/BookRepositoryTest.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java index 325dea87b..6b7b2b39a 100644 --- a/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java +++ b/version-number/src/test/java/com/iluwatar/versionnumber/BookRepositoryTest.java @@ -23,6 +23,7 @@ package com.iluwatar.versionnumber; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -31,40 +32,54 @@ import static org.junit.jupiter.api.Assertions.*; * Tests for {@link BookRepository} */ class BookRepositoryTest { - @Test - void testBookRepository() throws BookDuplicateException, BookNotFoundException, VersionMismatchException { - final var bookId = 1; + private final long bookId = 1; + private final BookRepository bookRepository = new BookRepository(); - var 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); - assertEquals(aliceBook.getTitle(), bobBook.getTitle()); - assertEquals(aliceBook.getAuthor(), bobBook.getAuthor()); - assertEquals(aliceBook.getVersion(), bobBook.getVersion()); - aliceBook.setTitle("Kama Sutra"); bookRepository.update(aliceBook); assertEquals(1, aliceBook.getVersion()); assertEquals(0, bobBook.getVersion()); - assertEquals(aliceBook.getVersion(), bookRepository.get(bookId).getVersion()); - assertEquals(aliceBook.getTitle(), bookRepository.get(bookId).getTitle()); + 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()); - assertEquals(1, bookRepository.get(bookId).getVersion()); - assertEquals(aliceBook.getVersion(), bookRepository.get(bookId).getVersion()); + var actualBook = bookRepository.get(bookId); + assertEquals(1, actualBook.getVersion()); + assertEquals(aliceBook.getVersion(), actualBook.getVersion()); assertEquals("", bobBook.getTitle()); assertNotEquals(aliceBook.getAuthor(), bobBook.getAuthor()); }