- 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.
- 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.
- 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
- 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.
- Make sure Maven project configuration is up-to-date.
Right click project > Maven > Update Project
- 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.icoFind your favorite "Login with Facebook" icon, and save it as src/mian/webapp/resources/fbstatus/facebook-login.png
- 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
- 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>© 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>
- 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
- 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.
- 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.
- 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/
- 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
- Add Apache Commons HttpClient
project add-dependency org.apache.httpcomponents:httpclient:4.1.3We 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
- 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;
}
}
- 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;
}
}
- 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.
- 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.
- 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.
- 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().
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 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.
Now you should be able to connect via SSH by using the depicted username and hostname:
Once logged in, go to the fbstatus/data directory and create the fbstatus.properties file using cat :
Reload your app (using rhc app reload) and the app should work fine.
This app's interface and purpose is actually simple. After deploying the app:
Testing the app locally very similar with OpenShift, but you need to use JBoss AS.
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 be honest, no, I didn't write this article in 5 minutes. but you could ;-)