Thursday, May 24, 2012

Vaadin applications and tabbed browsing


For Magnolia CMS 5 we're developing a new user interface using Vaadin and plenty of custom GWT (Google Web Toolkit) on the client side. One of the challenges that we've had to solve is support for tabbed browsing. Vaadin's core concept is that there's state on the server that represents the state of the UI and there's also state on the client that represents what's being displayed.

Given a running Vaadin application in one browser tab, what happens if the user copies the application's URL and opens a new tab with the same URL?

The new tab will use the same server side state and go from there causing the first tab to go out of sync. The problem is that there's now two clients using the same server side state. And here lies the problem:

How can we make sure each tab has its own state on the server when the server has no way of finding out which tab is sending it a request?

There's a fix suggested by the Vaadin folks that uses multiple windows within a Vaadin application where each tab is supposed to get its own window. In this context a window is a server side component representing a whole browser page. However, this has a number of flaws. The first obvious problem is that a Vaadin application is synchronized and processes requests one by one, so if there's a lengthy operation in one window then all windows are stalled. The second problem is how it uses the URL. When you open the application in a first tab the URL is the same as without the fix. It uses the default window. When you copy and paste its URL to a new tab Vaadin will automatically create a new window on the server and add its generated window name to the URL. So far so good, two tabs in the browser each using a different window component on the server side. But when the user copies the URL from the second tab, the one that includes a window name, there's nothing done to create a new window. As a consequence the newly created third starts using the same window causing the second tab to go out of sync.

Another possible solution could use the HTTP referer header, then the server could look at that and see which application the client is using. An id for the application would then be part of the URL. But the referer header is optional and there are browser plugins and proxy servers that removes it for privacy reasons so we don't want to depend on it being there.

The solution I came up with takes advantage of the fact that there's a property on the javascript window object called name that survives a page reload. As far as I now this is the only state that is kept when you navigate in a browser or reloads the page. We can use this to keep track of which application we're using. That is, the id of the server side state this browser tab is connected to. But the server still has no idea when it's serving a request.

Vaadin is a single page web application where the first request sends a bootstrap page that loads javascript which then drives the application by issuing ajax requests to the server. The same thing happens on reload or opening a bookmark. By adding a javascript snippet to this page that checks the window.name property we can direct the ajax calls towards a specific application on the server. In the bootstrap page we embed an application id suggested by the server. The client then decides if it wants to use it or if it wants to use an id it has placed in window.name.

Here's a simplified example of how it works:

public class MultipleBrowserWindowsApplicationServlet extends ApplicationServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (requestIsNotAnAjaxCall(request)) {

            // Generate a new application id that we'll suggest the client can use
            String applicationId = generateNewApplicationId();

            // Serve the bootstrap page with the suggested application id
            writeBootstrapPage(request, response, applicationId);
        } else {
            super.service(request, response);
        }
    }
}

The bootstrap page includes this script that does the trick:

<script type="text/javascript">
//<![CDATA[
  if (!window.name) {
    window.name = <application id suggested by the server>;
  }
  vaadin.vaadinConfigurations["ctxpathmagnoliavelvet-268765284"]["appUri"] += "/" + window.name;
//]]>
</script>

And voila, the client keeps track of which state on the server it's connected to and directs its ajax calls to it. This way it keeps using the same application on the server after a page reload or opening a bookmark and the user is free to copy and paste the URL in a new tab.

To finalize the solution there were a few things I had to solve.

The most problematic was that Vaadin creates and starts an application on the server before it sends the bootstrap page. This is problematic because it creates applications that are never actually used. It took some experimentation to get this solved properly. Fortunately Vaadin already supports creating and starting the application on the first ajax call if it hasn't already been done, so that part just worked.

Another problem was the 'restartApplication' parameter that is used to force the creation of a new application on the server. If the client always prefers the id it has in window.name that makes this parameter useless. To solve it I extended the bootstrap page a bit so it can force the client to use the suggested application id when necessary.

In summary, having state both on the server and on the client is a challenge when it comes to tabbed browsing. This solution works because it's a single page web application that's entirely driven by ajax after that first request.

The source code is available, posted on pastie.org for prettier formatting =) To use it change the servlet class in web.xml. Because the theme and the caption (the title of the page) is set in the bootstrap page and the application is started after the bootstrap page has been served those are set using init parameters to the servlet. Use a snippet like this:

  <servlet>
    <servlet-name>MyApplication</servlet-name>
    <servlet-class>info.magnolia.ui.vaadin.integration.servlet.MultipleBrowserWindowsApplicationServlet</servlet-class>
    <init-param>
      <param-name>theme</param-name>
      <param-value>myTheme</param-value>
    </init-param>
    <init-param>
      <param-name>caption</param-name>
      <param-value>My Application</param-value>
    </init-param>
  </servlet>

10 comments:

  1. Nice post, Tobias!

    There's a minor formatting glitch in the first para:

    s/being\ndisplayed./being displayed./ # :)

    ReplyDelete
  2. One is supposed to do longer operations outside UI thread.

    Name is good place to maintain window id.

    ReplyDelete
  3. Good work! But the Upload component seems not to deal with it, it won't let you upload the file and you actually can see the short trouble regarding the window id. Is there any possibility that it would work with the Upload component as well? Thanks!

    ReplyDelete
    Replies
    1. Ran into this issue too, it was unable to recognize the upload request. It's been fixed now, the source [1] is updated with the fix.

      [1] http://pastie.org/3962656

      Delete
  4. Great work!

    Isn't the AbstractApplicationServlet.getRequestPathInfo(...) a package private? How are you overriding that in your class?

    ReplyDelete
  5. Tobias, I am having trouble using the blossom tabbuilder's addinclude functionality. I was wondering if you might be able to help me troubleshoot it? I can't seem to get the jsp to be found.

    ReplyDelete
  6. muchas gracias me sirvio mucho este post ...

    ReplyDelete
  7. Awesome explanation. Many Thanks to you. I also have a problem with Vaadin 7 bookmarkable UI urls. The approach suggested in vaadin 6 is rather convoluted and vaadin 7 documentation is not yet updated.

    I stumbled when I was searching for my issue and found this post immensely useful in a different aspect.

    ReplyDelete
  8. Hi Tobias,

    I wish I could try your solution in my Vaadin 8 application but your source code is no longer available.

    Could you please publish it to GitHub/Gist or anywhere else?

    Thank you,
    Christophe

    ReplyDelete