Asynchronous on-demand login in GWT
A web-based client may read data from the server anonymously. But in order to modify data, the user must be logged in. Therefore the login dialog should be delayed, until there is demand for credentials. Furthermore, the session may expire while the user is inactive. A new session should be created as need arises. When a submission fails because the authentication information is unknown, the data must not be lost.
On a first glance those requirements appear to be fulfilled easily. But on a closer look, a rather complex process is involved:
The process starts with a Command, that is triggered by the user. The server may accept the action and return a result or throw any exception. In this case the process is Done.
Or the server may throw an AuthenticationRequiredException. In this case the client has to ask the user credentials by displaying a username and password dialog in the action ShowLoginForm.
The user might Cancel the login dialog and end the process. Or the user may provide a username and password. In this case, the client attempts a login in TryLogin.
The server may answer with an Failed response. This leads back to the ShowLoginForm actions. Or the server may accept the login. In this case, the client has to invoke the initial Command again.
Sounds like a piece of cake? Wait, there is an important catch:
All those actions happen asynchronously. This means, that the flow of control returns immediately after an action is executed. But at a later time, an event handler is called with the results.
If you have a look again at the process, you will notice, that the initial action is required in the very last event handler.
A simple approach
Lets start with a simple approach first: The client tries to save some data. If that failes with an AuthRequiredException it will display a dialog box asking the user to login.
This diagram is divided into three packages: The client and shared packages contain our code while the AsyncCallback is an interface provided by GWT.
The client invokes the save()-action on the server by using the GWT remote procedure call. It provides a SaveCallback as AsyncCallback handler to deal with the result. If the SaveCallback encounters an AuthRequiredException, it will ask the user to login by display a dialog box.
Approach with an CommandExecutor
Starting from this simple approach, we need to do a few modifications in order to support asynchronous login on demand:
- The authentication process should be handled by a central mechanism for all service calls.
- Service calls need to be repeatable.
We achieve these requirements by introducing a new class called CommandExecutor and an interface called Command.
The CommandExecutor will deal with the complex process internally. The developer of a domain functions, such as save(), can use it as blackbox without having to worry about all the messy details of the login process.
In order to enable toe CommandExecutor to repeat a service call, we encapsulate the service call in an implementation of the Command interface. Note that the AsynCallback instance used for the actual service call is not the initial callback handler, but an object provided as parameter to the execute() method.
Internals of the CommandExecutor
How does the CommandExecutor work internally?
The CommandExecutor is created with a Command, that invokes a remote service call, and an AsyncCallback handler.
The following diagram shows, that it creates a set of internal handlers:
When the CommandExecutor is asked to execute a command, it delegates the service invocation, to the implementation of the Command interface. But instead of the original AsyncCallback implementation, it passes its own AuthAwareCallback.
So when the service call returns, AuthAwareCallback sees the result. In case the result is not an AuthRequiredException, it will simply delegate it to the initial AsyncCallback implementation. But if authentication is required, it will display the login form.
The result of the login form is handled by LoginFormHandler. If the user provided username and password, it will attempt to login using a service with an AuthenticationResultHandler as AsyncCallback.
If AuthenticationResultHandler is notified about a failed login attempt, it will display the login form again. In case the login was successful, it will execute the initial command again.
The loop exists if either the service call does not throw an AuthRequiredException or the user cancels the login form.