Providing Tooltip Help Messages Dynamically Using Ajax Functionality

This is actually part of context sensitive help requirements in our web application project. We should be able to provide small informative messages for html components displaying them as tooltips in our web pages when user moves mouse over those components.
It is also in the requirements that continiously appearing tooltip message windows in those pages along the mouse movement shouldn’t be disturptive against user. Hence, we let the user determine the time of tooltip message appearence as pressing CTRL at the same time  moving mouse over components will enable tooltip message appearence.
We should further be able to change content of tooltip messages and add new tooltip messages for other components without any source code modification.
Our solution has two parts, one on the client side, employing javascript to fecth any available tooltip messages from serverside using Ajax functionality for currently focused html components (via CTRL + mouse over at this time) and displaying them with opening a DIV element, as tooltip message container, and positioning it near the bottom right corner of the related component.
On the server side there is a HelpServlet employed as Ajax service end point, returning tooltip messages according to given html component ids. Tooltip message solution is closely related with our other part of the context sensitive help mechanism as they both employ page help context ids together with component ids. The reason for that, we need unique tooltip message identifiers accross the whole web application in order to identify each component’s tooltip message. As a result we glue page help context id together with html component id and use it as key for component’s tooltip message, and ask HelpServlet with that key if there is any available tooltip message for the currently focused component.
Tooltip messages on the server side are kept in a message properties file and loaded at the startup time by HelpServlet.
Below is the client side javascript code:

var appContextPath  = '/webAppContext;
var helpServletPath = '/helpServlet';
var http;

function getPageHelpContextID() {
    var pageCtxObj = document.getElementById('form1:pageCtxId');
    if(pageCtxObj) {
        return pageCtxObj.value;
    } else {
        return document.forms[0].id;
    }
}

function showToolTip(evt) {
    evt = (evt)?evt:event;
    
    if(evt.ctrlKey) {
        var element = (evt.target)?evt.target:evt.srcElement;
        
        var toolTipElement = document.getElementById('toolTip');
        if(!toolTipElement) {
        
            toolTipElement = document.createElement('div');
            toolTipElement.id = 'toolTip';
            toolTipElement.style.position='absolute';
            toolTipElement.style.zIndex=1;
            toolTipElement.style.backgroundColor= '#FFFF99';
            toolTipElement.style.width=200;
            toolTipElement.style.height=30;
            toolTipElement.style.borderStyle='solid';
            toolTipElement.style.borderColor='#CCCCCC';
            toolTipElement.style.borderWidth='1px';
            
            document.body.appendChild(toolTipElement);
        }
            
        toolTipElement.style.left=findPosX(element) + element.offsetWidth + 10;
        toolTipElement.style.top=findPosY(element) + element.offsetHeight + 10;
        
        fetchAndDisplayToolTipMsg(element.id);
    }    
}

function hideToolTip(evt) {
    evt = (evt)?evt:event;
    var element = (evt.target)?evt.target:evt.srcElement;
    var toolTipElement = document.getElementById('toolTip');
    if(toolTipElement)
        toolTipElement.style.visibility='hidden';
}

function findPosX(obj) {
    var curleft = 0;
    if (obj.offsetParent) {
        while (obj.offsetParent) {
            curleft += obj.offsetLeft
            obj = obj.offsetParent;
        }
    }
    else if (obj.x)
        curleft += obj.x;
    return curleft;
}

function findPosY(obj) {
    var curtop = 0;
    if (obj.offsetParent) {
        while (obj.offsetParent) {
            curtop += obj.offsetTop
            obj = obj.offsetParent;
        }
    }
    else if (obj.y)
        curtop += obj.y;
    return curtop;
}

function fetchAndDisplayToolTipMsg(msgId) {
    if(msgId) {
        http = createHttpRequestObject();
        sendHttpRequest(getPageHelpContextID() + ':' + msgId);
    }
}

function createHttpRequestObject() {
    var ro;
       if(window.ActiveXObject) {
        ro = new ActiveXObject("Microsoft.XMLHTTP");
    }else{
        ro = new XMLHttpRequest();
    }
    return ro;
}

