Saturday, September 1, 2012

Returning a partial response in a JAX-RS web service - Part 1

Introduction


This two-part article provides an example of partial response functionality in a Java REST API.

In this example, I am using the Apache CXF framework, Jackson JSON processor, JPA/Hibernate and Spring.

Before I get started, I've got to give a shout out to the guys from apigee.  Their API Best Practices Blog and webinars have been invaluable resources for me.  I have incorporated many of their "Pragmatic REST" recommendations into my API.

What is partial response and why should your API support it?


Partial response is when an API returns only a subset of fields for a requested resource.  This can be done by providing a 'fields' query parameter where the client can specify exactly which fields to return.  Alternatively, the API can default to some subset of fields and only return additional fields when specifically requested.

According to Google,  "when you avoid retrieving and parsing unneeded data, you can significantly improve the efficiency of your client application."  Improved efficiency makes apps faster and reduces the amount of data sent across the wire to your client applications.

 

Why is it important to support partial response in this technology stack?


As mentioned above, I am using Apache CXF framework, Jackson JSON processor, JPA/Hibernate and Spring.

It is common to work with entities that have complex relationships with other entities.  Entity of type 'A' may have a One To Many relationship with entity of type 'B' which in turn has Many to Many relationship with entity 'C'.  These relationships are defined on my POJOs using JPA annotations and are typically represented as collections.

Anyone familiar with using JPA/Hibernate knows that complex entity relationships can can potentially result in Hibernate executing loads of queries and practically loading the entire database into memory.  Thankfully JPA provides the FetchType.LAZY annotation and Hibernate provides Lazy Loading.  This effectively prevents the loading of unnecessary data until it is actually requested.

Lazy Loading works well on the server side but a REST API needs to serialize data and send it across the wire.  In my case, I'm using Jackson to serialize Java objects into JSON format.  By default, the Jackson ObjectMapper has no awareness of Hibernate and it's Lazy Loading scheme.  During the serialization process, Jackson was touching all the properties on the entity which resulted in Hibernate fetching all the data thereby losing the benefits gained from Lazy Loading.

Thankfully the guys from Jackson were aware of this issue and developed the jackson-module-hibernate.  According to the ReadMe, "This module support JSON serialization and deserialization of Hibernate specific datatypes and properties; especially lazy-loading aspects."  With the addition of this module, Jackson no longer tries to serialize Lazy Loaded collections.

This is great and my hat's off to the guys from Jackson, particularly Tatu who has been very responsive and ready to help with Jackson related questions.

So why is partial response so important here?  Well, the problem arises when the client actually wants one of the Lazy Loaded collections.  We need a way to tell the web service which Lazy Loaded fields we want so that it can initialize them and Jackson can thereby serialize the data.

 An example scenario


Here's an example from a simple Flashcards app I've been working on.  In this app, a Flashcard represents a simple Question and Answer and can be organized by assigning one or more Tags.  Sometimes the client app simply needs to display a list of Flashcard questions.  In this case we need the 'id' and the 'question' associated with each Flashcard.  However, there is another case where the client app needs the same list of Flashcards along with the collection of Tags assigned to each card.

Well, there is of course a lot of ways to skin this cat.  But which solution requires the fewest web services, is the most generic and reusable and has the best performance by limiting the amount of data sent in each response?

Partial responses could solve this problem quite nicely.  Using partial response, we can fulfill the sencario described above while using a single JAX-RS web service.

Here's an example of some URL's that we could use:
  1. GET /api/v1/flashcards/?fields=id,question
  2. GET /api/v1/flashcards/?fields=id,question,tags,name
It should be pretty clear the first URL will provide a list of Flashcards with only the 'id' and 'question' properties.

To understand the second URL, it might help to see  the API doc for a list of properties associated with the Flashcard and Tag entities.  Suffice it to say, the second URL returns the same result as the first URL with the addition of their collection of Tags and the 'name' property of the Tag entity.

In Part 2 of this blog, I'll provide some code examples.

Returning a partial response in a JAX-RS web service - Part 2

