GWT/GXT and Session Timeouts

September 2nd, 2010 John No comments

One of the applications we develop at work is a GWT/GXT web application hosted in tomcat.  Recently we started getting complaints about strange behavior when the user’s session timed out. The RPC calls would fail so all sorts of strange error messages would popup, but the user was not redirected back to the login page.

This strange behavior was occurring because, after the initial application load, the only traffic to the server were our RPC calls. When an RPC call is made after the session timeout the call simply fails. The redirect from tomcat is ignored.

I searched the web for any solutions and came across this solution on the EXT-JS forum. The general idea was to setup a couple of timers after a user logs in. One timer will show the user a warning when the session is about to expire, the other timer will show a “Session Expired” dialog and close the browser when the user presses “OK”.

The problem with that implementation is that it doesn’t take into account user activity. So, if the session timeout is set to 30 minutes, the warning dialog would pop up at 29 minutes after the user logs in – regardless of whether the user was active for those 29 minutes. This behavior would be rather annoying to our users. So, I decided to modify this solution to make it a bit less annoying.

In my modified solution, the user does not get a “Session is about to expire” warning. Whenever the session times out a popup message alerts the user that their session has expired. When they click “OK” they are redirected back to the login page.

The basic algorithm is this:

  1. On GWT module load an RPC call gets the user session timeout in milliseconds.
  2. A timer is setup to run the RPC call again a few seconds after the timeout.
  3. If the call succeeds, the user has been active, so the timer is setup again. Otherwise, the session timeout dialog is shown.

Client side

public class Main implements EntryPoint
{
    private Timer sessionTimeoutResponseTimer;
    private MyServiceAsync service;

    /**
     * Added to the first session timeout check to allow for startup time
     */
    private final int INITIAL_TIMEOUT_PAD = 60000;
    /**
     * Added to the session timeout check timer.
     */
    private final int TIMEOUT_PAD = 15000; 

    /**
     * Entry point method.
     */
    public void onModuleLoad()
    {
        initSessionTimers();
        buildUi();
    }

    private void initSessionTimers()
    {
        service = (MyServiceAsync) GWT.create(MyService.class);
        ((ServiceDefTarget) service)
            .setServiceEntryPoint(GWT.getModuleBaseURL() + "gwt/myService");
        service.getUserSessionTimeout(new AsyncCallback<Long>()
        {
            public void onSuccess(Long timeout)
            {
                sessionTimeoutResponseTimer = new Timer()
                {
                    @Override
                    public void run()
                    {
                        checkUserSessionAlive();
                    }
                };
                sessionTimeoutResponseTimer.schedule(timeout+INITIAL_TIMEOUT_PAD);
            }

            public void onFailure(Throwable caught)
            {
                displaySessionTimedOut();
            }
        });
    }

    private void checkUserSessionAlive()
    {
        service.getUserSessionTimeout(new AsyncCallback<Long>()
        {
            public void onSuccess(Long timeout)
            {
                sessionTimeoutResponseTimer.cancel();
                sessionTimeoutResponseTimer.schedule(timeout+TIMEOUT_PAD);
            }

            public void onFailure(Throwable caught)
            {
                displaySessionTimedOut();
            }
        });

    }

    private void displaySessionTimedOut()
    {
        MessageBox.alert(
                "Session Timeout",
                "Your session has timed out.",
                new Listener<MessageBoxEvent>()
        {

            public void handleEvent(MessageBoxEvent be) {
                Window.Location.reload();
            }
        });
    }
}

 

Server side

 

public class MyServiceImpl extends RemoteServiceServlet implements MyService
{
    @Override
    public Integer getUserSessionTimeout() {
        int timeout = getThreadLocalRequest().getSession()
            .getMaxInactiveInterval() * 1000;
        return timeout;
    }
}

 

Note: The method getMaxInactiveInterval() returns the session timeout in seconds that is configured in web.xml. The session timeout in web.xml is specified in minutes.

 

public interface MyService extends RemoteService
{
    Integer getUserSessionTimeout();
}

public interface MyServiceAsync extends RemoteService
{
    void getUserSessionTimeout(AsyncCallback<Integer> callback);
}

 

