Project Experience with Acegi Security, Spring MVC and Oracle MapViewer americas cup win 2682133k1

Project Experience with Acegi Security, Spring MVC and Oracle MapViewer

For a customer with an interesting business we had to build a web application that has a RIA front end, displays data on maps, authenticates thru a web service that is connected with a back office customer subscription system, and that can handle any kind of data (for example shops, garages or whatever) as long as it conforms to a certain format. Depending on the specific dataset the application is configured in a different way and behaves differently. I call it a meta data driven application.

Anyway, taken all these aspects together, the building process has been an exciting adventure. From all frameworks we took the latest versions, and came up with the following tech stack: Spring 2.0, Acegi Security System 1.0, Dojo Javascript toolkit, Oracle (Ajax based) MapViewer 10.1.3.1 (released a month ago!) and Oracle 10g database. Spring formed the ecosystem for our application in which we plugged the Javascript presentation layer at the front end, and the unbreakable Oracle database at the back end. In the same container the Oracle MapViewer application is running. Ajax calls are made from the main application to mapViewer server in order to manage map standard behavior like panning and to display fields of interest (FOIs). Apart from these larger components of the architecture, there are other technologies used, for instance: Xfire Java SOAP framework, Commons file upload, JFreeChart, Junit, Log4j and more (yeah, Bruce Tate is probably right when he states in Beyond Java, that
developing a web application nowadays involves too many technologies and frameworks). Looking at this technological cathedral, I just like to mention – in the story below – some parts and experiences that I personally liked or disliked. The different subjects below are:

  • Upgrading from Acegi Security version 0.8 to 1.0
  • Providing authentication in Acegi Security by a webservice
  • Integrating Spring SimpleFormController and Dojo dojo.io.FormBind
  • Using Javascript API of Oracle Mapviewer.

Before I continue, I want to mention the infrastructure we used to conduct the project. First of all we used Subversion for source control. Secondly we used Maven2 for project management. Our project manager add a bunch of plugins in order to have Maven authomatically generate reports, like: Surefire test results of the project, JavaNCSS code metric analysis, file and developer activity reports from source control, et cetera. Thirdly, continuous integration by Continuum. My experience is that these three tools – Subversion, Maven and Continuum – are really important. Especially management of dependencies, the ease for a team member to set up a project in his development environment, the early detection of code errors, prevents waisting time and frustration and promotes focusing on the real stuff, namely programming – and with pleasure. I also noticed that these tools promote unit testing (test driven development). When I am fair, I have to admit that I always regarded unit tests as a kind of burden, however during this project I experienced the opposite: I have the feeling that I can not develop without unit tests. I am not yet at the level that I first write the test, but immediately after writing a method, I create the test. I don’t need to run the application to see if its fine and that saves a lot of time.

Upgrading from Acegi Security version 0.8 to 1.0

In a former project I used Acegi to authenticate and authorize users by means of information stored in the database. This was version 0.8.3. In the meanwhile this framework had been modified quite a bit. Classes and packages had been renamed, classes moved to other packages, anyway, not small changes. I was confronting a dilemma: should I upgrade to the newest version 1.0 and take advantage of the improvements or should I stay at 0.8.3 and benefit from the fact that I could nearly one-to-one copy-paste the (complex) configuration XML file for the Acegi beans? I decided to upgrade, I just went to the acegisecurity.org site and started reading Upgrading from 0.8.0 to 0.9.0 and Upgrading from 0.9.0 to 1.0.0 pages. I simply modified my original securityContext.xml file according to the change description on the site. Then I restarted the application… and it worked immediately! This has been one of my best experiences in my life as programmer.

The configuration of the Acegi beans does not differ much between one project and another, therefore it can be reused to a large extend in a new project. Here’s the listing for a 1.0 configuration that uses a JDBC authentication DAO:

