Monday, 15 July 2013

Consuming LinkedIn REST based Web Services using Scribe OAuth Java Library / API

In this post, I will demonstrate how you can consume/call LinkedIn’s REST based Web Services using a popular and easy to use OAuth Java library called Scribe. As an example, we will call LinkedIn’s Group API to fetch the number of members registered in that group.

Register a new LinkedIn Application

{ If you don’t have a LinkedIn account yet, you should create one now }

First you need to register a new application with LinkedIn to receive an API Key. This unique key helps LinkedIn to identify your application lets you make API calls.

Log into your LinkedIn Developer Account

Click on “+ Add New Application” button

Fill out the form – you need to fill following mandatory fields:

  • Application Name = My First Test App
  • Description = My First Test App
  • Website URL = {URL where your people should go to learn about your application.} – Note you can create a free Java Cloud Hosting Account as shown in my previous post
  • Application Use = For this example, you can select “Groups and Collaboration”
  • Live Status = Development (You can change it to “Live” once you have done your testing and want to go live)
  • Developer Contact Email = Your email address
  • Phone = Your contact number
  • OAuth User Agreement – Default Scope
    • r_basicprofile
    • rw_groups
  • Agreement Language = Browser Local Setting

linkedin developer new application

Once you fill the form and click on “Save” button, you will be provided with an API Key, Secret Key, OAuth User Token and OAuth User Secret. DO NOT SHARE THESE WITH ANY ONE ELSE.

linkedin oauth api key token

Create a new Java Web Application that will consume LinkedIn REST based Web Services

You will be amazed to see how Scribe made it easy to do OAuth and finally call the web service:

OAuthService oAuthService = new ServiceBuilder()
.provider(LinkedInApi.class)
.apiKey(apiKey)
.apiSecret(apiSecret)
.build();
OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, url);
oAuthService.signRequest(new Token(token, tokenSecret), oAuthRequest);
Response response = oAuthRequest.send();

I externalized the secret keys, token and other information like URL, duration etc. so it becomes easy to change the configuration of your application with out re-compile the application all again. In the init() life cycle method of HttpServlet, I loaded all those externalized variables:

@Override
public void init(ServletConfig config) throws ServletException {
this.apiKey = config.getServletContext().getInitParameter("apiKey");
this.apiSecret = config.getServletContext().getInitParameter("apiSecret");
this.token = config.getServletContext().getInitParameter("token");
this.tokenSecret = config.getServletContext().getInitParameter("tokenSecret");
this.url = config.getServletContext().getInitParameter("url");
try
{
durationInMillis = Integer.parseInt(config.getServletContext().getInitParameter("durationInMillis"));
}
catch(Exception e)
{
durationInMillis = 60 * 1000;
}
this.numMembersPrefix = config.getServletContext().getInitParameter("numMembersPrefix");
this.numMembersPostfix = config.getServletContext().getInitParameter("numMembersPostfix");
this.namePrefix = config.getServletContext().getInitParameter("namePrefix");
this.namePostfix = config.getServletContext().getInitParameter("namePostfix");
}

Here,

  • apiKey, apiSecret, token and tokenSecret are the ones you generated above.
  • url is the LinkedIn Group REST API’s URL (we will replace the {{gid}} with actual Group Id in our servlet)
    • http://api.linkedin.com/v1/groups/{{gid}}:(num-members,name)
  • durationInMillis is the time to cache the member count value. During that time any calls to this servlet will serve the client with cached value instead of making a call to LinkedIn Group REST API. Once that time is passed and comes a new client request, the servlet will attempt to fetch the value from LinkedIn Group REST API and update its cache. {durationInMillis defaults to 60 x 1000ms = 1 minute}
  • numMembersPrefix, numMembersPostfix, namePrefix and namePostfix will be used to extract the LinkedIn Group Name and Member Count from the REST API Response XML.

In the doGet() life cycle method of HttpServlet, notice that this servlet expects a request parameter ‘gid’ i.e. LinkedIn Group Id, from the client and if not found, it sets the content to ‘INV’ i.e. Invalid Group Id.

long gid;
try
{
gid = Long.parseLong(req.getParameter("gid"));
}
catch(Exception e)
{
gid = 0;
}
if(gid <= 0)
{
content = "INV";
logMsg("Invalid gid : " + req.getParameter("gid"));
}

If we received a valid ‘gid’, then we first check it in our cache which is a simple HashMap<Long, GroupInfo>:

GroupInfo groupInfo = groups.get(gid);
if(groupInfo == null)
groupInfo = new GroupInfo(gid);
if(groupInfo.getLastChecked() <= 0 || System.currentTimeMillis() - groupInfo.getLastChecked() > durationInMillis )

If we didn’t found an associated GroupInfo object in our cache, we create a new one. And then we check if it’s a first request coming for this ‘gid’ or the time passed since the last request against this ‘gid’ is greater than the configured ‘durationInMillis’ then:

OAuthService oAuthService = new ServiceBuilder()
.provider(LinkedInApi.class)
.apiKey(apiKey)
.apiSecret(apiSecret)
.build();
OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, url.replace("{{gid}}", "" + groupInfo.getId()));
oAuthService.signRequest(new Token(token, tokenSecret), oAuthRequest);
Response response = oAuthRequest.send();
groupInfo.setLastChecked(System.currentTimeMillis());
content = response.getBody();
groupInfo.setName(content.substring(content.indexOf(namePrefix) + namePrefix.length() + 1, content.indexOf(namePostfix)));
String mc = content.substring(content.indexOf(numMembersPrefix) + numMembersPrefix.length() + 1, content.indexOf(numMembersPostfix));
try
{
groupInfo.setMemberCount(Long.parseLong(mc));
logMsg(groupInfo.getId() + " - " + groupInfo.getName() + " - " + groupInfo.getMemberCount() + " members");
content = "" + groupInfo.getMemberCount();
groups.put(groupInfo.getId(), groupInfo);
}
catch(Exception e)
{
logMsg("ERROR >>> " + groupInfo.getId() + " - " + groupInfo.getName() + " | mc {" + mc + "} | " + e.toString());
e.printStackTrace(System.out);
}

Make a call to LinkedIn Group REST API

Save the time to GroupInfo’s lastChecked – so we knew when did we last fetched the Group Information from LinkedIn.

Extract the name and memberCount from the response xml and set them in GroupInfo object.

Also, set the extracted memberCount to content, to be returned to the client.

Save the updated (or newly created) GroupInfo object in cache.

Else, if the time passed since the last request against this ‘gid’ is less than the configured ‘durationInMillis’ then:

else
{
content = "" + groupInfo.getMemberCount();
logMsg(groupInfo.getId() + " - " + groupInfo.getName() + " - " + groupInfo.getMemberCount() + " members (cached)");
}

Simply return the member count value from the cache.

Click here to view this servlet live in action - http://smhumayun.ap01.aws.af.cm/ligmcs?gid=5046253

If you remember my last post about a FREE LinkedIn Group Member Count Widget for Blogger, Wordpress, Drupal, Joomla, Magento, Moodle, Typo, Alfresco, Windows Live, Blogspot, SharePoint, etc.., this widget service uses very similar code like the one I’ve demonstrated above.

You can see the production version of the same being used here.

DOWNLOAD COMPLETE SOURCE CODE FROM HERE

MemberCountServlet.java:

/*
* Consuming LinkedIn REST based Web Services using Scribe OAuth Java Library / API
* http://codeoftheday.blogspot.com/2013/07/consuming-linkedin-rest-based-web.html
*/
package smhumayun.codeoftheday.linkedin.group;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.scribe.builder.ServiceBuilder;
import org.scribe.builder.api.LinkedInApi;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.oauth.OAuthService;
/**
* Main Servlet exposed as a service which will be used by client(s) in order
* to determine number of group members registered in a particular LinkedIn Group
*
* @author smhumayun
*/
public class MemberCountServlet extends HttpServlet {
private String apiKey;
private String apiSecret;
private String token;
private String tokenSecret;
private String url;
private String numMembersPrefix;
private String numMembersPostfix;
private String namePrefix;
private String namePostfix;
private long durationInMillis;
private HashMap<Long, GroupInfo> groups = new HashMap<Long, GroupInfo>();
private SimpleDateFormat df = (SimpleDateFormat) SimpleDateFormat.getDateTimeInstance();
@Override
public void init(ServletConfig config) throws ServletException {
this.apiKey = config.getServletContext().getInitParameter("apiKey");
this.apiSecret = config.getServletContext().getInitParameter("apiSecret");
this.token = config.getServletContext().getInitParameter("token");
this.tokenSecret = config.getServletContext().getInitParameter("tokenSecret");
this.url = config.getServletContext().getInitParameter("url");
try
{
durationInMillis = Integer.parseInt(config.getServletContext().getInitParameter("durationInMillis"));
}
catch(Exception e)
{
durationInMillis = 60 * 1000;
}
this.numMembersPrefix = config.getServletContext().getInitParameter("numMembersPrefix");
this.numMembersPostfix = config.getServletContext().getInitParameter("numMembersPostfix");
this.namePrefix = config.getServletContext().getInitParameter("namePrefix");
this.namePostfix = config.getServletContext().getInitParameter("namePostfix");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String content;
try
{
long gid;
try
{
gid = Long.parseLong(req.getParameter("gid"));
}
catch(Exception e)
{
gid = 0;
}
if(gid <= 0)
{
content = "INV";
logMsg("Invalid gid : " + req.getParameter("gid"));
}
else
{
GroupInfo groupInfo = groups.get(gid);
if(groupInfo == null)
groupInfo = new GroupInfo(gid);
if(groupInfo.getLastChecked() <= 0 || System.currentTimeMillis() - groupInfo.getLastChecked() > durationInMillis )
{
OAuthService oAuthService = new ServiceBuilder()
.provider(LinkedInApi.class)
.apiKey(apiKey)
.apiSecret(apiSecret)
.build();
OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, url.replace("{{gid}}", "" + groupInfo.getId()));
oAuthService.signRequest(new Token(token, tokenSecret), oAuthRequest);
Response response = oAuthRequest.send();
groupInfo.setLastChecked(System.currentTimeMillis());
content = response.getBody();
groupInfo.setName(content.substring(content.indexOf(namePrefix) + namePrefix.length() + 1, content.indexOf(namePostfix)));
String mc = content.substring(content.indexOf(numMembersPrefix) + numMembersPrefix.length() + 1, content.indexOf(numMembersPostfix));
try
{
groupInfo.setMemberCount(Long.parseLong(mc));
logMsg(groupInfo.getId() + " - " + groupInfo.getName() + " - " + groupInfo.getMemberCount() + " members");
content = "" + groupInfo.getMemberCount();
groups.put(groupInfo.getId(), groupInfo);
}
catch(Exception e)
{
logMsg("ERROR >>> " + groupInfo.getId() + " - " + groupInfo.getName() + " | mc {" + mc + "} | " + e.toString());
e.printStackTrace(System.out);
}
}
else
{
content = "" + groupInfo.getMemberCount();
logMsg(groupInfo.getId() + " - " + groupInfo.getName() + " - " + groupInfo.getMemberCount() + " members (cached)");
}
}
}
catch(Exception e)
{
content = "ERR";
e.printStackTrace(System.out);
}
resp.setContentType("text/plain");
resp.setContentLength(content.length());
PrintWriter out = resp.getWriter();
out.print(content);
out.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
private void logMsg (String msg)
{
System.out.println(df.format(new Date(System.currentTimeMillis())) + " | " + msg);
}
}

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>LinkedIn Group Members Count Service by smhumayun@gmail.com</display-name>
<context-param>
<param-name>apiKey</param-name>
<param-value>XXXXXXXXXXXX</param-value>
</context-param>
<context-param>
<param-name>apiSecret</param-name>
<param-value>XXXXXXXXXXXXXXXX</param-value>
</context-param>
<context-param>
<param-name>token</param-name>
<param-value>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</param-value>
</context-param>
<context-param>
<param-name>tokenSecret</param-name>
<param-value>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</param-value>
</context-param>
<context-param>
<param-name>url</param-name>
<param-value>http://api.linkedin.com/v1/groups/{{gid}}:(num-members,name)</param-value>
</context-param>
<context-param>
<param-name>durationInMillis</param-name>
<param-value>300000</param-value>
</context-param>
<context-param>
<param-name>numMembersPrefix</param-name>
<param-value><![CDATA[<num-members]]></param-value>
</context-param>
<context-param>
<param-name>numMembersPostfix</param-name>
<param-value><![CDATA[</num-members]]></param-value>
</context-param>
<context-param>
<param-name>namePrefix</param-name>
<param-value><![CDATA[<name]]></param-value>
</context-param>
<context-param>
<param-name>namePostfix</param-name>
<param-value><![CDATA[</name]]></param-value>
</context-param>
<servlet>
<servlet-name>ligmcs</servlet-name>
<servlet-class>smhumayun.codeoftheday.linkedin.group.MemberCountServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ligmcs</servlet-name>
<url-pattern>/ligmcs</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

No comments:

Post a Comment