Showing posts with label google web toolkit. Show all posts
Showing posts with label google web toolkit. Show all posts

Thursday, November 8, 2012

GWT Auto Logout & Open Dialogs

Currently, I work for CounterPath, developing a large GWT application (as well as doing some GWT work for Green Motion Travel).

At CounterPath, my GWT application has an auto-logout feature.
This is to avoid the situation where an application user doesn't do anything for a while, and then edits a pile of information and clicks 'Save', only to find out their session timer has expired, meaning they just lost their work.

The auto-logout feature keeps an internal timer that matches the server's session timeout.
The internal timer is reset every time I do some server interaction.
To avoid inserting these calls all over the place in my code, I added an Async RPC facade;  My application uses the facade, and the facade makes the call to the server.
The downside is that I have to maintain another copy of the RPC interface.  Fortunately, that part is quite stable.

For example, I have:
  • MyData - the RPC interface
  • MyDataAsync - the Async version of MyData
and now,
  • MyDataAsyncSession - a class that implements MyDataAsync

MyData:

public interface MyData extends RemoteService
{
    public Data getData();
}

MyDataAsync:

public interface MyDataAsync
{
    public void getData(AsyncCallback<Data> callback);
    etc...
}

And, in my client code,

MyDataAsyncSession:

public class MyDataAsyncSession implements MyDataAsync
{
    MyDataAsync realRPC = (MyDataAsync)GWT.create(MyData.class);

    @Override
    public void getData(AsyncCallback<Data> result)
    {
        resetSessionTimer();
        realRPC.getData(result);
    }

    etc...
In my actual client code, I would then have:
MyDataAsyncSession rpc = new MyDataAsyncSession();
rpc.getData(new AsyncCallback<Data>(){...});
In this way, I don't have to worry about sprinkling calls to resetSessionTimer all over my code.
If this was all on the server, I could probably use AOP to insert calls to resetSessionTimer() in the right places...

On to the main point of this post.
In a few places, I use popup dialogs.
This leads to a problem: If you do something that generates a popup dialog, and then let it sit there for 30 minutes, the GWT application will auto-log-out, which will hide the other panels and show the login panel.  However, it won't hide the popup dialogs!  They will continue to show whatever was being worked on.

At first, I was considering a popup registration system, maintaining a list of popups that are currently open.  This could get messy, with some popups being auto-close, and others closed with a button or other control.
Fortunately, I found an easier way:  Just search the default root panel for PopupPanel widgets, and close them!

Here is the code, which is now part of my doLogout() function:

Code to Auto-Close Popup Panels

// Create a widget processing list, and
  // add the default root panel to it.
  List widgetList = new ArrayList();
  widgetList.add(RootPanel.get());
  while(!widgetList.isEmpty())
  {
      // Pull the first widget from the list
      Widget w = widgetList.remove(0);

      // If it is a popup, hide it!
      if (w instanceof PopupPanel)
      {
          ((PopupPanel)w).hide();
      }
      else if (w instanceof HasWidgets)
      {
          // Add any child widgets to the processing list.
          Iterator iter = ((HasWidgets)w).iterator();
          while(iter.hasNext())
          {
              Widget child = iter.next();
              widgetList.add(child);
          }
      }
  }

That's a lot easier than having to maintain a list of open popups!

Thursday, August 25, 2011

GWT and Username/Password autocomplete

Userbase Demanded Login-Autocomplete!

For one of my work projects, I had to pass-through login from my server code to another service.  No problem.  My initial design just used a GWT RPC post to send the username and login credentials to my server, which sent the login request to the other server.  If that succeeded, then I would assign a variable to the session and return success.

However,  the input boxes for username and password did not have the desired autocomplete feature.


After lots of messing about, and some Googling, I came up with a pretty easy strategy.

Part 1: Getting the browser to save the password

1. Convert the login to a form, and have the form submit a POST to a login servlet.  The inputs must be named 'username' and 'password', and the password input must be of type 'password' as well.

As an aside, I was disabling the inputs and the login button before submitting the post, so the user can't keep clicking it.  This caused the post to be empty!  In order for the post to actually send the username and password, I had to disable the inputs after submitting the post.


Essentially:
public class LoginForm extends VerticalPanel
{
    final TextBox username;
    final PasswordTextBox password;
    final private FormPanel formPanel = new FormPanel();
    final SubmitButton button = new SubmitButton("Log in");

    public LoginForm()
    {
        add(formPanel);
        formPanel.setAction("servletpath/login");
        formPanel.setMethod(FormPanel.METHOD_POST);

        VerticalPanel vp = new VerticalPanel();
        formPanel.setWidget(vp);

        username = new TextBox();
        username.setName("username");
        vp.add(username);

        password = new PasswordTextBox();
        password.setName("password");
        vp.add(password); 
        vp.add(button);

        formPanel.addSubmitCompleteHandler(
            new SubmitCompleteHandler(){...});
    }
}
Okay, that works for me: After clicking the Login button, Firefox asks if I want to save the password.
Also works for IE and Safari.  Chrome *sometimes* works.  I can't believe IE works better than Chrome for this.

Part 2: Getting the Presets Back into the Login Form
So, we've got the values saved.  But it doesn't work!
The browser does not apply the password database to forms that are added by JavaScript.

I read about one technique, which was to add a hidden to the root HTML page, and then grab the pre-filled values from those inputs.
However, this does not help in the case where you have more than one username/password!
In my case, this is a real concern, as some users need to log in as the super admin from time to time.

Here's my variation of the technique:
Add a hidden form to the root HTML page, and then wrap the inputs.
Works like a charm!

Here is my root HTML page:

<html>
  <head>
    ... usual GWT stuff ...
  </head>
  <body>
    <div id="title">
      etc...
    </div>

    <div style="display:none;">
      <form id="login_form" method="post" action="ccsadmin/login">
        <input id="creds_username" name="username"/>
        <input id="creds_password" name="password" type="password"/>
      </form>
    </div>
  </body>
</html>

Then, in my LoginForm class:

public LoginForm(String message)
{
    formPanel = new FormPanel();
    add(formPanel);
    formPanel.setAction("loginservlet/login");
    formPanel.setMethod(FormPanel.METHOD_POST);

    VerticalPanel vp = new VerticalPanel();
    formPanel.setWidget(vp);

    username = TextBox.wrap(DOM.getElementById("creds_username"));
    vp.add(username);

    password = PasswordTextBox.wrap(DOM.getElementById("creds_password"));
    vp.add(password);
    vp.add(button);
    formPanel.addSubmitCompleteHandler(new SubmitCompleteHandler(){...});
}

There you have it!
Clicking in the username box will show the list of saved credentials for my login page.

Note:
I just searched and found another example that just wraps the entire hidden form.  However, I like my version a bit better, as my app is internationalized, and so I am using the GWT i18n code to add labels and text programmatically.

Update 1:

Previously, I was using a normal Button to submit the form.
To try and get Chrome working, I converted it to a SubmitButton.
This does seem to work sometimes, not sure what's wrong.

Update 2:

I have zipped up a complete example of a GWT application.  You can download it here:
autologin.zip
To download it, click the link, then select File/Download.
To use it:

  • Start Eclipse
  • Then, select 'File/Import...'
  • Select 'Existing projects into Worksace' and click Next...
  • Choose 'Select archive file' and choose the downloaded file
  • Click 'Finish'
Note that you must have GEP installed, and with GWT 2.4+.