From 689fc2887c54e2e1407caa8fcf5e69e3757371da Mon Sep 17 00:00:00 2001 From: TatuJLund Date: Sun, 26 May 2024 18:34:06 +0300 Subject: [PATCH] Pessimistic locking example --- .../tatu/vaadincreate/VaadinCreateTheme.java | 1 + .../tatu/vaadincreate/crud/BookGrid.java | 44 +++++++++++-- .../vaadincreate/crud/BooksPresenter.java | 31 ++++++++- .../tatu/vaadincreate/crud/LockedBooks.java | 30 +++++++++ .../vaadincreate/crud/LockedBooksImpl.java | 64 +++++++++++++++++++ .../themes/vaadincreate/vaadincreate.scss | 9 ++- .../tatu/vaadincreate/AbstractUITest.java | 2 +- 7 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooks.java create mode 100644 vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooksImpl.java diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/VaadinCreateTheme.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/VaadinCreateTheme.java index 96e8fff..80568c2 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/VaadinCreateTheme.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/VaadinCreateTheme.java @@ -24,6 +24,7 @@ public class VaadinCreateTheme { public static final String BOOKVIEW_GRID_ALIGNRIGHT = "bookview-grid-alignright"; public static final String BOOKVIEW_GRID_EDITED = "bookview-grid-edited"; + public static final String BOOKVIEW_GRID_LOCKED = "bookview-grid-locked"; public static final String BOOKVIEW_AVAILABILITYLABEL = "bookview-availabilitylabel"; public static final String BOOKFORM_FORM = "bookform-form"; diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BookGrid.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BookGrid.java index 3a4c3cc..33e246b 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BookGrid.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BookGrid.java @@ -5,14 +5,20 @@ import java.util.Comparator; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.vaadin.tatu.vaadincreate.VaadinCreateTheme; import org.vaadin.tatu.vaadincreate.backend.data.Availability; import org.vaadin.tatu.vaadincreate.backend.data.Category; import org.vaadin.tatu.vaadincreate.backend.data.Product; +import org.vaadin.tatu.vaadincreate.crud.LockedBooks.BookEvent; +import org.vaadin.tatu.vaadincreate.eventbus.EventBus; +import org.vaadin.tatu.vaadincreate.eventbus.EventBus.EventBusListener; import org.vaadin.tatu.vaadincreate.i18n.HasI18N; import org.vaadin.tatu.vaadincreate.util.Utils; import com.vaadin.data.ValueContext; +import com.vaadin.data.provider.ListDataProvider; import com.vaadin.icons.VaadinIcons; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.ContentMode; @@ -28,7 +34,8 @@ * data sets. */ @SuppressWarnings("serial") -public class BookGrid extends Grid implements HasI18N { +public class BookGrid extends Grid + implements HasI18N, EventBusListener { private static final String CATEGORIES = "categories"; private static final String IN_STOCK = "in-stock"; @@ -39,6 +46,8 @@ public class BookGrid extends Grid implements HasI18N { private Registration resizeReg; private Label availabilityCaption; + private LockedBooks lockedBooks = LockedBooks.get(); + private EventBus eventBus = EventBus.get(); private Product editedProduct; private int edited; @@ -49,9 +58,15 @@ public BookGrid() { setSizeFull(); // Set highlight color to last edited row with style generator. - setStyleGenerator(book -> book.getId() == edited - ? VaadinCreateTheme.BOOKVIEW_GRID_EDITED - : ""); + setStyleGenerator(book -> { + if (book.getId() == edited) { + return VaadinCreateTheme.BOOKVIEW_GRID_EDITED; + } + if (lockedBooks.lockedBooks().contains(book.getId())) { + return VaadinCreateTheme.BOOKVIEW_GRID_LOCKED; + } + return ""; + }); addColumn(Product::getId, new NumberRenderer()).setCaption("Id") .setResizable(false); @@ -95,6 +110,8 @@ public BookGrid() { // Show all categories the product is in, separated by commas addColumn(this::formatCategories).setCaption(getTranslation(CATEGORIES)) .setResizable(false).setSortable(false); + + eventBus.registerEventBusListener(this); } public Product getSelectedRow() { @@ -231,6 +248,7 @@ public void detach() { // It is necessary to remove resize listener upon detach to avoid // resource leakage. resizeReg.remove(); + eventBus.unregisterEventBusListener(this); super.detach(); } @@ -245,4 +263,22 @@ public void setEdited(Product product) { edited = product != null ? product.getId() : -1; editedProduct = product; } + + @SuppressWarnings("unchecked") + @Override + public void eventFired(Object event) { + if (event instanceof BookEvent && isAttached()) { + logger.info("Book locking update"); + var bookEvent = (BookEvent) event; + getUI().access(() -> { + ListDataProvider dataProvider = (ListDataProvider) getDataProvider(); + dataProvider.getItems().stream() + .filter(book -> book.getId() == bookEvent.getId()) + .findFirst().ifPresent( + product -> dataProvider.refreshItem(product)); + }); + } + } + + private Logger logger = LoggerFactory.getLogger(this.getClass()); } diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java index 1cf418c..9d05b1c 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import org.vaadin.tatu.vaadincreate.VaadinCreateUI; import org.vaadin.tatu.vaadincreate.auth.AccessControl; +import org.vaadin.tatu.vaadincreate.auth.CurrentUser; import org.vaadin.tatu.vaadincreate.backend.ProductDataService; import org.vaadin.tatu.vaadincreate.backend.data.Category; import org.vaadin.tatu.vaadincreate.backend.data.Product; @@ -34,6 +35,8 @@ public class BooksPresenter implements Serializable { .getProductService(); private AccessControl accessControl = VaadinCreateUI.get() .getAccessControl(); + private LockedBooks lockedBooks = LockedBooks.get(); + private Integer editing; public BooksPresenter(BooksView simpleCrudView) { view = simpleCrudView; @@ -59,6 +62,7 @@ public void requestUpdateProducts() { } public void cancelUpdateProducts() { + unlockBook(); if (future != null) { boolean cancelled = future.cancel(true); future = null; @@ -76,6 +80,22 @@ public void init() { public void cancelProduct() { view.cancelProduct(); + unlockBook(); + } + + private void unlockBook() { + if (editing != null) { + lockedBooks.unlock(editing); + editing = null; + } + } + + private void lockBook(Integer id) { + if (editing != null) { + unlockBook(); + } + lockedBooks.lock(id); + editing = id; } public void enter(String productId) { @@ -114,6 +134,7 @@ public void saveProduct(Product product) { view.updateGrid(savedProduct); } else { view.updateProduct(savedProduct); + unlockBook(); } view.setFragmentParameter(""); } @@ -124,14 +145,17 @@ public void deleteProduct(Product product) { logger.info("Deleting product: {}", product.getId()); service.deleteProduct(product.getId()); view.removeProduct(product); + unlockBook(); view.setFragmentParameter(""); } public void editProduct(Product product) { if (product == null) { view.setFragmentParameter(""); + unlockBook(); } else { view.setFragmentParameter(product.getId() + ""); + lockBook(product.getId()); } logger.info("Editing product: {}", product != null ? product.getId() : "none"); @@ -147,7 +171,12 @@ public void newProduct() { public void rowSelected(Product product) { if (accessControl.isUserInRole(Role.ADMIN)) { - editProduct(product); + if (product != null + && lockedBooks.lockedBooks().contains(product.getId())) { + view.clearSelection(); + } else { + editProduct(product); + } } } diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooks.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooks.java new file mode 100644 index 0000000..c78e705 --- /dev/null +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooks.java @@ -0,0 +1,30 @@ +package org.vaadin.tatu.vaadincreate.crud; + +import java.io.Serializable; +import java.util.Collection; + +@SuppressWarnings("serial") +public interface LockedBooks { + + public Collection lockedBooks(); + + public void lock(Integer id); + + public void unlock(Integer id); + + public static LockedBooks get() { + return LockedBooksImpl.getInstance(); + } + + public static class BookEvent implements Serializable { + private Integer id; + + public BookEvent(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + } +} diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooksImpl.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooksImpl.java new file mode 100644 index 0000000..a2d8f0b --- /dev/null +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/LockedBooksImpl.java @@ -0,0 +1,64 @@ +package org.vaadin.tatu.vaadincreate.crud; + +import java.util.Collection; +import java.util.WeakHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vaadin.tatu.vaadincreate.eventbus.EventBus; + +@SuppressWarnings("serial") +public class LockedBooksImpl implements LockedBooks { + + private static LockedBooksImpl INSTANCE; + private EventBus eventBus = EventBus.get(); + + private final WeakHashMap books = new WeakHashMap<>(); + + public synchronized static LockedBooks getInstance() { + if (INSTANCE == null) { + INSTANCE = new LockedBooksImpl(); + } + return INSTANCE; + } + + private LockedBooksImpl() { + } + + @Override + public Collection lockedBooks() { + synchronized (books) { + return books.keySet(); + } + } + + @Override + public void lock(Integer id) { + synchronized (books) { + var match = books.keySet().stream().filter(i -> i.equals(id)) + .findFirst(); + if (match.isPresent()) { + throw new IllegalStateException( + "Can't open book already opened: " + id); + } + books.put(id, null); + eventBus.post(new BookEvent(id)); + logger.info("Locked book {}", id); + } + } + + @Override + public void unlock(Integer id) { + synchronized (books) { + var match = books.keySet().stream().filter(i -> i.equals(id)) + .findFirst(); + if (match.isPresent()) { + books.remove(match.get()); + } + eventBus.post(new BookEvent(id)); + logger.info("Unlocked book {}", id); + } + } + + private Logger logger = LoggerFactory.getLogger(this.getClass()); +} diff --git a/vaadincreate-ui/src/main/webapp/VAADIN/themes/vaadincreate/vaadincreate.scss b/vaadincreate-ui/src/main/webapp/VAADIN/themes/vaadincreate/vaadincreate.scss index 71f8f46..105d47d 100644 --- a/vaadincreate-ui/src/main/webapp/VAADIN/themes/vaadincreate/vaadincreate.scss +++ b/vaadincreate-ui/src/main/webapp/VAADIN/themes/vaadincreate/vaadincreate.scss @@ -78,6 +78,7 @@ $fake-grid-background-color: white; @import "../valo/valo.scss"; +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @mixin vaadincreate { @include valo; @@ -435,9 +436,15 @@ $fake-grid-background-color: white; .bookview-grid-edited { color: $v-focus-color; } + + .bookview-grid-locked:not(.v-grid-row-selected) { + opacity: $v-disabled-opacity; + pointer-events: none; + animation: fadeIn 1s ease-in-out 0s; + } .bookview-grid-descriptioncaption { - color: $highlight-contrast-color; + color: $highlight-contrast-color; } .whitespace-pre { diff --git a/vaadincreate-ui/src/test/java/org/vaadin/tatu/vaadincreate/AbstractUITest.java b/vaadincreate-ui/src/test/java/org/vaadin/tatu/vaadincreate/AbstractUITest.java index d712ba0..d660aa6 100644 --- a/vaadincreate-ui/src/test/java/org/vaadin/tatu/vaadincreate/AbstractUITest.java +++ b/vaadincreate-ui/src/test/java/org/vaadin/tatu/vaadincreate/AbstractUITest.java @@ -49,7 +49,7 @@ protected void waitForGrid(VerticalLayout layout, BookGrid grid) { protected void waitForCharts(VerticalLayout layout, CssLayout dashboard) { assertFalse(dashboard.getStyleName().contains("loaded")); - waitWhile(dashboard, d -> !d.getStyleName().contains("loaded"), 10); + waitWhile(dashboard, d -> !d.getStyleName().contains("loaded"), 15); } }