There is one small problem with this approach. It may keep the session alive longer than the session timeout. To illustrate this, lets say that the session timeout is set to 30 minutes. When the user logs in, a timer is setup to make an RPC call 31 minutes after the user logged in. Let’s say the user works for 15 minutes then switches to something else. At 31 minutes after logging in, the timer will cause an RPC call to be made, which resets the session timeout countdown on the server side. 30 minutes later the timer will then make the call again. This time the session will have timed out and the “Session Timeout” alert is displayed. So, even though the session timeout was set for 30 minutes, the user was inactive for 45 minutes before the session timed out. I suppose with a little thought and another timer you could come up with a way to make the session timeout after exactly 30 minutes of inactivity. Perhaps that will be a topic for future post.

Categories: Uncategorized Tags:

Reading Objects from an LDAP Directory using Spring

July 21st, 2010 John No comments

The title of this post is “Reading Objects from an LDAP Directory using Spring”, but I’m going to present a specific example. This example can easily be modified to read any object from an LDAP Directory as a Spring bean.

So, for my example I’m going to use a problem that I had to solve recently. I needed to move some database configuration for several web apps from being defined as a tomcat Resource to being read from an LDAP directory. Why would you want to do this, I hear you ask? Well, imagine you have several Tomcat instances possibly on several different machines. Anytime you need to make any database configuration changes (or even on installation) you have to duplicate the changes on every tomcat installation. By moving some configuration into a centralized location it’s much easier to maintain and administer.

Now, this doesn’t sound like it would be that difficult of a task, but it turns out it wasn’t quite as simple as I thought. The web apps I needed to modify use Spring+Hibernate, so our Spring configuration used to look something like this.

 

<bean id="myDataSource" class="org.springframework.jndi.JndiObjectFactoryBean"
      scope="singleton">
  <property name="jndiName" value="/jdbc/MyDatabase" />
  <property name="resourceRef" value="true" />
</bean>

<bean id="sessionFactory"    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
  <property name="dataSource"  ref="myDataSource" />
  <property name="annotatedClasses">
    <list>
      <value>...</value>
    </list>
  </property>
</bean>

 

We also had a resource defined in context.xml with all of the database connection information (driver, url, username/password, etc) and a reference to that resource from the web app.

So, instead of having this resource defined in tomcat, we want to read the DataSource from the LDAP Directory. Hibernate is expecting a javax.sql.DataSource. How do we read a javax.sql.DataSource from the LDAP Directory using Spring? We can use Spring’s JndiObjectFactoryBean and JndiTemplate. The template allows us to specify an object factory for constructing objects from JNDI entries. We can use this approach to create an object factory that reads an entry from the LDAP Directory (using JNDI) and constructs a javax.sql.DataSource from it.

 

public class DataSourceObjectFactory implements DirObjectFactory
{
    @Override
    public Object getObjectInstance(Object obj, Name name, Context ctx,
            Hashtable<?, ?> env, Attributes attrs) throws Exception
    {
        if (obj instanceof DirContext)
        {
            Attribute objectClass = attrs.get("objectClass");
            NamingEnumeration<?> ne = objectClass.getAll();
            while (ne.hasMore())
            {
                Object next = ne.next();
                if (next.equals("Database"))
                {
                    String driver = attrs.get("driver");
                    String scheme = attrs.get("scheme");
                    String server = attrs.get("server");
                    String instance = attrs.get("instance");
                    String database = attrs.get("database");
                    String username = attrs.get("username");
                    String password = attrs.get("password");

                    BasicDataSource ds = new BasicDataSource();
                    ds.setDriverClassName(driver);
                    String url = scheme + "://" + server + "/" + database;
                    if (null != instance)
                    {
                        url += ";instance=" + instance;
                    }
                    if (null != username)
                    {
                        ds.setUsername(username);
                        ds.setPassword(password);
                    }
                }
            }
        }
        return null;
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context ctx,
            Hashtable<?, ?> env) throws Exception
    {
        return getObjectInstance(obj, name, ctx, env, null);
    }
}

 