function sendHttpRequest(toolTipMsgId) {
    http.open('get', appContextPath + helpServletPath +'?toolTipMsgId=' + escape(toolTipMsgId),true);
    http.onreadystatechange = handleHttpResponse;
    http.send(null);
}

function handleHttpResponse() {
    if(http.readyState == 4) {
        var toolTipElement = document.getElementById('toolTip');
        
        var msg = http.responseText;
        
        if(msg && (msg.length > 0)) {
            toolTipElement.innerText = msg;
            toolTipElement.style.visibility='visible';
        }
        
    }
}

In order to activate above functionality we must add two event handlers in the BODY html element as follows:

<BODY onmouseover="showToolTip(event);" onmouseout="hideToolTip(event);" ...>
...
</BODY>

As you see above, main entry points are showToolTip() and hideToolTip() functions which are called on mouse over and mouse out events accordingly. In showToolTip(), we first create DIV element to display tooltip messages in it if there hasn’t been created before, then we fecth tooltip message for the current component from server side via XmlHttpRequest, and finally display it by placing any available tooltip message into the DIV element.

And here below is the excerpt from server side HelpServlet:


protected void doGet(HttpServletRequest request,HttpServletResponse response)
    throws ServletException, IOException {
        
    String toolTipMsgId = getToolTipMessageId(request);
        
    String msg = getToolTipMessage(toolTipMsgId);
        
    returnToolTipMessage(response, msg);
}
    
private void returnToolTipMessage(HttpServletResponse response, String msg) 
            throws IOException, UnsupportedEncodingException {
        
       if(StringUtils.isNotEmpty(msg)) {
        response.setContentType("text/plain");
        OutputStream out = response.getOutputStream();
        response.setContentLength(msg.getBytes("utf-8").length);
        out.write(msg.getBytes("utf-8"));
        out.close();
}
}

private String getToolTipMessage(String toolTipMsgId) {
        String msg = toolTipMessageProperties.getProperty(toolTipMsgId);
        return msg;
}

private String getToolTipMessageId(HttpServletRequest request) {
        String toolTipMsgId = request.getParameter("toolTipMsgId");
        return toolTipMsgId;
}

As you see above, we get toolTipMsgId parameter value from request, and lookup corresponding tooltip message value in the properties file. Finally if there is an existing tooltip message for the component, we return that message as plain text through the servlet response.

Enabling SSL with Client Authentication in Tomcat

It is very common to enable SSL only with server authentication, because it is required from SSL specification. However, it is not so common to activate client authentication as it is optional.

Enabling SSL is a server dependent process. I first give a rough overview of this process step by step and then explain each one in detail with examples using Tomcat.

  • First configure Connector definition in tomcat server.xml file.
  • Create a certificate with alias tomcat for server authentication and place it in a keystore that will be accessed by tomcat
  • Create a trust keystore that will be used to keep trusted certificate entry which will be used during client authentication.
  • Create a client certificate that will be used for client authentication.

That’s all! It is time to now go over each step in detail.

As a first step, we have to configure Connector definition in server.xml file. By default server.xml file contains a Connector definition for SSL, but it is commented, and we need to add some other attributes to it. We can start with commenting out that connector definition.

<Connector port="8443" 
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" disableUploadTimeout="true"
               acceptCount="100" debug="0" scheme="https" secure="true"
               clientAuth="true" sslProtocol="TLS" />

We have to convert clientAuth attribute’s value to true to enable client authentication, otherwise our server will not ask a client certificate. Then we specify our keystore file location, in which out server’s certificate will be kept, and its password.


keystoreFile=”/keystores/server.keystore”    keystorePass=”secret”

By default tomcat looks for server certificate in a keystore that is in user’s home directory with default password “changeit”.

In addition to specifying keystore location for server certificate, we need also to specify tomcat where to look for validating client certificates.


truststoreFile=”/keystores/trust.keystore”

