The General Problem
When the web was originally designed, sites were stateless. In addition, users would usually only use a single web-browser window. Unfortunately neither of these are now true; the sites that JSF and similar technologies are most useful for are highly stateful applications that resemble desktop applications. And users are used to having multiple browser windows or tabs open concurrently; it is simply natural for them to try to browse data with one window while editing it with another. In any application this requires some programming care to handle correctly, although for "desktop" applications this is not too difficult. For web applications handling this is trickier, but possible as long as the server can keep track of which window is performing which operations and allocate separate "state" data to each window.
When all state is embedded within a page (as form fields), or within the url (as query parameters); the window identity does not need to be tracked at all as each request includes all the necessary state. However there are a couple of significant problems with embedding state into a page, including:
- limited size of urls
- the long lifetime of urls (they can be bookmarked and later reused)
- state encoded into forms gets lost on GET requests (ie link anchors)
- network bandwidth needed to stream state from server to client
- not all state may be easily serializable (eg persistence contexts).
Unfortunately, the http protocol has not kept up with these new changes in web usage. The use of a "sessionId" embedded in either the url or a "cookie" was invented many years ago to support stateful behaviour per-browser. However this simply does not allow a server to properly provide per-window state.
There are various hacks available to handle per-window state on the server which work at least partially, but none of them are completely satisfactory.
The user actions which are particularly problematic are:
- Right-clicking a link and selecting "open in new window" or "open in new tab"
- Bookmarking a page, then later opening a new page and activating the bookmark.
- Copying the URL from the browser toolbar and pasting it into a new window
Note that this section is not discussing the issue of modal popup windows. Those occur only where the program explicitly chooses, and so which "window state" is associated with that window is under the control of the programmer and is not a major problem. It is those windows that the user may open without the knowledge of the server that this page addresses.
Orchestra and the Multiple Window Problem
Orchestra is explicitly intended to support stateful applications using server-side state. Therefore people who are interested in using Orchestra are almost certainly also interested in dealing with the general problem of multiple concurrent windows onto the stateful application.
Orchestra is also intended to provide a framework on which sophisticated "state management" for applications can be built. An example is the ability to click a link in a page header to save the current state, effectively providing a bookmark-with-state that the user can then restore at some later time. This would allow users to "interrupt" their current task to deal with an urgent call, then return to it when the interruption is dealt with. Therefore the concept of state management is core; the "ConversationContext" class holds the entire set of active conversations and a user can have multiple ConversationContext objects (although only one is used for any particular http request). Providing per-window state management is just a matter of associating a window with a particular ConversationContext.
There is also one Orchestra-specific issue related to multiple windows: the "access-scope conversation" feature fails when multiple windows are sharing the same state. Access scope monitors requests and automatically cleans up conversations that are no longer being used. However when two windows are accessing the same orchestra-enabled webapp concurrently and use the same per-window-state, then a request in one window would cause conversations to be discarded which the other window later wants to use.
Orchestra therefore provides built-in support for multiple windows. Unfortunately due to the limitations of http this is not a bullet-proof solution and correct handling of multiple windows requires some work by the programmer and some cooperation by the end users.
See the section below titled "Orchestra Multiple Window Support" for details of the current implementation provided by Orchestra.
Embedding a window-id in a hidden field
The most reliable way of tracking window identity is to embed a window-id within a hidden field in each form, and mark forms as using POST (which is the default). Browsers do not support an operation to do "submit form and show result in other window", and bookmarking does not store form data. In addition, when a POST is performed the url of the post is displayed in the browser navigation bar, but not the form field values so it is not stored by a later "bookmark" operation. Therefore a window identifier from one form cannot end up in another window (except by malicious behaviour on the part of the user, which doesn't matter here as the user can only confuse the state of their own windows, not anybody else's).
Unfortunately, the window identity will be lost as soon as the user does an operation that is not a POST. And this is a critical flaw in most cases; applications that care about window state generally want to allow navigation via links (GET) as well as form posts.
Even when state is only needed for POST sequences, that state does need to be cleaned up when the user does a GET. Therefore losing the window identity value when a GET is done makes it impossible to correctly clean up memory on the server.
Embedding a window-id in a cookie
Cookies are (name,value) pairs that a server can send to a browser, and which the browser will then echo back in each request that it sends to a server. These are not bookmarkable and are provided on all request types (GET, POST, etc). Therefore they are ideal to track window identity - except that all modern browsers treat cookies as shared across all windows in a browser. Setting it in one window affects all other windows too.
There is possibly a way to use cookies to track window identity when javascript is enabled. Below is the outline of a possible approach using cookies. However it has not been implemented at the current time because it has a number of significant flaws. Together these make this approach less appealing than the url-based approach that Orchestra currently implements. The limitations are:
- It requires javascript (though it is possible to "fall back" to other approaches).
- It requires session-scoped cookies
- It requires the server to detect whether the browser supports cookies or not.
- It requires javascript to be rendered into the head of each page.
Javascript can be used to update a cookie's value. Therefore it is possible for links to use an "onclick" handler to set a cookie to contain a window-id, then perform the requested GET operation and immediately reset the cookie. This causes the request to contain the needed window-id while preventing the id from appearing in the page URL (where it can be bookmarked or simply copy-and-pasted). In addition, the "open in new window" operation will not run the javascript so requests fetched into a new window will not send a window-id and the server can detect this.
Some example code that would be rendered into the head of each page:
window.document.cookie="windowId=-1"; function docookie(link) { window.document.cookie="windowId=3"; window.location.replace(link.href) window.document.cookie="windowId=-1"; return false; }
Javascript links then look like <a href="someurl" onclick="docookie(this)">..</a>
Embedding a window-id in the URL
The window identity can be embedded within the url as a query parameter. This means that each form's action url must contain the magic parameter, and so must the href of every link that is intended to be clicked to perform navigation in the same frame. A request that does not contain the magic query parameter is regarded as a "new window", and is allocated a new id.
Users must then be prevented from using "open in new window" on links containing the id, as that would copy the magic window-id parameter and both windows would appear to the server to be the same window. This can be done by setting the "href" parameter of these links to "javascript:0", and adding an onclick attribute that actually does the navigation by assigning the desired url to property "window.location". The FireFox browser simply does not render the "open in new window" option when the href is a javascript link; Internet Explorer does render the menu option but the newly opened window is always blank.
Users will want to open new windows, however. Therefore the webapp developer can arrange for some of the links to explicitly use javascript to open a new window and assign it a URL that does not have the magic window-id parameter, therefore causing the request from that window to be assigned a new id.
Unfortunately this approach does have some limitations:
- The window-id value appears in the navigation bar, which is slightly ugly.
- The window-id is saved when a bookmark is made; if two windows are opened and the same bookmark activated in each then the two windows then have the same window-id. A simple copy-and-paste of the url also duplicates the window-id parameter.
- Javascript is required; when javascript is not enabled then the whole application becomes unusable as the links are unusable (do nothing).
Because of the above limitations this approach does allow the user to have multiple windows on the same app, each with independent state, but does require them to avoid the problematic behvaiours.
Post-render detection of Window Properties
When a new window is opened, a new javascript Window object is created for it. Javascript can therefore check for a property on the window and if it is not set then it can assume this is a new window.
This approach is reasonably simple to implement. Its major limitations are:
- Javascript is required
- Testing can only be done after a page has been rendered. When the test fails (ie this is a new window) then the javascript can discard the current page and request a new one from the server, this time telling the server to allocate a new window-id. However if rendering of the page causes side-effects on the server then these cannot be undone. In most cases this is not an issue as new pages are always populated using GET requests and these should not have side-effects.
The server of course also needs to be told about the id, so this would need to be combined with something like the "Embedding a window-id in the URL" approach. However it does work around some of the limitations of that approach by detecting when a "forbidden" user operation has occurred.
This approach was (AFAIK) invented by the Apache Wicket project.
The fact that new-window-detection occurs after rendering makes this unsuitable for handling multiple windows with respect to Orchestra access-scope. Access-scoped beans within the per-window-state (conversation context) associated with the current window are purged following rendering of a new view; unforunately if it is later discovered on the browser that this request was rendered into a new window (ie should have run in a new conversation context) then it is too late to "unpurge" the conversation context.
Orchestra Multiple Window Support
Orchestra currently implements the "Embedding a window-id in the URL" approach. It overrides method ServletResponse.encodeURL to automatically insert a query-parameter named "conversationContext" into every url rendered in the page. This can be disabled for specific links using the JSF ox:separateConversationContext tag around the link; the link url will not have the magic parameter in it and therefore the request will cause a new context to be allocated on the server.
Other Web Frameworks
In the documentation for various webapp frameworks and conversation-management libraries a claim to handle multiple windows correctly is often found. However these appear to all be false. Spring WebFlow, JBoss Seam and Apache Wicket all fail to handle multiple windows correctly despite statements to the contrary in their documentation. This is not a flaw in their code; solving this correctly appears to be impossible without a change to the http protocol. However it is a flaw in the documentation. Claims by any other webapp framework to handle this issue 100% correctly should be carefully analysed. If one is found that does appear to implement a perfect solution for this approach, please contact the Orchestra mailing list!