From ed02ec33a370fcfaffc74d3eb51be52e409658c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20G=C3=B6tze?= Date: Wed, 7 Aug 2024 13:50:28 +0200 Subject: [PATCH] feat: Add CompletableFuture WebStorage API (#19747) Adds CompletableFuture methods for WebStorage.getItem. Fixes #19743 --- .../page/PendingJavaScriptResult.java | 6 +- .../flow/component/page/WebStorage.java | 103 ++++++++++++++++-- .../vaadin/flow/uitest/ui/WebStorageView.java | 15 ++- .../vaadin/flow/uitest/ui/WebStorageIT.java | 51 ++++++++- 4 files changed, 157 insertions(+), 18 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/component/page/PendingJavaScriptResult.java b/flow-server/src/main/java/com/vaadin/flow/component/page/PendingJavaScriptResult.java index dc5b9cf0c71..c02494027ec 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/page/PendingJavaScriptResult.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/page/PendingJavaScriptResult.java @@ -30,9 +30,9 @@ * evaluation. *

* If any of the then or toCompletableFuture methods - * have been invoked before the snippet is sent to the browser, then the there - * will be an additional round trip for sending the results of the evaluation - * back to any registered handler. If the JavaScript execution returns a + * have been invoked before the snippet is sent to the browser, then there will + * be an additional round trip for sending the results of the evaluation back to + * any registered handler. If the JavaScript execution returns a * Promise, then the result will be sent to the server only when it * is resolved. *

diff --git a/flow-server/src/main/java/com/vaadin/flow/component/page/WebStorage.java b/flow-server/src/main/java/com/vaadin/flow/component/page/WebStorage.java index b534fa2a00d..4ec756d266f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/page/WebStorage.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/page/WebStorage.java @@ -15,8 +15,12 @@ */ package com.vaadin.flow.component.page; -import com.vaadin.flow.component.UI; import java.io.Serializable; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.UI; /** * Wrapper for similarly named Browser API. WebStorage may be handy to save some @@ -177,20 +181,21 @@ public static void clear(UI ui, Storage storage) { } /** - * Asynchronously gets an item from the Storage.localStorage + * Asynchronously gets an item from the local storage. * * @param key * the key for which the value will be fetched * @param callback * the callback that gets the value once transferred from the - * client side + * client side or null if the value was not + * available. */ public static void getItem(String key, Callback callback) { getItem(Storage.LOCAL_STORAGE, key, callback); } /** - * Asynchronously gets an item from the given storage + * Asynchronously gets an item from the given storage. * * @param storage * the storage @@ -198,14 +203,15 @@ public static void getItem(String key, Callback callback) { * the key for which the value will be fetched * @param callback * the callback that gets the value once transferred from the - * client side + * client side or null if the value was not + * available. */ public static void getItem(Storage storage, String key, Callback callback) { getItem(UI.getCurrent(), storage, key, callback); } /** - * Asynchronously gets an item from the given storage + * Asynchronously gets an item from the given storage. * * @param ui * the UI for which the storage is related to @@ -215,17 +221,90 @@ public static void getItem(Storage storage, String key, Callback callback) { * the key for which the value will be fetched * @param callback * the callback that gets the value once transferred from the - * client side + * client side or null if the value was not + * available. */ public static void getItem(UI ui, Storage storage, String key, Callback callback) { - ui.getPage() - .executeJs("return window[$0].getItem($1);", storage.toString(), - key) - .then(String.class, callback::onValueDetected, s -> { - // for error (most likely non-existing mapping), return null + requestItem(ui, storage, key).then(String.class, + callback::onValueDetected, s -> { + LoggerFactory.getLogger(WebStorage.class.getName()).debug( + "Error while getting value for key '{}' from storage '{}': {}", + key, storage, s); + // fallback to null if there was an error callback.onValueDetected(null); }); } + /** + * Asynchronously gets an item from the local storage. + *

+ * It is not possible to synchronously wait for the result of the execution + * while holding the session lock since the request handling thread that + * makes the result available will also need to lock the session.
+ * See {@link PendingJavaScriptResult#toCompletableFuture} for more + * information. + * + * @param key + * the key for which the value will be fetched + * @return a CompletableFuture that will be completed with the value once + * transferred from the client side or null if the + * value was not available. + */ + public static CompletableFuture getItem(String key) { + return getItem(Storage.LOCAL_STORAGE, key); + } + + /** + * Asynchronously gets an item from the given storage. + *

+ * It is not possible to synchronously wait for the result of the execution + * while holding the session lock since the request handling thread that + * makes the result available will also need to lock the session.
+ * See {@link PendingJavaScriptResult#toCompletableFuture} for more + * information. + * + * @param storage + * the storage + * @param key + * the key for which the value will be fetched + * @return a CompletableFuture that will be completed with the value once + * transferred from the client side or null if the + * value was not available. + */ + public static CompletableFuture getItem(Storage storage, + String key) { + return getItem(UI.getCurrent(), storage, key); + } + + /** + * Asynchronously gets an item from the given storage. + *

+ * It is not possible to synchronously wait for the result of the execution + * while holding the session lock since the request handling thread that + * makes the result available will also need to lock the session.
+ * See {@link PendingJavaScriptResult#toCompletableFuture} for more + * information. + * + * @param ui + * the UI for which the storage is related to + * @param storage + * the storage + * @param key + * the key for which the value will be fetched + * @return a CompletableFuture that will be completed with the value once + * transferred from the client side or null if the + * value was not available. + */ + public static CompletableFuture getItem(UI ui, Storage storage, + String key) { + return requestItem(ui, storage, key).toCompletableFuture(String.class); + } + + private static PendingJavaScriptResult requestItem(UI ui, Storage storage, + String key) { + return ui.getPage().executeJs("return window[$0].getItem($1);", + storage.toString(), key); + } + } diff --git a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/WebStorageView.java b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/WebStorageView.java index 6a55fe0b5ba..27bda6f61bc 100644 --- a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/WebStorageView.java +++ b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/WebStorageView.java @@ -32,11 +32,12 @@ public WebStorageView() { value.setId("input"); NativeButton setData = new NativeButton(); NativeButton detect = new NativeButton(); + NativeButton detectCF = new NativeButton(); NativeButton remove = new NativeButton(); NativeButton clear = new NativeButton(); Div msg = new Div(); msg.setId("msg"); - add(value, setData, detect, remove, clear, msg); + add(value, setData, detect, detectCF, remove, clear, msg); value.setValue(LocalDateTime.now().toString()); @@ -58,6 +59,18 @@ public WebStorageView() { }); }); + detectCF.setText("Detect CompletableFuture"); + detectCF.setId("detectCF"); + detectCF.addClickListener(e -> { + WebStorage.getItem("test").thenAccept(v -> { + if (v == null) { + msg.setText(VALUE_NOT_SET); + } else { + msg.setText(v); + } + }); + }); + remove.setText("Remove 'test'"); remove.setId("remove"); remove.addClickListener(e -> { diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/WebStorageIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/WebStorageIT.java index 9cd7bf2aece..f9ba5486184 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/WebStorageIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/WebStorageIT.java @@ -25,7 +25,7 @@ public class WebStorageIT extends ChromeBrowserTest { @Test - public void testWebstorageSetAndRemove() { + public void testWebStorageSetAndRemove() { open(); WebElement input = findElement(By.id("input")); @@ -49,7 +49,7 @@ public void testWebstorageSetAndRemove() { } @Test - public void testWebstorageSetAndClear() { + public void testWebStorageSetAndClear() { open(); WebElement input = findElement(By.id("input")); @@ -72,4 +72,51 @@ public void testWebstorageSetAndClear() { WebStorageView.VALUE_NOT_SET)); } + @Test + public void testWebStorageSetAndRemove_completableFuture() { + open(); + + WebElement input = findElement(By.id("input")); + WebElement set = findElement(By.id("setText")); + WebElement detect = findElement(By.id("detectCF")); + WebElement remove = findElement(By.id("remove")); + + input.clear(); + input.sendKeys("foobar", "\n"); + + set.click(); + detect.click(); + + waitUntil(ExpectedConditions.textToBe(By.id("msg"), "foobar")); + + remove.click(); + detect.click(); + + waitUntil(ExpectedConditions.textToBe(By.id("msg"), + WebStorageView.VALUE_NOT_SET)); + } + + @Test + public void testWebStorageSetAndClear_completableFuture() { + open(); + + WebElement input = findElement(By.id("input")); + WebElement set = findElement(By.id("setText")); + WebElement detect = findElement(By.id("detectCF")); + WebElement clear = findElement(By.id("clear")); + + input.clear(); + input.sendKeys("foobar", "\n"); + + set.click(); + detect.click(); + + waitUntil(ExpectedConditions.textToBe(By.id("msg"), "foobar")); + + clear.click(); + detect.click(); + + waitUntil(ExpectedConditions.textToBe(By.id("msg"), + WebStorageView.VALUE_NOT_SET)); + } }