See Part 1 of this blog post for background and reasoning for providing partial response in your JAX-RS API.

Here's the basic steps:
  1. A custom Apache CXF RequestHandler filter inspects each Request to look for a 'fields' Query Parameter.
  2. When the 'fields' parameter is found and has a value, we make sure the requested fields are initialized by Hibernate.
  3. Lastly, we use a Jackson ObjectWriter to apply a Jackson filter to the object and then build and return a Response with the resulting JSON.
Here's a link  to the ResponseFilter I'm using in my sample project in Github.

First we inspect the Request and make sure it is a Get, the request succeeded (200 OK), has a JSON response type, and indeed has 'fields' query parameter and corresponding value.

At this point let's make sure any requested lazy loaded fields get initialized.  I'm sure there's a better way of doing the next part.


The code that initializes the fields is beyond scope of this post but here's a link to the class in my Github repo in case you are interested.

Lastly, let's apply the Jackson filter and build the response. Notice we are adding "apiFilter" to the SimpleFilterProvider. Your entity classes will need to be annotated with @JsonFilter("apiFilter") and your CustomObjectMapper should be configured with filterProvider.setFailOnUnknownId(false);


Any reader comments, questions, and suggestions are appreciated. Thanks!

Friday, August 12, 2011

Creating a server instance and deploying a Java app on Amazon's EC2 Cloud

My previous blog post detailed the process by which I chose to host my web application on the Amazons EC2 Cloud using the Bitnami Tomcat Stack AMI.  This post will describe some of the steps I followed when creating the server instance, connecting to it and deploying a Java .war.

Connecting to your Amazon EC2 server instance

There's no need for me to write step-by-step directions for creating the instance since Bitnami has already done a great job in providing instructions.  It's quite simple and I didn't run into any issues creating the instance.

Don't forget to open the SSH and HTTP ports for your server instance using the Security Groups function of the AWS Management Console.  Once you open port 80 you will be able to access the default Tomcat web page in your browser via your instance's public DNS name (ie. http://ec2-107-20-221-246.compute-1.amazonaws.com).

Connecting to the new server instance is a little trickier, particularly for someone who isn't exactly a Unix/Linux guru.  However, it's still relatively simple and again Bitnami has provided detailed instructions.

They key to connecting to your instance is the key.  :) Well, key pairs to be exact.  It's not just the key pair, you'll also need an SSH client like PuTTy.  You will be prompted to create and download the key when you create your first EC2 server instance.  Keep this file secure and do not lose it.

Here's the part that tripped me up: the key you download from AWS is not compatible with the PuTTy SSH client.  AWS provides a key in .pem format and you'll need to convert it to the .ppk format that PuTTy can use.  This excellent blog post provides details and instructions for converting the key and connecting with PuTTy.

Configuring MySQL

My next step was to access MySQL and create my database schema. I installed Navicat Lite on my desktop and configured it as described by Bitnami here.  This worked like a charm and it wasn't long before I had remotely logged into MySQL running on my server instance on EC2.  I  ran SQL commands to create a database schema, user and grant privileges.  I then imported a .sql file with the DDL I had exported from my local database using MySQL's "mysqldump" tool.

Deploying a .war file to Tomcat

I installed the WinSCP FTP client on my development machine and configured it to connect to my server instance.  Then I FTP'd my .war file to the "/opt/bitami/apache-tomcat/webapps" directory and voila!  My web application was now running on Amazon's EC2.

Getting started with Amazon's Elastic Compute Cloud (EC2) and the Bitnami Tomcat Stack

My next couple of blog posts will detail my experiences thus far with Amazon's Elastic Computing Cloud (Amazon EC2) and the Bitnami TomcatStack Amazon Machine Image (AMI).

Choosing Amazon's EC2

In the past, I have typically deployed my Java applications within a company intranet or extranet.  However,  my current project needs to be publicly accessible and it's up to me to choose a suitable and ultra-affordable web hosting provider.

My hosting requirements are fairly simple.  I want to deploy my application on Apache Tomcat using a MySQL database and several popular Java frameworks including Hibernate, Struts 2, and eventually Spring.  Other than that, I don't expect my current web application to require much in terms of bandwidth and resources.