We can use same password for both of the keystore files and specifying only one’s is enough. Providing truststoreFile in connector definition is important, otherwise internet explorer will not be able to display available certificates that can be used for client authetication.

Second step is creating necessary certificates both for server and client. We need a trusted certificate authority to create and validate those certificates. I used Microsoft Certification Authority for this step. It is also possible to create self signed server certificate using java keytool, and use it in server authentication, but for client authentication we need to create a personal client certificate and this is not possible with keytool.
We can download CA certificate from MS Certification Authority via its web interface,and then install it in our trust and server keystores. For example let say ca.cer is our downloaded CA certificate file;


keytool –import –trustcacerts –alias ca –file ca.cer –keystore /keystores/server.keystore –storepass secret
keytool –import –trustcacerts –alias ca –file ca.cer –keystore /keystores/trust.keystore –storepass secret

keytool –genkey –alias tomcat –keystore /keystores/server.keystore –storepass secret

This will generate public-private key pair that will be used for server authentication. It is important to make key and keystore passwords same, otherwise tomcat will not be able to access certificate. Later we create a certification request using this generated key pair.


keytool –certreq –alias tomcat –file /keystores/tomcat.req –keystore /keystores/server.keystore –storepass secret

We use this certification request to create a certificate from Certification Authority, again via its web interface. When our certificate is ready, we need to import it into our keystore. Let say our generated certificate is in file tomcat.cer;


keytool –import –alias tomcat –file /keystore/tomcat.cer –keystore /keystores/server.keystore –storepass secret

imports certificate that will be used for server authentication.

We also need to create a client certificate that will be used to authenticate our client/user. We create it via Certification Authority. We make a certification request. When CA issues a certificate for us, we need to install it using Internet Explorer, accessing through web interface of Certification Authority. Internet Explorer keeps our client certificate’s private key  in a safe place, that is local to our client’s machine, so this certificate will be working only from our client’s machine from which we make certification request.

One important key note here: Internet Explorer will not display client certificate selection dialog, if there is no valid or there is only one valid client certificate installed in the client machine. If we want to display this dialog, even for those cases, we need to configure it using Internet Options>Security Settings window, by disabling “Don’t prompt for client certificate selection when no certificates or only one certificate exists”. Internet Explorer determines valid client certificates via tomcat’s trusted certificate located in trustkeystore. If we don’t provide trustkeystore for Tomcat, Internet Explorer will not be able to show our valid client certifices in its certificate selection dialog.

Finally it is time to give a try for our SSL configuration: https://localhost:8443.

Delegating Authentication to JAAS Module in Acegi Security

We are currently using Acegi Security in our web project. At the moment we employ its form based authentication. In the future, we have to integrate our web application with an environment, in which JAAS based single sign on mechanism will be used for authentication. For this moment, as a first step, we tried to delegate authentication to a JAAS module using Acegi’s JaasAuthenticationProvider. Configuration process is very simple, and is explained in Acegi Reference Documentation.
There is an inbalance between JAAS and Acegi Security System. In JAAS everything, even roles, are represented as principals, but in Acegi there is Authentication object, in which there exists one named principal, which corresponds simply to username, and multiple GrantedAuthority objects, which simply correspond to roles.There must be a facility to map between JAAS Principal objects and Acegi GrantedAuthority objects. Acegi provides AuthorityGranter interface for this mapping. JaasAuthenticationProvider passes each principal fetched from login module to the AuthorityGranter object. AuthorityGranter object inspects that principal object and returns a string, as a role information, if current principal corresponds to a valid role. JaasAuthenticationProvider uses those role information and principal name to create JaasGrantedAuthority objects. Finally, Acegi Authentication object consists of those GrantedAuthority objects.
We have implemented a derivation of database based JAAS authentication module from Tagish JAAS Login Modules, and used a principal type similar to its TypedPrincipal. This contains information which means what type of principal it is, such as user or role. Our login module gets user information from specified database location for authentication. This user information contains username, password and its roles. Later JaasAuthenticationProvider passes each principal into our RoleNameBasedAuthorityGranter implementation.  RoleNameBasedAuthorityGranter checks if passed principal represents a role of current user. If it is, then it retuns role name string back to JaasAuthenticationProvider. Finally, JaasAuthenticationProvider uses those information to construct an Authentication object if authentication is successfull.
One drawback of JaasAuthenticationProvider in current Acegi distribution is that it isn’t able to cache authenticated user information. In order to remedy this problem, we have extended JaasAuthenticationProvider and added a UserCache object. Our CachingJaasAuthenticationProvider first looks into user cache for user details and if it finds any, uses it to perform authentication, otherwise it delegates authentication to its super. After successfull authentication, its caches user details in case it is needed for successive queries.
I would like to mention, as a footnote, that there is a nice blog entry in Thomas Dudziak’s Weblog, where he explains how to enable JAAS authentication and authorization for a Struts based web application step by step. It is very easy to adapt it for other types of web applications, too.
Finally, I appended source code of our RoleNameBasedAuthorityGranter, CachingJaasAuthenticationProvider and its spring bean definition below for future reference:

