diff --git a/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoController.java b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoController.java new file mode 100644 index 00000000..ee0c10d9 --- /dev/null +++ b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoController.java @@ -0,0 +1,45 @@ +package mybookstore.handoff; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import tools.dynamia.navigation.NavigationManager; + +import java.io.IOException; + +/** + * Real-world navigation hand-off cases for manual/automated verification of + * {@code NavigationManagerSession} and its request-scope filter: + * + */ +@Controller +public class HandOffDemoController { + + @GetMapping("/demo/handoff/redirect") + public void redirect(HttpServletResponse response) throws IOException { + NavigationManager.setPageLater("library/invoices"); + NavigationManager.runLater(HandOffDemoState.CALLBACKS_RUN::incrementAndGet); + response.sendRedirect("/demo/handoff/desktop"); + } + + @GetMapping("/demo/handoff/desktop") + public ModelAndView desktop() { + return new ModelAndView("embed"); + } + + @GetMapping(value = "/demo/handoff/status", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public String status() { + return "{\"callbacksRun\":" + HandOffDemoState.CALLBACKS_RUN.get() + "}"; + } +} diff --git a/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoLoginFilter.java b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoLoginFilter.java new file mode 100644 index 00000000..4d9e6827 --- /dev/null +++ b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoLoginFilter.java @@ -0,0 +1,39 @@ +package mybookstore.handoff; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import tools.dynamia.navigation.NavigationManager; + +import java.io.IOException; + +/** + * Simulates what a real login does in an app that uses Spring Security: a listener reacting to the + * authentication success ({@code LoginListener.onLoginSuccess}) calls {@code setPageLater}/{@code runLater} + * inside the security filter chain (order -100, before any controller) and the request ends with an + * HTTP redirect. Both things must work: the navigation scope must already be bound at that point, and the + * pending intent must survive the redirect. + *

+ * Try: {@code GET /demo/handoff/login} (redirects to {@code /demo/handoff/desktop}, which must open Customers). + */ +@Component +@Order(-100) +public class HandOffDemoLoginFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !"/demo/handoff/login".equals(request.getRequestURI()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + NavigationManager.setPageLater("library/customers"); + NavigationManager.runLater(HandOffDemoState.CALLBACKS_RUN::incrementAndGet); + response.sendRedirect("/demo/handoff/desktop"); + } +} diff --git a/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoState.java b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoState.java new file mode 100644 index 00000000..daffc42a --- /dev/null +++ b/examples/demo-zk-books/src/main/java/mybookstore/handoff/HandOffDemoState.java @@ -0,0 +1,15 @@ +package mybookstore.handoff; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Counts how many {@code NavigationManager.runLater} callbacks queued by the hand-off demo endpoints + * actually ran, so the flow can be verified from the outside (see {@link HandOffDemoController}). + */ +final class HandOffDemoState { + + static final AtomicInteger CALLBACKS_RUN = new AtomicInteger(); + + private HandOffDemoState() { + } +} diff --git a/platform/core/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java b/platform/core/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java index 3817b891..8c5632d4 100644 --- a/platform/core/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java +++ b/platform/core/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** *

@@ -81,6 +82,7 @@ public abstract class BaseNavigationManager implements Serializable, NavigationM private NavigationBuilder currentNavigationBuilder; private Map currentPageParams; + private final String id; /** * Creates a new BaseNavigationManager with the given {@link ModuleContainer}. @@ -91,6 +93,22 @@ public BaseNavigationManager(ModuleContainer container) { this.logger = new SLF4JLoggingService(BaseNavigationManager.class); this.attributes = new HashMap<>(); this.container = container; + this.id = UUID.randomUUID().toString(); + var registry = NavigationManagerRegistry.getInstance(); + if (registry != null) { + registry.register(this); + } + } + + /** + * Returns a stable, framework-agnostic identifier for this instance, generated once at + * construction time. + * + * @return an opaque, stable id for this instance + */ + @Override + public String getId() { + return id; } /** diff --git a/platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java b/platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java index cd8b8030..abe7b936 100644 --- a/platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java +++ b/platform/core/navigation/src/main/java/tools/dynamia/navigation/NavigationManager.java @@ -105,7 +105,15 @@ static NavigationManager getCurrent() { } /** - * Delegate set current {@link Page} using a {@link NavigationManagerSession} when NavigationManager is builded + * Delegate set current {@link Page} using a {@link NavigationManagerSession} when NavigationManager is builded. + *

+ * {@link NavigationManagerSession} is a {@link ScopedValue}: this must be called within the same + * scope that will forward into the target ZK desktop (e.g. from a controller rendering an + * {@code index}/{@code embed} ZUL view via a server-side forward, with the scope established by + * a request-lifecycle filter around the whole request). If the request ends (e.g. with an HTTP + * redirect) before any desktop consumed the value, the filter hands it off through the HTTP + * session to the next request. Throws {@link java.util.NoSuchElementException} if no scope is + * currently bound. * * @param page */ @@ -114,7 +122,8 @@ static void setPageLater(Page page) { } /** - * Delegate set current {@link Page} using a {@link NavigationManagerSession} when NavigationManager is builded + * Delegate set current {@link Page} using a {@link NavigationManagerSession} when NavigationManager is builded. + * See {@link #setPageLater(Page)} for the scope requirement. * * @param page * @param params @@ -132,7 +141,9 @@ static void setPageLater(String path, Map params) { } /** - * Delegate callback to run when {@link NavigationManager} are builded. Its store a Queue using {@link NavigationManagerSession} + * Delegate callback to run when {@link NavigationManager} are builded. Its store a Queue using {@link NavigationManagerSession}. + * See {@link #setPageLater(Page)} for the scope requirement — this must be called within the + * same scope that will build the target {@link NavigationManager}. * * @param callback */ @@ -331,4 +342,19 @@ static void runLater(Callback callback) { * @param navigationBuilder the navigation builder to set */ void setCurrentNavigationBuilder(NavigationBuilder navigationBuilder); + + /** + * Returns a stable identifier for this {@link NavigationManager} instance, unique within the + * current user session, framework-agnostic (it never references a UI-specific concept such as a + * ZK {@code Desktop} id). + *

+ * Used to correlate a manager instance with "which tab/iframe/desktop" it belongs to when more + * than one instance is active in the same session at once (e.g. several {@code