Programming Thoughts
Struts 2 - Server State per Browser Tab
Third-party Links and Bookmarks

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 first attempt describes a workable but flawed solution. This article attempts to fix these flaws. See also Injected Per-tab Session, Copying Per-tab Session to New Tab and Example.

Opening a JSP page

New tabs should open a JSP page before calling any Action that uses a tab id so JavaScript code can detect lack of tab id and set it. A welcome or login JSP page can usually accomplish this. Alas, users bookmark pages, directly invoking an Action, bypassing the welcome page, and authentication cookies may be remembered between sessions. In such a case, the Action uses a blank tab id, then the JSP page detects no tab id and reloads the Action. This is still functional but is inefficient.

Setting tab id in server side

The obvious solution is the browser tab interceptor must set the tab id cookie, which will be copied by the receiving JSP page. As Actions can redirect to another Action, the tab id must be stored in the session.

private static final String ATTR_NAME_TAB_ID_COOKIE = BrowserTabInterceptor2.class.getName() + "_TabIdCookie"; protected String getTabId(ActionInvocation invocation) { Cookie newCookie; Cookie[] cookies; HttpServletRequest request; HttpServletResponse response; String result; request = invocation.getInvocationContext().getServletRequest(); cookies = request.getCookies(); result = ServletLibrary.getBrowserTabId(cookies); result = (result != null)?result:""; if (result.length() == 0) { newCookie = (Cookie)request.getSession().getAttribute(ATTR_NAME_TAB_ID_COOKIE); if (newCookie == null) { newCookie = ServletLibrary.makeSetTabIdCookie(); request.getSession().setAttribute(ATTR_NAME_TAB_ID_COOKIE, newCookie); } response = invocation.getInvocationContext().getServletResponse(); response.addCookie(newCookie); result = newCookie.getValue(); } return result; } @Override public String intercept(ActionInvocation invocation) throws Exception { BrowserTabAware2 browserTabAware; BrowserTabSession browserTabSession, oldBrowserTabSession; HttpServletRequest request; Cookie[] cookies; String result, cookieValue, oldCookieValue; if (invocation.getAction() instanceof BrowserTabAware2) { request = invocation.getInvocationContext().getServletRequest(); cookies = request.getCookies(); cookieValue = getTabId(invocation); oldCookieValue = ServletLibrary.getBrowserOldTabId(cookies); oldCookieValue = (oldCookieValue != null)?oldCookieValue:""; browserTabSession = new BrowserTabSessionImpl(invocation.getInvocationContext().getServletRequest().getSession(), cookieValue); browserTabAware = (BrowserTabAware2)invocation.getAction(); if (oldCookieValue.length() > 0 && !oldCookieValue.equals(cookieValue)) { oldBrowserTabSession = new BrowserTabSessionImpl(invocation.getInvocationContext().getServletRequest().getSession(), oldCookieValue); attributesFromOldTabToNewTab(browserTabAware, invocation, oldBrowserTabSession, browserTabSession); } browserTabAware.setBrowserTabSession(browserTabSession); } result = invocation.invoke(); return result; }

JavaScript

Client side code should use the new cookie if it's set.

function tabStartup() { var cookieValue; deleteOldTabIdCookie(); cookieValue = getCookie("_SetTabId"); if (cookieValue != "") { window.top.name = cookieValue; deleteSetTabIdCookie(); } if (window.top.name == "") { window.top.name = "" + (new Date()).getTime(); cookieValue = getCookie("_TabId"); setTabIdCookie(); if (cookieValue != "" && window.opener != null && !urlHasNoServerState(location.toString())) { setOldTabIdCookie(cookieValue); location.reload(); } } window.addEventListener("focus", setTabIdCookie); document.body.addEventListener("focus", setTabIdCookie); document.body.addEventListener("mouseover", setTabIdCookie); } function deleteSetTabIdCookie() { document.cookie = "_SetTabId=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; }

Conclusion

This solves the crashing of pages opened in a new tab problem, as well as other flaws, but requirements and limitations remain and new ones introduced.

  • Actions must implement a new interface but this can be implemented in a Template class like ActionSupport.
  • Actions must distinguish between shared and tab-specific session data. However, actions will make session data tab-specific by default and shared session data, such as authentication and authorisation, are typically injected by interceptors.
  • Tab-specific session data must be copyable, whether by copy constructor or implementing Cloneable, be immutable, or the Action must manually copy attributes. Only shallow copying is typically needed.
  • Hyperlinks should not normally change tab-specific, server side state, otherwise the state for the old tab is changed before server code realises it's working on a new tab. GET requests aren't expected to change persistent data but might change conversation state, such as currently selected record in a list. This is rarely a problem in practice.

Example

A working example can be downloaded from Example.