This factory constructs an apache dbcp BasicDataSource (which implements javax.sql.DataSource). The LDAP context will be passed to the factory along with an instance of Attributes which contains the object that was requested. This factory first checks that it is supposed to handle this type of object by checking that the objectClass is a “Database” object (the LDAP schema I’m using was extended to include a custom “Database” object). Once we are sure we should be handling this object we go ahead and construct the DataSource.

Now – how do we use it? Our Spring config file from above is modified like so:

 

<bean id="myJndiTemplate" class="org.springframework.jndi.JndiTemplate">
  <property name="environment">
    <props>
      <prop key="java.naming.factory.initial">com.sun.jndi.ldap.LdapCtxFactory</prop>
      <prop key="java.naming.provider.url">ldap://localhost:389/dc=myapp,dc=johnhite,dc=com</prop>
      <prop key="java.naming.factory.object">com.johnhite.ldap.jndi.DataSourceObjectFactory</prop>
      <prop key="java.naming.security.authentication">none</prop>
    </props>
  </property>
</bean>

<bean id="myDataSource"
      class="org.springframework.jndi.JndiObjectFactoryBean"
      scope="singleton">
  <property name="proxyInterface" value="javax.sql.DataSource" />
  <property name="jndiTemplate" ref="myJndiTemplate" />
  <property name="jndiName" value="cn=MyDatabase, o=Databases" />
</bean>

 

As  you can see we are still using Springs JndiObjectFactoryBean, but we are using it in conjunction with Springs JndiTemplate in order to specify the LDAP directory location and our ObjectFactory. The jndiName has become the DN (Distinguished Name) of the database entry.

Also notice that we added the proxyInterface. This tells the JndiObjectFactoryBean to return a proxy that implements that interface. In our case we want a javax.sql.DataSource. If we don’t do this hibernate will throw a class cast exception because its expecting a javax.sql.DataSource.

Please Note: This example does not take security into consideration. If you want to use this you must ensure that your LDAP directory is secure.

Categories: Hibernate, JNDI, Java, Spring Tags: , , , ,

Redirecting System.out to a JTextArea

April 5th, 2010 John 1 comment

I was recently working on a small Demo UI for a Metro web service client. Creating the UI was fairly simple, but I was thinking that it would be nice to show the raw SOAP messages in the UI. Getting Metro to output the SOAP messages was easy, but the Metro was outputting them to System.out. I thought, “Surely there must be a way to redirect System.out to a JTextArea. After all, if Eclipse can do it, then so can I!” So here’s what I came up with.

public class TextAreaOutputStream extends OutputStream
{
    private static int BUFFER_SIZE = 8192;
    private JTextArea target;
    private byte[] buffer = new byte[BUFFER_SIZE];
    private int pos = 0;

    public TextAreaOutputStream(JTextArea target)
    {
        this.target = target;
    }

    @Override
    public void write(int b) throws IOException
    {
        buffer[pos++] = (byte)b;
        //Append to the TextArea when the buffer is full
        if (pos == BUFFER_SIZE) {
            flush();
        }
    }

    @Override
    public void flush() throws IOException
    {
        byte[] flush = null;
        if (pos != BUFFER_SIZE) {
            flush = new byte[pos];
            System.arraycopy(buffer, 0, flush, 0, pos);
        }
        else {
            flush = buffer;
        }

        target.append(new String(flush));
        pos = 0;
    }
}

As you can see, this is a very simple buffered OuptutStream that writes to a JTextArea when the buffer is full. Well, that handles writing to a JTextArea, but how about redirecting System.out?

JTextArea consoleText = new JTextArea();
TextAreaOutputStream textOut = new TextAreaOutputStream(consoleText);
PrintStream outStream = new PrintStream(textOut, true);
System.setOut(outStream);

Note the PrintStream constructor I used. The second argument is the “autoFlush” argument. Setting this to true means that whenever the PrintStream encounters a newline, it will flush the OutputStream (which appends to the JTextArea).

And there you have it. Now anything that calls System.out.println will now end up printing directly to your JTextArea.

Categories: Java Tags: ,