I have no special talent. I'm only passionately curios - Albert Einstein
November 03, 2009
Automate your Twitter Feed in Java
Posted by Dave Malone
in Spring,
Java
I set out to automate the Twitter feed for the social networking site I developed, MyBuckStory.com. The site is written in Java, and I wanted to continue using bit.ly to shorten my URLs, and the custom format I had come up with to use as a Twitter status update for new stories posted on the site.
I was pleasantly surprised to find an open source library out there already, called Twitter4J. The library was quick and simple to get myself up and running. The closest thing I could find for a Java library for bit.ly was a JSP tag library. I wanted to be able to run this in the service layer of my application, so I began developing my own custom solution.
I've recently been developing webservices using Apache CXF, and have gotten used to the JaxB annotations which make working with XML a breeze. I went ahead and sent a sample request to the bit.ly API for the 'shorten' function (a RESTful service API), and took a look at the response, and modeled my JaxB bean after that response. Here's what I came up with:
Bitly.java
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="bitly")
@XmlAccessorType(XmlAccessType.FIELD)
public class Bitly{
@XmlElement(name = "errorCode")
private String errorCode;
@XmlElement(name = "errorMessage")
private String errorMessage;
@XmlElement(name = "statusCode")
private String statusCode;
@XmlElementWrapper(name="results")
@XmlElement(name="nodeKeyVal",type=NodeKeyVal.class)
private final List results;
public Bitly(){
results = null;
}
public String getErrorCode(){
return errorCode;
}
public String getErrorMessage(){
return errorMessage;
}
public String getStatusCode(){
return statusCode;
}
public List getResults(){
return results;
}
}
NodeKeyVal.java
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="nodeKeyVal")
@XmlAccessorType(XmlAccessType.FIELD)
public class NodeKeyVal{
@XmlElement(name = "userHash")
private String userHash;
@XmlElement(name = "shortKeywordUrl")
private String shortKeywordUrl;
@XmlElement(name = "hash")
private String hash;
@XmlElement(name = "nodeKey")
private String nodeKey;
@XmlElement(name = "shortUrl")
private String shortUrl;
public String getUserHash(){
return userHash;
}
public String getShortKeywordUrl(){
return shortKeywordUrl;
}
public String getHash(){
return hash;
}
public String getNodeKey(){
return nodeKey;
}
public String getShortUrl(){
return shortUrl;
}
}
BitlyClient.java - assumes XML format
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.bind.JAXBException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import model.bitly.Bitly;
import util.JAXBUtil;
public class BitlyClient {
private static final Logger logger = Logger.getLogger(BitlyClient.class);
private static final String PARAM_VERSION = "version";
private static final String PARAM_FORMAT = "format";
private static final String PARAM_LOGIN = "login";
private static final String PARAM_API_KEY = "apiKey";
private static final String PARAM_LONG_URL = "longUrl";
private String version;
private String login;
private String apiKey;
private String shortenApiUrl;
public String shorten(String urlToShorten){
String shortenedUrl = "";
InputStream is = null;
try {
StringBuffer buffer = new StringBuffer(shortenApiUrl)
.append("?").append(PARAM_VERSION).append("=").append(version)
.append("&").append(PARAM_API_KEY).append("=").append(apiKey)
.append("&").append(PARAM_LOGIN).append("=").append(login)
.append("&").append(PARAM_FORMAT).append("=").append("xml")
.append("&").append(PARAM_LONG_URL).append("=").append(urlToShorten);
String url = buffer.toString();
logger.debug("Calling bit.ly shorten API URL: " + url);
URL bitlyUrl = new URL(url);
is = bitlyUrl.openStream();
Bitly bitly = (Bitly)JAXBUtil.parseXML(new BufferedReader(new InputStreamReader(is)), Bitly.class);
shortenedUrl = bitly.getResults().get(0).getShortUrl();
} catch (MalformedURLException e) {
logger.error("Bad bit.ly URL", e);
}catch(IOException e){
logger.error("IOException when trying to call openStream bit.ly URL", e);
}catch(JAXBException e){
logger.error("JAXB error when reading bit.ly shorten response", e);
}finally{
IOUtils.closeQuietly(is);
}
return shortenedUrl;
}
public void setVersion(String version) {
this.version = version;
}
public void setLogin(String login) {
this.login = login;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public void setShortenApiUrl(String shortenApiUrl) {
this.shortenApiUrl = shortenApiUrl;
}
}
TwitterClient.java - uses Twitter4j
import org.apache.log4j.Logger;
import org.htmlparser.util.ParserException;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import model.Story;
import util.PreviewGenerator;
public class TwitterClient {
private static final Logger logger = Logger.getLogger(TwitterClient.class);
private static final String MAIN_URL = "http://mybuckstory.com";
private static final int STATUS_MAX_LENGTH = 140;
private Twitter twitter;
private BitlyClient bitly;
public void updateStatus(Story story) throws ParserException{
String fullUrl = MAIN_URL + story.getUri();
String bitlyUrl = bitly.shorten(fullUrl);
String prefix = story.getTitle() + " - ";
String suffix = "..." + bitlyUrl;
if(!story.getCategories().isEmpty()){
suffix += " #" + story.getCategories().iterator().next().getName();
}
int lengthSoFar = prefix.length() + suffix.length();
int lengthRemaining = STATUS_MAX_LENGTH - lengthSoFar;
//PreviewGenerator uses an HTMLParser to get the Text only content, removing HTML tags
String storyPreview = PreviewGenerator.generatePreview(story.getText(), lengthRemaining);
String status = prefix + storyPreview + suffix;
try {
twitter.updateStatus(status);
} catch (TwitterException e) {
logger.warn("Error occurred when updating Twitter status with new Story", e);
}
}
public void setTwitter(Twitter twitter) {
this.twitter = twitter;
}
public void setBitlyClient(BitlyClient bitly) {
this.bitly = bitly;
}
}
JAXBUtil.java - this code was taken from here
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.ValidationEventLocator;
import org.w3c.dom.Node;
/**
* Tools for working with the JAXB (XML Binding) library.
*/
public class JAXBUtil{
/**
* Parse the XML supplied by the reader into the corresponding tree of Java
* objects.
* @param
*
* @param reader
* Cannot be null. The source of the XML.
* @param rootElementClass
* Cannot be null. The type of the root element.
* @return the Java object that is the root of the tree, of type
* rootElement.
* @throws JAXBException
* if an error occurs parsing the XML.
*/
@SuppressWarnings("unchecked")
public static Object parseXML(Reader reader, Class rootElementClass) throws JAXBException{
if(rootElementClass == null)
throw new IllegalArgumentException("rootElementClass is null");
if(reader == null)
throw new IllegalArgumentException("reader is null");
JAXBContext context = JAXBContext.newInstance(rootElementClass);
Unmarshaller unmarshaller = context.createUnmarshaller();
CollectingValidationEventHandler handler = new CollectingValidationEventHandler();
unmarshaller.setEventHandler(handler);
Object object = unmarshaller.unmarshal(reader);
if(!handler.getMessages().isEmpty()){
String errorMessage = "XML parse errors:";
for(String message : handler.getMessages()){
errorMessage += "\n" + message;
}
throw new JAXBException(errorMessage);
}
return object;
}
/**
* Generate XML using the supplied root element as the root of the object
* tree and write the resulting XML to the specified writer
*
* @param rootElement
* Cannot be null.
* @param writer
* Cannot be null.
* @throws JAXBException
*/
public static void generateXML(Object rootElement, Writer writer) throws JAXBException{
if(rootElement == null)
throw new IllegalArgumentException("rootElement is null");
if(writer == null)
throw new IllegalArgumentException("writer is null");
JAXBContext context = JAXBContext.newInstance(rootElement.getClass());
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(rootElement, writer);
}
private static class CollectingValidationEventHandler implements ValidationEventHandler{
private List messages = new ArrayList();
public List getMessages(){
return messages;
}
public boolean handleEvent(ValidationEvent event){
if(event == null)
throw new IllegalArgumentException("event is null");
// calculate the severity prefix and return value
String severity = null;
boolean continueParsing = false;
switch(event.getSeverity()){
case ValidationEvent.WARNING:
severity = "Warning";
continueParsing = true; // continue after warnings
break;
case ValidationEvent.ERROR:
severity = "Error";
continueParsing = true; // terminate after errors
break;
case ValidationEvent.FATAL_ERROR:
severity = "Fatal error";
continueParsing = false; // terminate after fatal errors
break;
default:
assert false : "Unknown severity.";
}
String location = getLocationDescription(event);
String message = severity + " parsing " + location + " due to " + event.getMessage();
messages.add(message);
return continueParsing;
}
private String getLocationDescription(ValidationEvent event){
ValidationEventLocator locator = event.getLocator();
if(locator == null){
return "XML with location unavailable";
}
StringBuffer msg = new StringBuffer();
URL url = locator.getURL();
Object obj = locator.getObject();
Node node = locator.getNode();
int line = locator.getLineNumber();
if(url != null || line != -1){
msg.append("line " + line);
if(url != null)
msg.append(" of " + url);
}else if(obj != null){
msg.append(" obj: " + obj.toString());
}else if(node != null){
msg.append(" node: " + node.toString());
}
return msg.toString();
}
}
}