I need to send notifications from service classes that automatically go to the correct user’s UI without explicitly passing user IDs. The specific use case is sending progress notifications during long-running tasks:
Current issue: When a service method broadcasts a notification, it goes to all users. I want it to automatically go to just the user who initiated the service call, maintaining the user context from controller to service layer.
Example flow:
- User A logs in
- User B logs in
- User A starts a research task
- Service broadcasts progress notifications that should automatically route to User A’s UI
- User B should not see these notifications, without me having to explicitly pass User A’s ID
Yes, I could pass user IDs with every broadcast call, but I’m looking for a cleaner architectural solution that maintains the user context automatically. Here’s my current code…
public interface Broadcaster {
void sendNotification(String message);
Registration registerListener(SerializableBiConsumer<String, UI> listener);
}
public class SessionBroadcaster implements Broadcaster {
private final Executor executor = Executors.newSingleThreadExecutor();
private final List<SerializableBiConsumer<String, UI>> uiListeners = new CopyOnWriteArrayList<>();
public SessionBroadcaster() {
}
@Override
public void sendNotification(String message) {
broadcastMessage(message);
}
@Override
public Registration registerListener(SerializableBiConsumer<String, UI> listener) {
uiListeners.add(listener);
return () -> uiListeners.remove(listener);
}
private void broadcastMessage(String message) {
UI currentUI = UI.getCurrent();
for (SerializableBiConsumer<String, UI> listener : uiListeners) {
executor.execute(() -> listener.accept(message, currentUI));
}
}
}
Then I use it in the onAttach method of the MainLayout:
@Override
protected void onAttach(AttachEvent attachEvent) {
UI ui = attachEvent.getUI();
broadcasterRegistration = broadcaster.registerListener((message, _) -> ui.access(() -> { // Use the UI from attachEvent
if (!"REFRESH_GRIDS".equals(message)) {
Notification notification = Notification.show(message);
notification.setPosition(Notification.Position.TOP_CENTER);
}
}));
}
and here’s the entire MainLayout:
@JsModule("./prefers-color-scheme.js")
@Layout
@AnonymousAllowed
public class MainLayout extends AppLayout {
private H1 viewTitle;
private final AuthenticatedUser authenticatedUser;
private Registration broadcasterRegistration;
private final Broadcaster broadcaster;
public MainLayout(AuthenticatedUser authenticatedUser, Broadcaster broadcaster) {
this.authenticatedUser = authenticatedUser;
this.broadcaster = broadcaster;
setPrimarySection(Section.DRAWER);
addDrawerContent();
addHeaderContent();
}
private void addHeaderContent() {
DrawerToggle toggle = new DrawerToggle();
toggle.setAriaLabel("Menu toggle");
viewTitle = new H1();
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
Optional<Account> currentAccount = authenticatedUser.get();
String credit = "?";
if (currentAccount.isPresent()) {
credit = String.valueOf(currentAccount.get().appCredit().balance());
}
Paragraph creditBalanceParagraph = new Paragraph("Credit balance: " + credit);
creditBalanceParagraph.setId("credit-balance");
creditBalanceParagraph.addClassNames(LumoUtility.FontSize.SMALL, LumoUtility.Margin.NONE);
HorizontalLayout headerLayout = new HorizontalLayout(viewTitle, creditBalanceParagraph);
headerLayout.setWidthFull();
headerLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
headerLayout.addClassNames(LumoUtility.Padding.Right.LARGE);
addToNavbar(true, toggle, headerLayout);
}
private void addDrawerContent() {
Span appName = new Span("B2B Demand Generation Strategy");
appName.addClassNames(LumoUtility.FontWeight.SEMIBOLD, LumoUtility.FontSize.LARGE);
Header header = new Header(appName);
header.addClickListener(_ -> header.getUI().ifPresent(ui -> ui.navigate(KeywordCollectionsView.class)));
Scroller scroller = new Scroller(createNavigation());
addToDrawer(header, scroller, createFooter());
}
private SideNav createNavigation() {
SideNav nav = new SideNav();
List<MenuEntry> menuEntries = MenuConfiguration.getMenuEntries();
menuEntries.forEach(entry -> {
if (entry.icon() != null) {
nav.addItem(new SideNavItem(entry.title(), entry.path(), new SvgIcon(entry.icon())));
} else {
nav.addItem(new SideNavItem(entry.title(), entry.path()));
}
});
return nav;
}
private Footer createFooter() {
Footer layout = new Footer();
Optional<Account> maybeUser = authenticatedUser.get();
if (maybeUser.isPresent()) {
Account account = maybeUser.get();
String name;
if (account.person() != null && account.person().name() != null) {
name = account.person().name();
} else {
name = account.username();
}
Avatar avatar = new Avatar(name);
avatar.setThemeName("xsmall");
avatar.getElement().setAttribute("tabindex", "-1");
MenuBar userMenu = new MenuBar();
userMenu.setThemeName("tertiary-inline contrast");
MenuItem userName = userMenu.addItem("");
Div div = new Div();
div.add(avatar);
div.add(name);
div.add(new Icon("lumo", "dropdown"));
div.getElement().getStyle().set("display", "flex");
div.getElement().getStyle().set("align-items", "center");
div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
userName.add(div);
SubMenu subMenu = userName.getSubMenu();
subMenu.addItem("Sign out", _ -> authenticatedUser.logout());
layout.add(userMenu);
} else {
Anchor loginLink = new Anchor("login", "Sign in");
layout.add(loginLink);
}
return layout;
}
@Override
protected void afterNavigation() {
super.afterNavigation();
viewTitle.setText(getCurrentPageTitle());
}
private String getCurrentPageTitle() {
return MenuConfiguration.getPageHeader(getContent()).orElse("");
}
@Override
protected void onAttach(AttachEvent attachEvent) {
UI ui = attachEvent.getUI();
broadcasterRegistration = broadcaster.registerListener((message, _) -> ui.access(() -> { // Use the UI from attachEvent
if (!"REFRESH_GRIDS".equals(message)) {
Notification notification = Notification.show(message);
notification.setPosition(Notification.Position.TOP_CENTER);
}
}));
}
@Override
protected void onDetach(DetachEvent detachEvent) {
if (broadcasterRegistration != null) {
broadcasterRegistration.remove();
broadcasterRegistration = null;
}
}
}