public class CachingJaasAuthenticationProvider extends
        JaasAuthenticationProvider {

    private UserCache userCache = new NullUserCache();
    
    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }
    
    public UserCache getUserCache() {
        return userCache;
    }
    
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {
        String username = "NONE_PROVIDED";
        
        if (auth.getPrincipal() != null) {
            username = auth.getPrincipal().toString();
        }

        if (auth.getPrincipal() instanceof UserDetails) {
            username = ((UserDetails) auth.getPrincipal()).getUsername();
        }

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        
        if(user != null && isPasswordCorrect(auth,user)) {
            publishSuccessEvent((UsernamePasswordAuthenticationToken)auth);
        } else {
            cacheWasUsed = false;
            
            auth = super.authenticate(auth);
            if(auth != null) {
                auth = createSuccessAuthentication(auth);
                user = (UserDetails)auth.getPrincipal();
                this.userCache.putUserInCache(user);
            }
        }
        
        return auth;
            
    }
    
    private Authentication createSuccessAuthentication(Authentication auth) {
  UserDetails userDetails = new JaasUser((String)auth.getPrincipal(),
auth.getCredentials().toString(),auth.getAuthorities());
        
        UsernamePasswordAuthenticationToken result = 
new UsernamePasswordAuthenticationToken(
                userDetails,auth.getCredentials(),auth.getAuthorities());
        
        return result;
    }
    
    private boolean isPasswordCorrect(
Authentication authentication, UserDetails user) {
        if(StringUtils.isNotEmpty(user.getPassword())) {
            return user.getPassword().equals(
authentication.getCredentials());
        }
        return false;
    }
}


public class RoleNameBasedAuthorityGranter implements AuthorityGranter {

    public String grant(Principal principal) {
        if(principal instanceof TypedPrincipal) {
            TypedPrincipal typedPrincipal = (TypedPrincipal)principal;
            if(typedPrincipal.getType() == TypedPrincipal.ROLE) {
                return principal.getName();
            }
        }
        return null;
    }

}
<bean id="jaasAuthenticationProvider" class="tbs.verisozlugu.guvenlik.jaas.CachingJaasAuthenticationProvider">
<property name="loginConfig">
            <value>VeriSozlugu.login</value>
      </property>
      <property name="loginContextName">
            <value>VeriSozlugu</value>
      </property>
      <property name="callbackHandlers">
            <list>
<bean class="net.sf.acegisecurity.providers.jaas.JaasNameCallbackHandler"/>
<bean class="net.sf.acegisecurity.providers.jaas.JaasPasswordCallbackHandler"/>
            </list>
      </property>
      <property name="authorityGranters">
            <list>
      <bean class="tbs.verisozlugu.guvenlik.jaas.RoleNameBasedAuthorityGranter"/>
            </list>
          </property>
    <property name="userCache">
        <ref bean="userCache"/>
    </property>
</bean>