I was considering trying Google's App Engine until I learned the App Engine doesn't support the Hibernate ORM framework and requires some tweaking to work with Struts 2.

I began to look more closely at Amazon's EC2 service after reading several StackOverflow posts (here and here) suggesting EC2 as an alternative to traditional web hosting services for Java applications.  I soon learned that Amazon offers a Free Usage Tier for new customers.  As described on their website, the "free usage tier can be used for anything you want to run in the cloud: launch new applications, test existing applications in the cloud, or simply gain hands-on experience with AWS."

At first, getting started with EC2 initially seemed a like daunting task.  I explored using Amazon's Elastic Beanstalk which is promoted as "an even easier way for you to quickly deploy and manage applications in the AWS cloud."  However, I couldn't find any documentation or examples of using Beanstalk with MySQL.  The examples I found used the Amazon SimpleDB and it wasn't clear whether I could easily use SimpleDB with Hibernate.  Lacking a clear understanding of how to use Beanstalk with MySQL I decided not to try Beanstalk.  (Note: Please let me know if you have used Beanstalk with MySQL or if you've found some resources on this topic.  Thanks!)

Choosing an Amazon Machine Image (AMI)

After creating an Amazon AWS account, it was time to select and launch a server instance on the EC2.  But which AMI should I select for my EC2 instance?  There are thousands of Community AMI's, hundreds of which are listed as free tier eligible.

Here's a useful blog post I found describing the steps necessary to select and install Java, Tomcat, and MySQL.  The blog author chose the Amazon Linux AMI.  This AMI is a good starting point and is actively supported by Amazon.  "Amazon Web Services also provides ongoing security and maintenance updates to all instances running the Amazon Linux AMI. The Amazon Linux AMI is provided at no additional charge to Amazon EC2 users."

But alas, is there an even easier way to get started?  With all those Community AMI's out there can I find one that bundles Linux, Java, Apache, Tomcat and MySQL.  The answer is yes!  The Bitnami Tomcat Stack "includes ready-to-run versions of Apache, Tomcat, MySQL and Java and required dependencies" and "is distributed for free under the Apache 2.0."  Nice!

Bitnami also offers a Cloud Hosting service which in conjunction with the Amazon EC2 provides a number of ease-of-use benefits.  However, it is not required to use the Bitnami Cloud Hosting service when selecting one of their free Community AMI's.

In my next blog post I'll explain the process of creating and connecting to the Amazon EC2 instance.

Sunday, August 7, 2011

Using Hibernate's event architecture to manage created and updated timestamp fields

This blog post describes a method for managing created and last modified timestamp table columns using Hibernate 3.6.  Before I detail the working solution, I'll explain a failed approach I attempted.  I'm including this approach because it turned out to be useful learning experience.  You can skip to the bottom section of this post if you want to go straight to the working solution. :)

A little Googling found a Stack Overflow post suggesting using @Prepersist and @PreUpdate JPA Annotations.  Up to this point I'd been using Hibernate mapping files (hbm.xml) to define my entities.  I'd been using Ant and the hibernatetool task to generate both the POJO classes and the Database tables.  For the most part I was satisfied using the hbm xml mappings although I had planned on eventually converting to JPA Annotations mappings anyway.

The @Prepersist and @PreUpdate annotations looked simple enough and I decided now was time to bite the bullet and I convert all my .hbm xml mappings to JPA Annotations.  This took some time but was a worthwhile experience.  Maybe I'll write a blog post discussing the process I used for migrating from .hbm xml mapping files to JPA Annotations.

When I was finally satisfied my newly converted JPA Annotated classes were functioning as expected I went ahead and added @Prepersist and @PreUpdate to my entity POJOs as follows:

@Temporal(TemporalType.TIMESTAMP)
@Column(name="created", nullable = false)
public Date getCreated() {return created;}
public void setCreated(Date created) {this.created = created;}
private Date created;

