Programming Thoughts
Struts 2 - Server State per Browser Tab
Example

The same page in different browser tabs that don't interfere with each other, improved

Users expect pages in different tabs to not interfere with each other's session data, even if they're the same page but for different records. The initial design describes a workable but flawed solution. The subsequent articles, starting at Injected Private Session fixed these flaws and this article shows a working example with notes how it differs from typical design.

Live example

A demonstration application can be accessed here. The start page looks like the following.

Figure 1: Starting page

Download

As the revised code was reworked on my own time, I can freely provide it. Source code of a demonstration application can be downloaded here as a zip file. It requires the following.

  • JDK 8 or better
  • Maven 3
  • Jakarta EE server, such as Tomcat 9

It compiles into a WAR file and is run from the /example1 URL page. The start page looks like the following.

The application demonstrates finding, navigating and editing a simple, in-memory table of countries.

Struts 2 Actions

The application originally used ServletRequestAware to get the session from the servlet request with session = getServletRequest().getSession(). Instead, Actions implement BrowserTabAware2 and session = getBrowserTabSession().

If it used SessionAware, the session would be retrieved with session = getBrowserTabSession().getAttributeMap().

Helper classes

The application makes use of a bespoke helper class for holding the search results and the index of the current selected record and page. Both the search result and record detail viewer Actions use it and, if a URL parameter is set, can change the currently selected record or page. Thus, if a user right-clicks a search result entry to display record details in a new tab, the currently selected record is changed before the new tab is detected and the helper class copied. This violates requirements by changing tab-specific state but this does not matter as the effect is invisible on the search result page. The search result Action uses the current selection index to determine the current page and the new index can only be from that page.

Unlike the standard collections, bespoke classes don't have copy constructors and aren't cloneable by default. This imposes extra code on the search results helper class, which is the following.

public SearchResults(SearchResults<T> oldValue) { this.list = new ArrayList<T>(oldValue.getList()); this.pageSize = oldValue.getPageSize(); this.selectedIndex = oldValue.getSelectedIndex(); }