Introduction
This is the third post in a four part series about a wine rating and recommendation Web application built using open source Java technology. The purpose of this series is to document key design and implementation decisions that can be applied to other Web applications. Please read the first and second posts to get up-to-speed. You can download the project (with source) from here.
In this posting, I implement several common features that should help users find and contribute to your application. Specifically, I integrate with Facebook Connect (based on OAuth 2) to allow Facebook users to instantly register and authenticate using their Facebook profile. From there, I integrate the Facebook Like social plug-in which allows users to share content in your application with their friends on Facebook.
Authentication using Facebook Connect
The Facebook team has made integrating Facebook Connect into your application very easy. There are open source Java libraries available for integrating with Facebook, however I found it easier to just use the JavaScript SDK. Let's go through the process in five simple steps:I. Register Your Application with Facebook
First, you need to register an application in Facebook to get a unique Application ID (please use something other than "VinWiki" for your application name since I'll be using that one in the near future). Update resources/WEB-INF/components.xml to set the facebookAppId property on the app component: <component name="app" auto-create="true" scope="application" class="org.vinwiki.App">
...
<property name="facebookAppId">ENTER_YOUR_FB_APPLICATION_ID_HERE</property>
</component>
II. Initialize the Facebook JavaScript Library
Second, you need to initialize the Facebook JavaScript library when your page loads. For this, I created a new Facelets include file view/WEB-INF/facelets/facebook.xhtml and loaded it into the footer in my layout template view/layout/template.xhtml: <h:panelGroup rendered="#{app.isFacebookEnabled()}">
<ui:include src="/WEB-INF/facelets/facebook.xhtml"/>
</h:panelGroup>
In facebook.xhtml, I have the following JavaScript: (function() {
var e = document.createElement('script'); e.async = true;
e.src = document.location.protocol + '//connect.facebook.net/en_US/all.js';
document.getElementById('fb-root').appendChild(e);
}());
This function, borrowed from the Facebook developer documentation, asynchronously loads the Facebook JavaScript file into your page. III. Register a JavaScript callback handler for Facebook session events
Once the JavaScript library is loaded, the window.fbAsyncInit function is called automatically. window.fbAsyncInit = function() {
FB.init({appId:'#{app.facebookAppId}', status:true, cookie:true, xfbml:true});
FB.Event.subscribe('auth.sessionChange', function(response) {
if (response.session) {
// Login successful
var uid = response.session.uid;
FB.api('/me', function(resp) {
onFbLogin(uid, resp.email, resp.name, resp.first_name, resp.last_name, resp.gender);
});
} else {
// The user has logged out, and the cookie has been cleared
onFbLogout();
}
});
};
After initializing the library (see FB.init), the application registers a listener for auth.sessionChange events (login or logout). On login, I use Facebook's Graph API to get some basic information about the current user (see FB.api). In the response callback handler for FB.api('/me'), I invoke a JavaScript function onFbLogin that executes the #{guest.onFbLogin()} action: <a4j:form prependId="false">
<s:token/>
<a4j:jsFunction immediate="true" name="onFbLogin" ajaxSingle="true" action="#{guest.onFbLogin()}">
... params here ...
</a4j:jsFunction>
</a4j:form>
Notice that I'm using Seam's <s:token/> tag to prevent cross-site request forgery since the #{guest.onFbLogin()} action simply trusts that the user was authenticated by Facebook on the client-side. For an overview of the thinking behind the <s:token/> tag, I refer you to Dan Allen's post at http://seamframework.org/Community/NewComponentTagStokenAimedToGuardAgainstCSRF. However, please realize that you must set javax.faces.STATE_SAVING_METHOD to "server" in web.xml for this method to secure your forms: <context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
The action handler on the server side is straight-forward because Seam supports alternative authentication mechanisms out-of-the-box. Specifically, all you need to do is invoke the acceptExternallyAuthenticatedPrincipal method of the org.jboss.seam.security.Identity object. I utilized the existing guest component because it handles other guest related actions such as register and login (see src/hot/org/vinwiki/user/GuestSupport.java) Identity.instance().acceptExternallyAuthenticatedPrincipal(new FacebookPrincipal(user.getUserName()));
Contexts.getSessionContext().set("currentUser", user);
Events.instance().raiseEvent(Identity.EVENT_POST_AUTHENTICATE, Identity.instance());
I also raise the Identity.EVENT_POST_AUTHENTICATE event manually so that my nav component can re-configure the default for the authenticated user instead of showing the guest view.IV. Show Facebook Connect Button on Login Panel
Lastly, we need to let users know that they login (or register) using their Facebook credentials. This is accomplished with the <fb:login-button> tag, see view/WEB-INF/facelets/guestSupport.xhtml. <fb:login-button perms="email,publish_stream">
<fb:intl>Login with Facebook</fb:intl>
</fb:login-button>
The new RichFaces Login dialog looks like:
Notice that VinWiki requests access to the email and publish_stream extended permissions. The publish_stream permission allows users to share wines of interest found on VinWiki with their friends on Facebook. When accessing VinWiki for the first time, users will see a dialog that allows them to grant permissions to the application:
V. Logout
It's doubtful whether many users will ever explicitly logout of your site unless they are accessing it from a public computer. Consequently, you'll want to keep your session timeout value as low as possible. That said, you still need to offer the ability to logout. In view/WEB-INF/facelets/headerControls.xhtml, the logout action is implemented using a simple JSF commandLink: <h:commandLink rendered="#{nav.isFbSession()}" onclick="FB.logout();" styleClass="hdrLink">
<h:outputText value="Logout"/>
</h:commandLink>
The magic is in our sessionChange event listener discussed above. The Facebook JavaScript function FB.logout() triggers an auth.sessionChange event, which in turn calls onFbLogout() to execute the #{guest.onFbLogout()} action on the server.So that covers authentication using Facebook Connect. There are many other ways to integrate your application with Facebook. In the next section, I'll implement a way for users to share content in your application with their friends on Facebook. For this feature, it is helpful to have bookmarkable URLs.
Sharing Content with Friends
In VinWiki, users may want to share specific wines of interest with their friends on Facebook. For this, I used Facebook's Like social plug-in. There are other options, including just posting a shared link to the user's activity stream. There's not much to integrating the Like social plug-in into your page once you've accounted for bookmarkable URLs (see posting 1). The <fb:like> tag will use the URL of the current page if you don't specify an href attribute. However, I want to make sure the URL that is shared with Facebook is as clean as possible. Thus, I introduced a new setting for the org.vinwiki.App component named baseUrl. You should change this to match your server in resources/WEB-INF/components.xml: <component name="app" auto-create="true" scope="application" class="org.vinwiki.App">
...
<property name="baseUrl">http://192.168.1.2:8080/vinwiki/</property>
</component>
I also decided to add the Open Graph meta tags to the header of my wine details page, view/wine.xhtml: <meta property="og:title" content="#{currentWine.fullName}"/>
<meta property="og:type" content="drink"/>
<meta property="og:url" content="#{viewWine.openGraphUrl}"/>
<meta property="og:site_name" content="VinWiki"/>
<meta property="og:description" content="#{jsf.truncate(currentWine.description,100,'...')}"/>
Presumably, when you share a page that supports the Open Group protocol, Facebook is able to present this additional metadata to your friends. Of course, the URL needs to be public before this will actually work ;-)What's Next?
In the next and final post in this series, I'll integrate Mahout for making wine recommendations and discuss some considerations for scaling the application.