Friday, March 16, 2012

How to Create a Facebook App using Java EE 6 and Easily Deploy to RedHat OpenShift Cloud [Tutorial]

UPDATE: Please see the updated Facebook App Tutorial with OAuth Support.

________________________________________________________

How to Develop a Facebook App using:
  • Java EE 6 standards including CDI (JSR-299 Dependency Injection) and JSF 2.0
  • Twitter's excellent CSS framework Bootstrap
  • Easily test and rapidly iterate your app in your local development computer with JBoss AS 7.1.1
  • Deploy to the cloud in seconds to RedHat OpenShift Express, for free!
What Will You Get?
This app's interface and purpose is actually simple :
  • Put your Facebook access token in it. You can use the Facebook Graph Explorer and your own app to generate an access token. You need to allow the publish_stream privilege.
  • Put a message, click Post and the app will update your Facebook status.
You check out the live app at fbstatus-soluvas.rhcloud.com.
Preparation
Before you start, you'll need to get some basics ready :
  1. Get the latest JBoss Developer Studio 5.0.0.Beta1.
    Simply the quickest way to get started. This contains Eclipse IDE, JBoss Tools, JBoss AS, JBoss Forge, OpenShift plug-in and several other handy tools at your command.
    If you want, you can install these separately:
    - Eclipse IDE, Java EE Developer edition (3.7.2 or later)
    - JBoss Tools (get 3.3.0.Beta1 or later)
    - JBoss AS 7.1.1 or later
    - JBoss Forge 1.0.Final or later
  2. While downloading the things above, signup for RedHat OpenShift Express.
    Set up your SSH keys so you're good to go. You can try creating a test app yourself, but  we'll create another app for this tutorial. (OpenShift allows multiple applications in one account).
  3. You'll also need to have a Facebook app. Go to Facebook Developers and create an app. Any test app will do. We'll need to generate an access token for your Facebook profile (later on).