@Temporal(TemporalType.TIMESTAMP)
@Column(name="updated", nullable = false)
public Date getUpdated() {return updated;}
public void setUpdated(Date updated) {this.updated = updated;}
private Date updated;

@PrePersist
protected void onCreate() {
setCreated(new Date());
setUpdated(new Date());}

@PreUpdate
protected void onUpdate() {
setUpdated(new Date());}

I soon realized an important drawback to this approach.  It simply doesn't work when using Hibernate's Session API.  This was a big problem for me since as I've discussed in detail in previous blog posts I am in fact using org.hibernate.Session.  :( As described in this Stack Overflow post, it is necessary to use the EntityManager API when using the @Prepersist and @PreUpdate annotations.

I considered converting from the Session API to the EntityManager API but decided against it for the time being.  That's when I looked more closely at Hibernate's interceptor and events architecture.  I also found a very helpful blog post by a blogger name neildo that explained how to use an event listener to manage last modified date.

Using neildo's blog post and the documentation as a starting point, I developed the following code that I am using today.  My code differs from the neildo's example in that I'm setting both the created and the modified dates in the custom event listener.

First there's the interface that each entity POJO must implement.  As you can see the interface includes "id" which I'll explain later.
public interface AuditableDate {

    public void setId(int id);
    public int getId();
    
    public void setCreated(Date date);
    public Date getCreated();
    
    public void setUpdated(Date date);
    public Date getUpdated();
}

Here's an example of an entity POJO that implements AuditableDate.  For this example, I've stripped out the rest of the entity properties and left only those needed to implement AuditableDate.  Obviously the real entity would have other properties as well.
public class Tag implements AuditableDate, Serializable {

    @Id
    @GeneratedValue
    @Column(name="TagId")
    @Override
    public int getId() {return this.id;}
    public void setId(int id) {this.id = id;}
    private int id;

    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="CreatedDate",
            nullable=false, 
        updatable = false)
    public Date getCreated() {return created;}
    public void setCreated(Date date) {this.created = date;}
    private Date created;
    
    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="UpdatedDate", nullable = false)
    public Date getUpdated() {return updated;}
    public void setUpdated(Date date) {this.updated = date;}
    private Date updated;

    public Tag() {
    }
}

The event listener is as follows.  You can see where I'm checking the Id property to determine if the we are working with a new record and if so I'm setting the both the Created Date and the Updated Date.
public class SaveOrUpdateDateListener extends DefaultSaveOrUpdateEventListener {
    
    static Logger logger = Logger.getLogger(SaveOrUpdateDateListener.class);

    @Override
    public void onSaveOrUpdate(SaveOrUpdateEvent event) {
        logger.debug("Entering onSaveOrUpdate()");
        
        if (event.getObject() instanceof AuditableDate) {
            AuditableDate record = (AuditableDate) event.getObject();
            
            // set the Updated date/time
            record.setUpdated(new Date());

            // set the Created date/time
            if (record.getId() == 0) {
                record.setCreated(new Date());
            }
        }
}

The last step is to add a reference to the Hibernate configuration so that it knows to fire the custom listener.
<event type="save-update">
    <listener class="org.robbins.persistence.SaveOrUpdateDateListener"/>
    <listener class="org.hibernate.event.def.DefaultSaveOrUpdateEventListener"/>
</event>

You'll notice that I've included BOTH the custom listener and the default listener.  I'm not sure why that was necessary but I was unable to get the default listener to fire in the custom listener by using:
super.onSaveOrUpdate(event);

I hope you found this post useful.  I'd appreciate any questions or suggestions you may have.  Thanks!

Thursday, August 4, 2011

Integrating OpenId into my Struts 2 app - Part 4 - The OpenIdAuthenticator bean

In the final post in this series, we'll concentrate on the OpenIdAuthenticator which does the heavy lifting, with help from the OpenId4Java library, in our interaction with the user's selected OpenID Provider.

According to the OpenId4Java website, the OpenId4Java library "allows you to OpenID-enable your Java webapp."  Many thanks to the folks who put together this solid library.  I suggest checking out the sample code.  There's a number of example that will help you get started.

One of the tricky things I encountered was managing to get the OpenId provider to provide information about the logged in user.  Things like the user's first and last name, their email address, country, and language.  I was disappointed to discover the OpenID Providers (Google, Yahoo, AOL, etc) are not consistent in the level of detail they provide about the user.  Not only that but the format of the attributes are different as well.  This difference leads to the if/else section of the getValidateOpenIdUrl() method.

public class OpenIdAuthenticator {
    
