Monday, March 19, 2012

How to Develop an OpenShift Cloud App with Facebook OAuth Support [Tutorial]

How to Develop a Facebook App with OAuth 2.0 authorization to publish activities to the social network. Technologies:

  • 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 :

  • Login with Facebook and allow the app. The app requires 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).

 

    Prepare the Project: Some Boilerplate Stuff

    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 as src/main/webapp/resources/fbstatus/fbstatus_16p.png
      Also save the 16x16 version as an ICO file at src/main/webapp/favicon.ico

      Find your favorite "Login with Facebook" icon, and save it as src/mian/webapp/resources/fbstatus/facebook-login.png

    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/WEB-INF/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">

      <ui:composition template="/WEB-INF/templates/basic.xhtml">

      <ui:define name="pageTitle">Post status to Facebook</ui:define>

      <ui:define name="content">

          <p>Hello</p>

      </ui:define>

      </ui:composition>

      </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.

     

    A Cloud-Ready Facebook App in Java EE in 5 Minutes*

    1. 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>

      I'd love to use Seam Security as well, but due to the similar reason as above, well... let's stick to the raw basics. :-D

    2. Add Apache Commons HttpClient
      project add-dependency org.apache.httpcomponents:httpclient:4.1.3

      We need it because we'll be creating HTTP requests soon.

      Tip: You may also want to add Apache Commons IO because it's very useful in many situations: (but it's not required in this tutorial)

      project add-dependency commons-io:commons-io:2.1

    3. Create FacebookAuthenticator application-scoped bean
      This class forms the bulk of the Facebook authentication logic, and here I implement the entire logic using HTTP operations. For some reason, RestFB does not provide a utility to get an access token (or probably I didn't read the docs well). You can also use an OAuth library. However, this shows actually how simple OAuth 2.0 is in practice.

      Put in src/main/java/org/soluvas/fbstatus/FacebookAuthenticator.java

      package org.soluvas.fbstatus;

      import java.io.FileNotFoundException;
      import java.io.FileReader;
      import java.io.IOException;
      import java.io.InputStream;
      import java.net.URI;
      import java.util.List;
      import java.util.Properties;

      import javax.annotation.PostConstruct;
      import javax.enterprise.context.ApplicationScoped;
      import javax.faces.context.ExternalContext;
      import javax.faces.context.FacesContext;
      import javax.inject.Named;
      import javax.ws.rs.core.UriBuilder;

      import org.apache.commons.io.IOUtils;
      import org.apache.http.HttpResponse;
      import org.apache.http.NameValuePair;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.client.utils.URLEncodedUtils;
      import org.apache.http.impl.client.DefaultHttpClient;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      /**
       * @author ceefour
       * Handles Facebook OAuth authentication.
       */
      @Named @ApplicationScoped
      public class FacebookAuthenticator {

          private transient Logger log = LoggerFactory.getLogger(FacebookAuthenticator.class);
          private String appId = "";
          private String appSecret = "";
          private URI redirectUri;

              @PostConstruct public void init() throws IOException {
              // Load Facebook App credentials from ${OPENSHIFT_DATA_DIR}/fbstatus.properties if exists,
              // otherwise load from WEB-INF/fbstatus.properties
              String dataDir = System.getenv("OPENSHIFT_DATA_DIR");
              Properties props = new Properties();
              if (dataDir != null) {
                  String propsFile = dataDir + "/fbstatus.properties";
                  log.info("Loading credentials from file: {}", propsFile);
                  FileReader fileReader = new FileReader(propsFile);
                  props.load(fileReader);
              } else {
                  log.info("Loading credentials from resource: {}", "/WEB-INF/fbstatus.properties");
                  InputStream stream = FacesContext.getCurrentInstance().getExternalContext().getResourceAsStream("/WEB-INF/fbstatus.properties");
                  if (stream == null)
                      throw new FileNotFoundException("Either ${OPENSHIFT_DATA_DIR}/fbstatus.properties or /WEB-INF/fbstatus.properties must exist.");
                  props.load(stream);
              }
              appId = props.getProperty("facebook.app.id");
              appSecret = props.getProperty("facebook.app.secret");
              log.info("App ID: {}", appId);

                      // Generate redirect URI
              final ExternalContext external = FacesContext.getCurrentInstance()
                      .getExternalContext();
              redirectUri = UriBuilder.fromPath(external.encodeActionURL("/faces/fb_auth.xhtml"))
                      .scheme(external.getRequestScheme())
                      .host(external.getRequestServerName())
                      .port(external.getRequestServerPort()).build();
          }

              public URI getRedirectUri() {
              return redirectUri;
          }

          public URI getAuthorizeUri() {
              return UriBuilder.fromUri("https://graph.facebook.com/oauth/authorize").queryParam("client_id", appId)
                  .queryParam("redirect_uri", getRedirectUri())
                  .queryParam("scope", "publish_stream").build();
          }

              public URI getAccessTokenUri(String authCode) {
              return UriBuilder
                      .fromUri("https://graph.facebook.com/oauth/access_token")
                      .queryParam("client_id", appId)
                      .queryParam("redirect_uri", getRedirectUri())
                      .queryParam("client_secret", appSecret)
                      .queryParam("code", authCode).build();
          }

          /**
           * Sets the authCode received from Facebook and exchanges it with the access token.
           * @param authCode
           * @throws IOException
           * @throws ClientProtocolException
           */
          public String fetchAccessToken(String authCode) throws ClientProtocolException, IOException {
              log.info("Retrieving access token using authCode {}", authCode);
              URI accessTokenUri = getAccessTokenUri(authCode);
              DefaultHttpClient client = new DefaultHttpClient();
              HttpGet accessTokenReq = new HttpGet(accessTokenUri);
              HttpResponse response = client.execute(accessTokenReq);
              if (response.getStatusLine().getStatusCode() != 200)
                  throw new IOException(String.format("GET %s throws HTTP Error %d: %s",
                          accessTokenUri, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()));
              Scanner scanner = new Scanner(response.getEntity().getContent());
              ArrayList<NameValuePair> data = new ArrayList<NameValuePair>();
              URLEncodedUtils.parse(data, scanner, "UTF-8");
              // Probably due to non-existing Content-Encoding, this one is not working:
      //        List<NameValuePair> data = URLEncodedUtils.parse(response.getEntity());
              // see: https://issues.apache.org/jira/browse/HTTPCLIENT-1175
              String accessToken = data.get(0).getValue();
              //log.debug("Access token = {}", fbAccessToken); // security risk?
              return accessToken;
          }

          }

    4. Create the UserSession session-bean
      This bean handles the OAuth code that provided by Facebook, asks the FacebookAuthenticator to exchange it with an access token, then stores the access token in the session.

      Put in src/main/java/org/soluvas/fbstatus/UserSession.java :

      package org.soluvas.fbstatus;

      import java.io.IOException;
      import java.io.Serializable;

      import javax.enterprise.context.SessionScoped;
      import javax.faces.context.ExternalContext;
      import javax.faces.context.FacesContext;
      import javax.inject.Inject;
      import javax.inject.Named;

      import org.apache.http.client.ClientProtocolException;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      @Named @SessionScoped
      public class UserSession implements Serializable {

          private static final long serialVersionUID = 1L;
          private transient Logger log = LoggerFactory.getLogger(UserSession.class);
          @Inject FacebookAuthenticator facebookAuth;
          private String fbAccessToken;

              public String getFacebookAuthCode() {
              return null;
          }

              /**
           * Sets the authCode received from Facebook and exchanges it with the access token.
           * @param authCode
           * @throws IOException
           * @throws ClientProtocolException
           */
          public void setFacebookAuthCode(String authCode) throws ClientProtocolException, IOException {
              String accessToken = facebookAuth.fetchAccessToken(authCode);
              setFbAccessToken(accessToken);

                      log.info("Access token received, redirecting to Post Status page");
              ExternalContext external = FacesContext.getCurrentInstance().getExternalContext();
              external.redirect(external.encodeActionURL("/faces/post.xhtml"));
          }

          public String getFbAccessToken() {
              return fbAccessToken;
          }

          public void setFbAccessToken(String fbAccessToken) {
              this.fbAccessToken = fbAccessToken;
          }

          }

    5. Create the "login button"
      The login "button" is actually a normal link to a Facebook URL with special parameters.

      Edit the basic.xhtml template as follows:

      <p class="navbar-text pull-right">
         
      </p>

      Also edit the contents of the ui:define name="content" in front.xhtml page as follows:

      <div class="row">

          <div class="span12">
              <div class="hero-unit">
                  <h:graphicImage library="fbstatus" name="fbstatus_64p.png"
                      style="float: right;" alt="FBStatus" />
                  <h1>FBStatus</h1>
                  <p>Post status to Facebook...</p>
                  <p>Easy!</p>
                  <p>
                      Login with Facebook »
                  </p>
              </div>

          </div><!--/span-->

          </div><!--/row-->

      The login button/link uses the URI given by "FacebookAuthenticator.authorizeUri" bean, which is implemented like this:

      public URI getAuthorizeUri() {
          return UriBuilder.fromUri("https://graph.facebook.com/oauth/authorize").queryParam("client_id", appId)
              .queryParam("redirect_uri", getRedirectUri())
              .queryParam("scope", "publish_stream").build();
      }

      As you can see, the Facebook authorization URL has 3 query parameters:
      - client_id: Your Facebook App ID
      - redirect_uri: the destination URL after authorization complete
      - scope: Requested permissions, hardcoded to "publish_stream" in this case. You can see the complete list of permissions in Facebook Graph API documentation.

    6. Create the Facebook Authentication Receiver
      We need a page that handles the OAuth verifier code given by Facebook.
      Put in src/main/webapp/fb_auth.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">

      <f:metadata>
          <f:viewParam name="code" value="#{userSession.facebookAuthCode}"/>
      </f:metadata>

      <h:head>
          <title>Facebook Authentication</title>
      </h:head>
      <h:body>
      </h:body>

      </html>

      Also add the mapping in pretty-config.xml :

      <url-mapping id="fb_auth">
          <pattern value="/fb_auth" />
          <view-id value="/faces/fb_auth.xhtml" />
      </url-mapping>

      The page code is very simple because it simply calls UserSession.setFacebookAuthCode(), which in turn delegates the real work to FacebookAuthenticator bean.

    7. Create the Post Status form
      UserSession redirects to /faces/post.xhtml, however we haven't created this page.
      Put this 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="/WEB-INF/templates/basic.xhtml">

      <ui:define name="pageTitle">Post status to Facebook</ui:define>

      <ui:define name="content">

          <div class="row">
              <div class="span12">

                          <h:form>
                      <h2>Post Status</h2>
                      <p><h:inputTextarea value="#{postView.message}" style="width: 30em;" rows="4"/></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>

                                      <p><small>Access token: #{userSession.fbAccessToken}</small></p>
                  </h:form>

              </div> <!--/span-->
          </div> <!--/row-->

      </ui:define>

      </ui:composition>

      </html>

      The JSF page above simply presents text boxes to input 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 website.

    8. Create the bean which posts status to Facebook using RestFB
      This is the backing bean that powers the post.xhtml form above.

      Put in src/main/java/org/soluvas/fbstatus/PostView.java :

      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.context.FacesContext;
      import javax.inject.Inject;
      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;
          @Inject UserSession userSession;
          String message;
          String lastStatusId;
          String lastStatusUrl;

              public void manualPost() {
              FacesContext faces = FacesContext.getCurrentInstance();
              DefaultFacebookClient client = new DefaultFacebookClient(userSession.getFbAccessToken());
              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 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.

      If you want to perform unit testing, you need to supply the (probably mock) UserSession object, set message property then call manualPost().

    And that's it!

    As you can see only a few steps are 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.

    Deploying to OpenShift

    Deploying the app is easy, simply do a "git push".

    The app requires a one-time setup. The Facebook App credentials are not stored in the application, but must be configured separately (typically by the sysadmin aka DevOps guy).

    You need to create a file in your OpenShift data directory (environment value OPENSHIFT_DATA_DIR) named fbstatus.properties .

    There are several ways to create this file, but here I'll show you how to do it with SSH.

    Use the rhc app show command to find out the Git / SSH URL:

    $ rhc app show -a fbstatus

    Application Info
    ================
    fbstatus
        Framework: jbossas-7
         Creation: 2012-03-15T00:22:35-04:00
             UUID: 7652c51ac0b84512b5edef59f00***
          Git URL: ssh://7652c51ac0b84512b5edef59f00***@fbstatus-soluvas.rhcloud.com/~/git/fbstatus.git/
       Public URL: http://fbstatus-soluvas.rhcloud.com/

     Embedded:
          mongodb-2.0 - Connection URL: mongodb://127.13.155.1:27017/

    Now you should be able to connect via SSH by using the depicted username and hostname:

    ssh 7652c51ac0b84512b5edef59f00***@fbstatus-soluvas.rhcloud.com

    Once logged in, go to the fbstatus/data directory and create the fbstatus.properties file using cat :

    cd fbstatus/data
    cat > fbstatus.properties
    facebook.app.id=YOUR_APP_ID
    facebook.app.secret=YOUR_APP_SECRET
    ( press Ctrl+D )

    Now it should look like this:

    [fbstatus-soluvas.rhcloud.com data]\> ls -l
    total 4
    -rw-r--r--. 1 86 Mar 19 04:32 fbstatus.properties

    Reload your app (using rhc app reload) and the app should work fine.

    How to Use the App

    This app's interface and purpose is actually simple. After deploying the app:

    • Click Login with Facebook button. The user will be shown a series of Facebook authorization screens before being redirected back to the site:

    • 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 see the status update at http://www.facebook.com/ceefour/posts/10150732341781672 . Oh and by the way, you're welcome to add me on Facebook :-)

    To make it even more convenient for you, the entire project for this tutorial is available as open source soluvas/fbstatus project at GitHub. It's directly deployable to OpenShift too!

     

    How to Test the App Locally using JBoss AS

    Testing the app locally very similar with OpenShift, but you need to use JBoss AS.

    To provide the fbstatus.properties configuration file, you can either:

    • Set the OPENSHIFT_DATA_DIR environment yourself. This way mirrors how OpenShift environment works. Or:

    • Put the fbstatus.properties file in src/main/webapp/WEB-INF folder. This is an additional  logic I provided to make it more in-line with "traditional" Java EE apps.

    For application settings in Facebook developer site, you need to put "http://localhost/" as your website URL. So that Facebook will allow your app to redirect to "localhost" address (your development machine).

     

    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