.{beans}
.        {bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"}
.          {property name="userDetailsService" ref="jdbcAuthenticationDao" /}
.                {property name="userCache"}
.                        {bean id="userCache" class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache"}
.                                {property name="cache"}
.                                        {bean class="org.springframework.cache.ehcache.EhCacheFactoryBean"}
.                                                {property name="cacheManager" ref="cacheManager" /}
.                                                {property name="cacheName" value="userCache" /}
.                                        {/bean}
.                                {/property}
.                        {/bean}
.                {/property}
.        {/bean}
.
.        {bean id="jdbcAuthenticationDao" class="nl.amis.project.security.AuthenticationJdbcDaoImpl"}
.                {property name="dataSource"}
.                        {ref bean="dataSource" /}
.                {/property}
.        {/bean}
.
.        {bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" /}
.
.        {bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"}
.                {property name="providers"}
.                        {list}
.                                {ref local="daoAuthenticationProvider" /}
.                        {/list}
.                {/property}
.        {/bean}
.
.        {bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"}
.                {property name="authenticationManager"}
.                        {ref bean="authenticationManager" /}
.                {/property}
.                {property name="authenticationFailureUrl" value="/login.do?error=1" /}
.                {property name="defaultTargetUrl" value="/" /}
.                {property name="filterProcessesUrl" value="/j_acegi_security_check" /}
.        {/bean}
.
.        {bean id="httpRequestAccessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased"}
.                {property name="allowIfAllAbstainDecisions" value="false" /}
.                {property name="decisionVoters"}
.                        {!-- An access decision voter that reads ROLE_* configuration settings --}
.                        {bean class="org.acegisecurity.vote.RoleVoter" /}
.                {/property}
.        {/bean}
.
.        {bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter"}
.                {property name="authenticationEntryPoint" ref="authenticationEntryPoint" /}
.        {/bean}
.
.        {bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"}
.                {property name="context" value="org.acegisecurity.context.SecurityContextImpl" /}
.        {/bean}
.
.        {bean id="authenticationEntryPoint" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"}
.                {property name="loginFormUrl" value="/login.do" /}
.                {property name="forceHttps" value="false" /}
.        {/bean}
.
.        {bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor"}
.                {property name="authenticationManager" ref="authenticationManager" /}
.                {property name="accessDecisionManager" ref="httpRequestAccessDecisionManager" /}
.                {property name="objectDefinitionSource"}
.                        {value}
.                                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
.                                PATTERN_TYPE_APACHE_ANT
.                                /secure/*.do=ROLE_CUSTOMER
.                                /secure/admin/*.do=ROLE_ADMIN
.                       {/value}
.                {/property}
.        {/bean}
.
.        {bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"}
.                {property name="filterInvocationDefinitionSource"}
.                        {value}
.                                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
.                                PATTERN_TYPE_APACHE_ANT
.                                /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor
.                        {/value}
.                {/property}
.        {/bean}
.{/beans}

and the Dao itself:

.public class AuthenticationJdbcDaoImpl extends JdbcDaoImpl {

.    protected void initMappingSqlQueries() {
.        setUsersByUsernameQuery("select username as username, 'welcome' as password, 1 as enabled from wlv_users where upper(username)=upper(?)");
.        setAuthoritiesByUsernameQuery("select username as username, 'ROLE_CUSTOMER' as authority from wlv_users where upper(username)=upper(?)");
.        // must user super keyword otherwise run into unending recursive loop
.        super.initMappingSqlQueries();
.    }
.}

Providing authentication in Acegi Security by a webservice

Our customer has a subscription system. Organizations subscripe themselves to datasets and (record and/or attribute based) subsets of these datasets. Members
of these organizations are registered and have access to the data of their organization. Users logon thru a webservice. Therefore a second challenge was how to integrate this webservice with Acegi Security. Challenge? In fact it was quite easy: only the authentication provider bean referenced by the providers property of the authenticationManager bean must be replaced by another bean, namely an AbstractUserDetailsAuthenticationProvider bean.

.{bean id="wsAuthenticationProvider" class="nl.amis.project.security.WSAuthenticationProvider"}
.        {property name="userDetailsService" ref="authenticationWebService" /}
.{/bean}
.
.{bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"}
.        {property name="providers"}
.                {list}
.                        {ref local="wsAuthenticationProvider" /}
.                {/list}
.        {/property}
.{/bean}

In the API documentation (acegisecurity.org/multiproject/acegi-security/apidocs), it can be seen that AbstractUserDetailsAuthenticationProvider is one level higher in the class hierarchy then the DaoAuthenticationProvider. Authentication thru a webservice means that we have to create our own implementation of an AuthenticationProvider. The abstract class helps us a lot. In the example below I just implemented the retrieveUser method (and leaving the implementation of the other abstract method, additionalAuthenticationChecks, empty):

.public class WSAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
.
.    private AuthorizationServiceInterface userDetailsService;
.
.    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
.
.        String pw = (String) authentication.getCredentials();
.
.        Credentials cred = new Credentials();
.
.        // call abonnementen WS
.        try {
.            cred = userDetailsService.getCredentials(username, pw);
.        } catch (WSException e) {
.            throw new WLVSecurityException(e.getErrorMsg());
.        }
.
.        return new WLVUser(username, pw, cred.getUserId(), cred.getUserName(),
.            cred.getOrganisationId(), cred.getOrganisationName());
.    }
.
.    protected void additionalAuthenticationChecks(UserDetails userDetails,
.            UsernamePasswordAuthenticationToken authentication) {
.    }
.
.    public void setUserDetailsService(AuthorizationServiceInterface userDetailsService) {
.        this.userDetailsService = userDetailsService;
.    }
.}

The webservice method getCredentials is called with two arguments: username and password. The values of these parameters come from the logon form and are stored
in the UsernamePasswordAuthenticationToken object. When the webservice does not throw an exception the user is authenticated successfully. The Credentials
object stores the output of the webservice (some IDs and names). The WLVUser object is a subclass of org.acegisecurity.userdetails.User (that implements
UserDetails). Note that the webservice is injected by method setUserDetailsService. The webservice bean is configured in a service configuration XML file:

.{bean id="authenticationWebService" class="org.codehaus.xfire.spring.remoting.XFireClientFactoryBean"}
.       {property name="lookupServiceOnStartup"}
.               {value}false{/value}
.       {/property}
.       {property name="serviceClass"}
.               {value}
.                 nl.amis.project.webservice.AuthorizationServiceInterface
.               {/value}
.       {/property}
.       {property name="wsdlDocumentUrl"}
.               {value}
.                 http://customer.nl:8888/weblv-webservice-context-root/loginHttpPort?WSDL
.               {/value}
.       {/property}
.{/bean}

Integrating Spring SimpleFormController and Dojo dojo.io.FormBind

A different subject is the question how the SimpleFormController might be used when the presentation layer is Dojo Javascript in the browser and that makes
Ajax calls to the server. This problem was a challenge. Of course one can work around this problem by using a different Spring controller type (for example
MultiActionController), but in that case one does not leverage the SimpleFormController features, like form validation. Below a solution is presented. It works. However I am not sure if it is the best way, or whether is can be improved. So please, let me know what you think of it. I am very curious what other possibilities there might be. Here we go.

In the JSP below a regular form is defined. The action URL manageMyDataForm.do is mapped to a SimpleFormController, named ManageMyDataFormController. Maybe the
only weird thing of the form is the table row element with id manageMyDataFormFieldList. Another piece of Javascript inserts HTML here in order to display a table with multiple records that can be updated simultanously (just imagine that table there). Above the form there is a piece of Javascript. The function init creates the dojo.io.FormBind object. This object send forms via dojo.io.bind. The callback function receives and processes the response of the Spring controller. This processing is very simple, namely when there is a (validation) error, then the error message will be displayed by setting the innerHTML attribute of the error DIV element. When success, then the dialog window is hided. The funny thing was that I couldn’t get this to work for hours, just to discover that a name was already used somewhere else in the Javascript code of the application. Therefore I decided to use these long names, to make sure variables are unique. Javascript does not complain: it just doesn’t do what you expect. The Firefox plugin FireBug is therefore an indispensible tool when working with Javascript. The IDE IntelliJ IDEA also has good Javascript tools.

.{script language=javascript}
.    var manageMyDataDialog;
.
.    function init(e) {
.        manageMyDataDialog = dojo.widget.byId("manageMyDataDialog");
.        var cancelButton = dojo.byId("manageMyDataFormCancel");
.        manageMyDataDialog.setCloseControl(cancelButton);
.
.        x = new dojo.io.FormBind({
.            formNode: dojo.byId("manageMyDataForm"),
.            load: function(type, data, e) {
.                if (data.indexOf('1') == 0) {
.                    manageMyDataDialog.hide();
.                }
.                else {
.                    dojo.byId("manageMyDataFormErrorMessage").innerHTML = data;
.                }
.            }
.        });
.    }
.
.    dojo.addOnLoad(init);
.{/script}
.
.{form id="manageMyDataForm" method="post" action="manageMyDataForm.do"}
.    {table}
.        {tr}
.            {td}
.                {fieldset}
.                    {legend}
.                        {spring:message code="form.myData.manage.title"}{/spring:message}
.                    {/legend}
.                    {div class="error" id="manageMyDataFormErrorMessage"}{/div}
.                    {table}
.                        {tr id="manageMyDataFormFieldList"}{/tr}
.                    {/table}
.                {/fieldset}
.            {/td}
.        {/tr}
.        {tr}
.            {td align="right"}
.                {input type="submit" id="manageMyDataFormSave" name="save"
.                       value="{spring:message code='form.save'/}"/}
.                {input type="button" id="manageMyDataFormCancel" name="cancel"
.                       value="{spring:message code='form.cancel'/}"/}
.            {/td}
.        {/tr}
.    {/table}
.{/form}

Now we have a look at the server side: the Spring controller layer. The SimpleFormController is configured like this:

.{bean id="manageMyDataFormController"
.       class="nl.amis.project.web.ManageMyDataFormController"}
.            {property name="metaFieldService"}
.                    {ref bean="metaFieldService" /}
.            {/property}
.            {property name="commandClass"}
.              {value}nl.amis.project.domain.metadata.MyDataMetaFieldList{/value}
.            {/property}
.            {property name="validator"}
.              {ref local="manageMyDataValidator"/}
.            {/property}
.            {property name="formView"}
.                    {value}formView{/value}
.            {/property}
.            {property name="successView"}
.                    {value}successView{/value}
.            {/property}
.            {property name="sessionForm"}
.                    {value}false{/value}
.            {/property}
.            {property name="bindOnNewForm"}
.                    {value}false{/value}
.            {/property}
.{/bean}

The controller bean uses a service bean. The form is bound to a domain object, which wraps a collection. A validator is registered. The code of the class is also quite simple:

.public class ManageMyDataFormController extends SimpleFormController {
.
.    private MetaFieldService metaFieldService;
.
.    protected MyDataMetaFieldList formBackingObject(HttpServletRequest request)
.            throws Exception {
.        List l = metaFieldService.getMyOwnMyDataMetaFields();
.        MyDataMetaFieldList fieldList = new MyDataMetaFieldList();
.        for(MyDataMetaField my : l) {
.            fieldList.getList().add(my);
.        }
.        return fieldList;
.    }
.
.    protected void doSubmitAction(Object command) throws Exception {
.        MyDataMetaFieldList l = (MyDataMetaFieldList)command;
.        List myDataFields = l.getList();
.        for (MyDataMetaField my : myDataFields) {
.            metaFieldService.modifyMyDataMetaField(my);
.        }
.    }
.
.    public void setMetaFieldService(MetaFieldService metaFieldService) {
.        this.metaFieldService = metaFieldService;
.    }
.}

Important to note is that the formBackingObject just prepopulates the collection in the domain object, because we are dealing here with indexed properties. They
need to be initialized before binding can take place. If the form consists of regular fields, then the formBackingObject is not needed and we just need to
implement the doSubmitAction method. In this example a service is called that performs a database update. This is not important. However what is important are
the views that are rendered, namely successView and formView. The idea has been that the successView.jsp just contains a constant, e.g. ‘1’ or ‘ OK’. The callback function will just look for this constant and decide that the form has been successfully processed. This means that there will be a single successView for all SimpleFormControllers, that has the general purpose to let the Dojo client side know that form submit has been success. Same holds true for (validation) errors. The formView.jsp could look like this:

.{spring:bind path="command.errorMessage"}
.    {c:if test="${not empty status.errorMessage}"}
.      {c:out value="${status.errorMessage}"/}
.    {/c:if}
.{/spring:bind}

Note that each domain object used for form binding, should have an errorMessage attribute. Doesn’t this look like the good old client/server stuff?

Using Javascript API of Oracle Mapviewer

The developer in the team that was implementing and integrating Oracle MapViewer into the Spring based application, unexpectedly and sadly left the team because
of illness. This happened quit near the date that we had planned to deliver the application to system test by the customer. The other team members (including
myself) suddenly had to dive into this map stuff. What I learned from this sudden change, is the importance of communication between developers. We had the
tendency that once the tasks were more or less divided, the developers were quit focused on their own stuff. This might be very well understandable from a
psychological standpoint, but the result is that after a while the developers do not know too much about the others regarding: technics, issues and status. This
reminds me of what I once read about Extreme Programming. For example the Quick Design Session in XP in which people spend 10 – 30 minutes discussing
the design on a regular basis, makes a lot of sense. The same holds true for Pair Programming. This would prevent my hair getting grey because of a sudden switch to the MapViewer technology.

Yes indeed, MapViewer is an interesting product. It exists for quite some time, but the latest version 10.1.3.1, has the Google/Ajax experience. It’s cool. More information can be found at oracle.com/technology/software/products/mapviewer. However after writing some Javascript code that uses the MapViewer Javascript API and testing it, I encountered a dramatically bad performance. The purpose of the code was to display objects on a raster map. When selecting a node in a tree widget, the children of the selected tree node must be displayed on the map. This works as follows: in the MapViewer server a so-called theme base FOI (field of interest) layer is defined (in XML). This layer contains a SQL select statement that contains the spatial attribute that must be selected (e.g. XY coordinates) and a query condition. By means of a Javascript function a parameter is bound to this query. This is shown in the example below:

.{html}
.{head}
.    {script type="text/javascript" xsrc="js/oraclemaps.js" mce_src="js/oraclemaps.js"            }{/script}
.    {script type="text/javascript" xsrc="js/mv_globals.js" mce_src="js/mv_globals.js"            }{/script}
.    {script type="text/javascript" xsrc="js/mv_functions.js" mce_src="js/mv_functions.js"            }{/script}
.
.    {script language="JavaScript" type="text/javascript"}
.        function showRasterMap() {
.            // initialize map
.            raster_mvo = new MVMapView(document.getElementById("rastermap"), baseURL);
.            raster_mvo.addBaseMapLayer(new MVBaseMap("MVDS.RASTER_MAPCACHE"));
.            raster_mvo.setCenter(MVSdoGeometry.createPoint(150000, 450000, 90112));
.            raster_mvo.setZoomLevel(0);
.            raster_mvo.addScaleBar();
.            raster_mvo.display();
.
.            // add FOI
.            raster_foi = new MVThemeBasedFOI('rasterUnitsFoi', 'MVDS.THEMERASTERUNITS', baseURL);
.            //raster_foi.setBringToTopOnMouseOver(true);
.            raster_foi.enableInfoTip(true);
.            raster_foi.setVisible(true);
.            raster_mvo.addThemeBasedFOI(raster_foi);
.        }
.
.        function showRasterFOI(arr) {
.            var arrParam = new ArrayParameter(arr, 'narray', 't_unitids');
.            raster_foi.setQueryParameters(arrParam);
.            raster_foi.refresh(true);
.            //raster_mvo.display(); THIS KILLS PERFORMANCE
.        }
.    {/script}
.{/head}
.
.{body onload="javascript:showRasterMap()"}
.  {div id="rastermap" style="position:absolute; left:10px; top:10px; width:700px; height:700px"}{/div}
.  {input type="button" onclick="javascrip:showRasterFOI('829')" value="show 1 point"
.       style="position:absolute; left:710px; top:10px;"/}
.  {input type="button" onclick="javascrip:showRasterFOI('1379,336,418,11,401')" value="show 5 points"
.       style="position:absolute; left:710px; top:50px;"/}
.{/body}
.{/html}

Firstly, a map is initialized (new MVMapView). A base map layer is added. This is the raster map itself on top of it other stuff can be projected, like FOIs. A
theme based FOI object is created that references the FOI layer on the server (THEMERASTERUNITS). Secondly, a parameter is created which is an array of
numbers (the IDs of the records we want to display as icons on the map). This parameter is then passed into a Javascript function that sets the query
parameters on the server (setQueryParameters). When a call is made to function refresh, the query is performed and the XY coordinates magically rendered on the
map as icons. This looks quite simple. However note the commented line:

raster_mvo.display();

The API documenation just says about the display function: “This method displays all map content inside the map container (which is an HTML DIV element
defined in the web page)”. When I read that the first time, I was under the impression that a call to this display function would be necessary in order to
display the changed map (or at least it wouldn’t harm to do so). However, this results into a serious performance penalty (30 seconds for just one icon)!
Firstly, I didn’t notice this line, and only after many frustrated hours I detected its impact on the performance. Same holds true for the argument of the refresh method:

raster_foi.refresh(true);

It means that the map zooms to a kind of rectangle around the FOI icons on the map. That can be handy functionality. However if you don’t want this automatic zooming, then you also got a performance problem, because for some mysterious reason, the performance is seriously downgraded. I guess this will be a bug.

One Response

  1. jj February 4, 2007