    static Logger logger = Logger.getLogger(OpenIdAuthenticator.class);

    private static ConsumerManager getConsumerManager(Map<String, Object> application) {
        logger.debug("Entering getConsumerManager()");
        
        ConsumerManager manager;

        // try to get the ConsumerManager from the Application scope
        manager = (ConsumerManager)application.get("consumermanager");
        
        if (manager == null) {
            // create a new ConsumerManager
            try {
                manager = new ConsumerManager();
                manager.setAssociations(new InMemoryConsumerAssociationStore());
                manager.setNonceVerifier(new InMemoryNonceVerifier(5000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        
            // add the Consumer Manager to the application scope
            application.put("consumermanager", manager);
        }

        return manager;
    }
    
    @SuppressWarnings("unchecked")
    public static String getValidateOpenIdUrl(String returnUrl, String openIdIdentifier, Map<String, Object> httpSession, Map<String, Object> application) throws DiscoveryException,
            MessageException, ConsumerException {
        
        logger.debug("Entering getOpenIdDestinationUrl()");

        // get a reference to the Consumer Manager
        ConsumerManager manager = getConsumerManager(application);
        
        // perform discovery on the user-supplied identifier
        List<DiscoveryInformation> discoveries = manager.discover(openIdIdentifier);

        // attempt to associate with the OpenID provider
        // and retrieve one service endpoint for authentication
        DiscoveryInformation discovered = manager.associate(discoveries);

        // store the discovery information in the user's session for later use
        // leave out for stateless operation / if there is no session
        httpSession.put("discovered", discovered);

        // obtain a AuthRequest message to be sent to the OpenID provider
        AuthRequest authReq = manager.authenticate(discovered, returnUrl);

        // Attribute Exchange
        FetchRequest fetch = FetchRequest.createFetchRequest();

        // different Open Id providers accept different attributes
        if (openIdIdentifier.contains("google.com")) {
            logger.debug("Open Id Identifier is: google.com");
            
            fetch.addAttribute("first", "http://axschema.org/namePerson/first", true);
            fetch.addAttribute("last", "http://axschema.org/namePerson/last", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else if (openIdIdentifier.contains("yahoo.com")) {
            logger.debug("Open Id Identifier is: yahoo.com");
            
            fetch.addAttribute("fullname", "http://axschema.org/namePerson", true);
            fetch.addAttribute("nickname", "http://axschema.org/namePerson/friendly", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else if (openIdIdentifier.contains("aol.com")) {
            logger.debug("Open Id Identifier is: aol.com");

            fetch.addAttribute("first", "http://axschema.org/namePerson/first", true);
            fetch.addAttribute("last", "http://axschema.org/namePerson/last", true);
            fetch.addAttribute("email", "http://axschema.org/contact/email", true);
            fetch.addAttribute("language", "http://axschema.org/pref/language", true);
        }
        else {
            logger.debug("Open Id Identifier is: something else");
            
            fetch.addAttribute("fullname", "http://schema.openid.net/namePerson", true); 
            fetch.addAttribute("email", "http://schema.openid.net/contact/email", true); 
            fetch.addAttribute("country", "http://axschema.org/contact/country/home", true);    
        }

        // attach the extension to the authentication request
        authReq.addExtension(fetch);

        logger.info("The request string is: " + authReq.getDestinationUrl(true).replaceAll("&", "\n"));

        return authReq.getDestinationUrl(true);
    }
    
    public static User getAuthenticatedUser(Map<String,String[]> parmList,
            final StringBuffer receivingURL, Map<String, Object> httpSession, Map<String, Object> application)
            throws MessageException, DiscoveryException, AssociationException {

        logger.debug("Entering getAuthenticatedUser()");

        // extract the parameters from the authentication response
        // (which comes in as a HTTP request from the OpenID provider)
        ParameterList openidResp = new ParameterList(parmList);
        
        // retrieve the previously stored discovery information
        final DiscoveryInformation discovered = (DiscoveryInformation) httpSession.get("discovered");

        // get a reference to the Consumer Manager
        ConsumerManager manager = getConsumerManager(application);
        
        // verify the response
        final VerificationResult verification = manager.verify(receivingURL.toString(), openidResp, discovered);
        
        // examine the verification result and extract the verified identifier
        Identifier verified = verification.getVerifiedId();
        if (verified == null) {
            return null;
        }

        AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();

        User user = new User();
        user.setOpenid(authSuccess.getIdentity());
        
        if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
            logger.info("Processed as OPENID_NS_AX");
            
            FetchResponse fetchResp = (FetchResponse) authSuccess.getExtension(AxMessage.OPENID_NS_AX);

            // populate the User object with attributes from the FetchResponse
            user.setNickname(fetchResp.getAttributeValue("nickname"));
            user.setEmail(fetchResp.getAttributeValue("email"));
            user.setFullName(fetchResp.getAttributeValue("fullname"));
            user.setFirstName(fetchResp.getAttributeValue("first"));
            user.setLastName(fetchResp.getAttributeValue("last"));
            user.setLanguage(fetchResp.getAttributeValue("language"));
            user.setCountry(fetchResp.getAttributeValue("country"));

            logger.info("User: " + user.toString());
        }

        if (authSuccess.hasExtension(SRegMessage.OPENID_NS_SREG)) {
            logger.info("Processed as OPENID_NS_SREG");
            
            SRegResponse sregResp = (SRegResponse) authSuccess.getExtension(SRegMessage.OPENID_NS_SREG);

            // if we didn't get the user's email addy from the FetchResponse, try to get it from the SRegResponse 
            if (StringUtils.isBlank(user.getEmail())) {
                user.setEmail(sregResp.getAttributeValue("email"));
            }
        }
        return user;
    }
}

Integrating OpenId into my Struts 2 app - Part 3 - The LoginAction Action class

The LoginAction class controls the flow between the user's authentication request and the business bean that does our heavy lifting.  Aside from a Logout method that removes the user's http session, the two main methods in this class are as follows:
  • validateOpenId() - This method is called after the user selects an OpenID provider from the OpenID Selector component on the Login form.  The Action then calls the business bean (OpenIdAuthenticator) which generates the destination URL for OpenId Provider.  The request is then redirected to the destination URL.  It's important to note that we are now redirecting the user away from our web application.  Our application never sees or has access to the user's password.  The OpenID Provider will let us know if the use has successfully authenticated.
  •  validateOpenId() - Once the user has successfully authenticated with the OpenID Provider.  The Provider redirects the user back to our web application, using the return URL we provided.  This method processes that request.  Again we pass it off to the business bean and let it confirm the login was successful and glean any relevant information sent by the OpenID Provider.  The business bean will return a User object which we'll persist in the database and add to the user's HTTP Session.  At this point we'll redirect the user to the resource he was originally requesting before we forced him to authenticate.
Here's the Action class:
public class LoginAction extends FlashCardsAppBaseAction implements Preparable, ServletRequestAware, ServletResponseAware, SessionAware, ApplicationAware {

    static Logger logger = Logger.getLogger(LoginAction.class);

    // this is the only form field we will be looking for from OpenID Selector on the front end
    private String openid_identifier;

    // Hibernate Session
    private Session hibernateSession;
    
    // we'll need access to the Servlet spec objects, rather than just their attribute or parm maps
    private HttpServletRequest request;
    private HttpServletResponse response;
    
    // we'll be storing the User object in the Session
    private Map<String, Object> httpSession;
    
    // the OpenIdAuthenticator class needs access to the application to store a OpenId4Java related object
    private Map<String, Object> application;
    
    // we'll need to send this to the OpenId provider so it knows where to send its response 
    private final String returnAction = "/home/authenticateOpenId.action";

    // the OpenID Selector form will submit to this Action method
    public String validateOpenId() throws Exception {

        logger.debug("Entering validateOpenId()");
        
        // get rid of trailing slash
        if (getOpenid_identifier().endsWith("/")) {
            setOpenid_identifier(getOpenid_identifier().substring(0, getOpenid_identifier().length() - 1));
        }

        logger.debug("The requested OpenId identifier is: " + getOpenid_identifier());

        // determine a return_to URL where the application will receive
        // the authentication responses from the OpenID provider
        String returnUrl = getServerContext(request) + returnAction;
        
        // construct the destination Url to send to the Open Id provider
        String destinationUrl = OpenIdAuthenticator.getValidateOpenIdUrl(returnUrl, this.getOpenid_identifier(), httpSession, application); 
        
        // redirect to the Auth Request
        response.sendRedirect(destinationUrl);
        
        // no need to return a view
        return NONE;
    }
    
    public String authenticateOpenId() throws Exception {
        logger.debug("Entering authenticateOpenId()");

        Map<String,String[]> parmList = request.getParameterMap();

        // extract the receiving URL from the HTTP request
        final StringBuffer receivingURL = request.getRequestURL();
        final String queryString = request.getQueryString();

        if (queryString != null && queryString.length() > 0) {
            receivingURL.append("?").append(request.getQueryString());
        }
        
        logger.debug(receivingURL.toString().replaceAll("&", "\n"));

        // verify the user has authenticated with the Open Id provider and 
        // get a reference to the authenticated User
        User user = OpenIdAuthenticator.getAuthenticatedUser(parmList, receivingURL, httpSession, application);

        // save the user to the DB
        UserPersister uPersister = new UserPersister();
        uPersister.saveOrUpdateUser(user, hibernateSession);
        
        // add the user to the HTTP Session
        httpSession.put("user", user);
        
        // retrieve the original URL from the Session
        String desitinationURL = (String)httpSession.get("originalURL");

        // was a destination URL provided?
        if (desitinationURL == null) {
            logger.debug("No destination URL provided, will send to Home");
            return "home";
        }
        else {
            logger.debug("Redirecting to : " + desitinationURL);
            response.sendRedirect(desitinationURL);
            return NONE;
        }
    }

    @SuppressWarnings("rawtypes")
    public String logout() {
        logger.debug("Entering logout()");
        
        try {
            // invalidate the user's session
            httpSession.remove("user");

            if (httpSession instanceof org.apache.struts2.dispatcher.SessionMap) {
                try {
                    ((org.apache.struts2.dispatcher.SessionMap) httpSession).invalidate();
                } catch (IllegalStateException e) {
                    logger.error("Exception in logout()", e);
                }
            }
            
            return "success";
        } catch (Exception e) {
            logger.error("Exception in logout():", e);
            
            return "error";
        }
    }

    private String getServerContext(final HttpServletRequest request) {
        // Get the base url.
        final StringBuilder serverPath = new StringBuilder();
        
        serverPath.append(request.getScheme() + "://");
        serverPath.append(request.getServerName());

        if (request.getServerPort() != 80) {
            serverPath.append(":" + request.getServerPort());
        }
        serverPath.append(request.getContextPath());
        
        return serverPath.toString();
    }
    
    @Override
    public void prepare() throws Exception {
        logger.debug("Entering prepare()");

        hibernateSession = getHibernateSession();
    }

    public String getOpenid_identifier() {
        return openid_identifier;
    }

    public void setOpenid_identifier(String openid_identifier) {
        this.openid_identifier = openid_identifier;
    }

    @Override
    public void setSession(Map<String, Object> httpSession) {
        this.httpSession = httpSession;
    }
    
    @Override
    public void setServletResponse(final HttpServletResponse response) {
        this.response = response;
    }

    @Override
    public void setServletRequest(final HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public void setApplication(Map<String, Object> application) {
        this.application = application;
    }
}

In the final post in this series, I'll include the code for the OpenIdAuthenticator business bean which does the heavy lifting and relies heavily on the OpenId4Java library.