A Cloud-Ready Facebook App in Java EE in 5 Minutes*
  1. Create a new jbossas-7 OpenShift application.
    In JBoss Developer Studio / Eclipse IDE, create a new OpenShift application :
    File > New > Other... > choose OpenShift Application.
    Name it anything you want, in my case it's fbstatus.
    Pick the jbossas-7 cartridge.
    Other fancy options are not needed for this purpose.
  2. Cleanup the OpenShift scaffold project.
    Delete everything in src/main/webapp except WEB-INF.
    Also delete src/main/webapp/WEB-INF/faces-config.xml first (JBoss Tools may automatically create it for you).
    We delete this so Forge's faces setup (below) runs well.
  3. Add SLF4J Logging dependency.
    JDK Logging is okay, but SLF4J is much nicer. We'll configure SLF4J to log using JDK Logger, which is handled properly by JBoss AS.
    Using Forge Console, run:project add-dependency org.slf4j:slf4j-api:1.6.4
    project add-dependency org.slf4j:slf4j-jdk14:1.6.4:runtime
  4. Setup JSF and CDI.
    Using Forge Console, run:faces setup
    When asked for CDI, enter "Y".
    For Faces servlet and mapping, enter "N" because it's not required for Servlet 3.0 and beyond.
  5. Make sure Maven project configuration is up-to-date.
    Right click project > Maven > Update Project
  6. Get Icons
    Some people think is an "extraneous" step, but I think initial appearance is very important as it sets your mood. ;-)
    Get a "normal" size icon (~64px) e.g. from IconFinder, and save it as src/main/webapp/resources/fbstatus/fbstatus_64p.png
    Grab or resize the icon to 16x16 (for navbar, buttons, etc.), save it in src/main/webapp/resources/fbstatus/fbstatus_16p.png
    Also save the 16x16 version as an ICO file at src/main/webapp/favicon.ico
  7. Grab Twitter's Bootstrap HTML5 Framework
    Like the icons above, this is not really "necessary" for the functionality of the apps, but it seems the mood since initially the app will look "acceptable" instead of "plain HTML".
    Download Bootstrap dist and extract it as src/main/webapp/resources/bootstrap
    Also get the jQuery plugins from Bootstrap sources, put the bootstrap-*.js plugins from js/ folder into src/main/webapp/resources/bootstrap/js
    You also need jQuery library litself and put this file as src/main/webapp/resources/jquery.js
  8. Create a JSF template in src/main/webapp/templates/basic.xhtml :<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:f="http://java.sun.com/jsf/core">
    <f:view>
    <h:head>
        <meta charset="utf-8" />
        <title><ui:insert name="pageTitle">Post status to Facebook</ui:insert> | FBStatus</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="description" content="Post status to Facebook." />
        <meta name="author" content="FBStatus" />
        <!-- Le styles -->
        <!-- Workaround for JSF resource URLs and css URLs:
        if URL appended with ?ln=bootstrap, glyph URL is wrong:
        http://localhost:9080/fbstatus/faces/javax.faces.resource/img/glyphicons-half...
        should be:
        http://localhost:9080/fbstatus/faces/javax.faces.resource/bootstrap/img/glyph... -->
        <h:outputStylesheet name="bootstrap/css/bootstrap.css"/>
        <style type="text/css">
    body {
        padding-top: 60px;
        padding-bottom: 40px;
    }
    .sidebar-nav {
        padding: 9px 0;
    }
    </style>
        <h:outputStylesheet name="bootstrap/css/bootstrap-responsive.css"/>
        <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
        <!--[if lt IE 9]>
          <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
        <!-- Le fav and touch icons -->
        <link rel="shortcut icon" href="#{request.contextPath}/favicon.ico" />
        <!--     <link rel="apple-touch-icon-precomposed" sizes="114x114" href="#{request.contextPath}/resources/bootstrap/ico/apple-touch-icon-114-precomposed.png">
        <link rel="apple-touch-icon-precomposed" sizes="72x72" href="#{request.contextPath}/resources/bootstrap/ico/apple-touch-icon-72-precomposed.png">
        <link rel="apple-touch-icon-precomposed" href="#{request.contextPath}/resources/bootstrap/ico/apple-touch-icon-57-precomposed.png"> -->
    </h:head>
    <h:body>
        <div class="navbar navbar-fixed-top">
            <div class="navbar-inner">
                <div class="container">
                    <a class="btn btn-navbar" data-toggle="collapse"
                        data-target=".nav-collapse"> <span class="icon-bar"></span> <span
                        class="icon-bar"></span> <span class="icon-bar"></span>
                    </a> FBStatus
                    <div class="nav-collapse">
                        <ul class="nav">
                            <li>Home</li>
                            <li>Post Status</li>
                            <li>About</li>
                            <li>Contact</li>
                        </ul>
                        <p class="navbar-text pull-right">
                            Login
                        </p>
                    </div>
                    <!--/.nav-collapse -->
                </div>
            </div>
        </div>
        <div class="container">
                    <div class="row">
                <div class="span12">
                    <h:messages/>
                </div>
            </div>
                <ui:insert name="content"/>
            <hr />
            <footer>
            <p>&copy; Hendy Irawan 2012</p>
            </footer>
        </div>
        <!--/.container-->
        <!-- Le javascript
        ================================================== -->
        <!-- Placed at the end of the document so the pages load faster -->
        <h:outputScript name="jquery.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-transition.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-alert.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-modal.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-dropdown.js"/>
            <h:outputScript library="bootstrap" name="js/bootstrap-scrollspy.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-tab.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-tooltip.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-popover.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-button.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-collapse.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-carousel.js"/>
        <h:outputScript library="bootstrap" name="js/bootstrap-typeahead.js"/>
    </h:body>
    </f:view>
    </html>
  9. Create a JSF page named src/main/webapp/front.xhtml :<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:ui="http://java.sun.com/jsf/facelets"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>FBStatus</title>
    </h:head>
    <h:body>
        <p>Hello</p>
    </h:body>
    </html>
    Launch the web app using JBoss AS server, you should be able to access the front page using http://localhost:8080/fbstatus/faces/front.xhtml
    You can also deploy to OpenShift by Git committing the entire project and Git push it to OpenShift. It should be accessible at http://yourapp-yourdomain.rhcloud.com/faces/front.xhtml
  10. Add PrettyFaces
    For more SEO-friendly URLs PrettyFaces is a must. Use Forge Console to run:project add-dependency com.ocpsoft:prettyfaces-jsf2:3.3.3
    Note: For automatic PrettyFaces configuration, Servlet version in web.xml must be at least 3.0.
  11. Enable PrettyFaces development mode
    This can be turned off in production setting but for now, addd in web.xml as follows :<context-param>
    <param-name>com.ocpsoft.pretty.DEVELOPMENT</param-name>
    <param-value>true</param-value>
    </context-param>
    This will allow PrettyFaces mapping to be changed anytime without reloading the webapp.
  12. Create PrettyFaces mapping
    Create src/main/webapp/WEB-INF/pretty-config.xml :<pretty-config xmlns="http://ocpsoft.com/prettyfaces/3.3.3"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://ocpsoft.com/prettyfaces/3.3.3
                            http://ocpsoft.com/xml/ns/prettyfaces/ocpsoft-pretty-faces-3.3.3.xsd">
        <url-mapping id="home">
            <pattern value="/" />
            <view-id value="/faces/front.xhtml" />
        </url-mapping>
    </pretty-config>
    The above maps the context root path to /faces/front.xhtml.
    Deploy the webapp in JBoss AS or OpenShift and you should be able to access the front page at webapp root, e.g. in JBoss would be: http://localhost:8080/fbstatus/
    Note that these steps that we've done above are all simply boilerplate.
    You can save the project at this point and put it somewhere or on Git to get started with a new OpenShift-JSF-Bootstrap project rapidly.
  13. Add RestFB
    Now let's get to business, I will use RestFB Facebook API library for Java. Using JBoss Forge Console :project add-dependency com.restfb:restfb:1.6.9
    There are other alternative libraries for accessing Facebook:
    - Spring Social. It's easy to use and works well. However it drags quite some dependencies (especially the whole Spring Framework), which makes sense, but RestFB serves the purpose of this tutorial quite well.
    - Seam Social. A CDI oriented Facebook client library.
    - facebook-java-api. Actually I tried it before RestFB, but it throws an error regarding invalid session key. I'm using access tokens not session IDs so either it's using an outdated Facebook API version or I didn't read the manual correctly.
    At this point I should have added Seam Faces and Seam International too, to make working with JSF and FacesMessages more convenient. Too bad, Seam artifacts aren't available yet in OpenShift Maven repository. So, plain JSF API for now.
    But in case you're curious how to add Seam Faces and Seam International, here's the needed Maven POM dependencies :
      <properties>
        <seam.version>3.1.0.Final</seam.version>
      </properties>
       <dependencyManagement>
          <dependencies>
             <dependency>
                <groupId>org.jboss.seam</groupId>
                <artifactId>seam-bom</artifactId>
                <version>${seam.version}</version>
                <type>pom</type>
                <scope>import</scope>
             </dependency>
          </dependencies>
       </dependencyManagement>
    ...
            <dependency>
                <groupId>org.jboss.seam.international</groupId>
                <artifactId>seam-international</artifactId>
            </dependency>
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>1.6.2</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.seam.faces</groupId>
                <artifactId>seam-faces</artifactId>
            </dependency>
  14. Create the bean src/main/java/org/soluvas/fbstatus/PostView.java :Most of the things below, like always, is Java verbose cruft. (How I wish Scala would prevail over Java the language.) Anyway, here's the code:
    package org.soluvas.fbstatus;
    import java.io.Serializable;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.enterprise.context.SessionScoped;
    import javax.faces.application.FacesMessage;
    import javax.faces.application.FacesMessage.Severity;
    import javax.faces.context.FacesContext;
    import javax.inject.Named;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import com.restfb.DefaultFacebookClient;
    import com.restfb.Parameter;
    import com.restfb.types.FacebookType;
    @SessionScoped @Named
    public class PostView implements Serializable {
        static final long serialVersionUID = 1L;
        private transient Logger log = LoggerFactory.getLogger(PostView.class);
    //    @Inject Messages messages;
        String accessToken;
        String message;
        String lastStatusId;
        String lastStatusUrl;
            public void manualPost() {
            FacesContext faces = FacesContext.getCurrentInstance();
            DefaultFacebookClient client = new DefaultFacebookClient(accessToken);
            FacebookType response = client.publish("me/feed", FacebookType.class, Parameter.with("message", message));
            lastStatusId = response.getId();
            log.info("Got Post ID: {}", lastStatusId);
            Matcher matcher = Pattern.compile("(\\d+)_(\\d+)").matcher(lastStatusId);
            if (matcher.matches()) {
                String userId = matcher.group(1);
                String postId = matcher.group(2);
                lastStatusUrl = "http://www.facebook.com/" + userId + "/posts/" + postId;
                log.info("Post URL is {}", lastStatusUrl);
                faces.addMessage(null, new FacesMessage(
                        "Status posted to Facebook with ID: " + lastStatusId, ""));
    //            messages.info("Status posted to Facebook with ID: {0}", lastStatusId);
                message = "";
            } else {
                log.error("Cannot parse Post ID: {}", lastStatusId);
                faces.addMessage(null, new FacesMessage(faces.getMaximumSeverity(),
                        "Cannot parse Post ID: " + lastStatusId, ""));
            }
        }
            public String getAccessToken() {
            return accessToken;
        }
        public void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }
        public String getMessage() {
            return message;
        }
        public void setMessage(String message) {
            this.message = message;
        }
        public String getLastStatusUrl() {
            return lastStatusUrl;
        }
        public void setLastStatusUrl(String lastStatusUrl) {
            this.lastStatusUrl = lastStatusUrl;
        }
        public String getLastStatusId() {
            return lastStatusId;
        }
        public void setLastStatusId(String lastStatusId) {
            this.lastStatusId = lastStatusId;
        }
        }
    This is the core meat of this tutorial. As you can see the real code is not very complicated. :-) In Java EE 6, you use managed beans and annotate it with @Named to make it accessible from JSF page (below). By annotating with @SessionScoped the bean's property values are preserved within the same browser session. Different visitors or different browsers will have its own instance of the session-scoped bean.
    By setting accessToken and message properties then calling manualPost(), you should be able to perform some unit testing on the above code if you want.
  15. Create JSF page for Post Status form
    This page will display the form and call the postView.manualPost() method above.
    Put in src/main/webapp/post.xhtml :<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:ui="http://java.sun.com/jsf/facelets"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:h="http://java.sun.com/jsf/html">
    <ui:composition template="/templates/basic.xhtml">
    <ui:define name="pageTitle">Post status to Facebook via access token</ui:define>
    <ui:define name="content">
        <div class="row">
                <div class="span12">
                        <div class="row">
                                <h:form>
                    <div class="span12">
                        <h2>Post Status Manually</h2>
                        <p>Access token: <h:inputText value="#{postView.accessToken}"/></p>
                        <p>Message: <h:inputText value="#{postView.message}"/></p>
                        <p><h:commandLink action="#{postView.manualPost}" value="Post" styleClass="btn"/></p>
                        <h:panelGroup rendered="#{not empty postView.lastStatusId}">
                            <p>Last status ID: #{postView.lastStatusId}</p>
                        </h:panelGroup>
                    </div>
                    </h:form>
                   
                </div>
                <!--/row-->
            </div>
            <!--/span-->
        </div>
        <!--/row-->
    </ui:define>
    </ui:composition>
    </html>
    The JSF page above simply presents text boxes to input the Facebook access token and the desired message, and a button to call the PostView.manualPost() method.
    Also displayed is the link to the last status so you can see it in Facebook.
And that's it!
Steps 1-12 are actually pure boilerplate for most JSF apps. Only steps 13, 14, 15 which is unique to this web application. Which highlights the necessity to create some form of project template or prototype where you can easily construct the basic JSF webapp necessities quickly.
How to Use the App
This app's interface and purpose is actually simple. Deploy the webapp to JBoss AS or to OpenShift (using git push) and :
  • Put your Facebook access token in it. You can use the Facebook Graph Explorer and your own app to generate an access token. You need to allow the publish_stream privilege.
  • Put a message, click Post and the app will update your Facebook status.
You check out the live app at fbstatus-soluvas.rhcloud.com.
This is an example of the status generated by the app :
You can check it out here: http://www.facebook.com/ceefour/posts/10150732341781672 . Oh and by the way, you're welcome to add me on Facebook :-)


To learn more about Java Web Development using Java EE 6, I highly recommend The Java EE 6 Tutorial: Basic Concepts (4th Edition) (Java Series) by Eric Jendrock, Ian Evans, Devika Gollapudi and Kim Haase.

* (to be honest, no, I didn't write this article in 5 minutes. but you could ;-)

No comments:

Post a Comment