Thursday, June 25, 2009

Tinyurl Clone in Java in 40 lines

My colleague (in fact my Boss' Boss) Sau Sheong wrote a hack: Build a tinyurl.com clone in just 40 lines of code. He used Ruby and I was left wondering if Java could be used to do something similar. I tried it last week and have a working code in 40 lines of Java code. This post is to show the code and how I did it:
First, persistence is a breeze in Googe AppEngine and hence I used their JDO approach to store it. The class (with the annotation) is as follows:


package info.shreeni.snip4java;import javax.jdo.annotations.*;
@PersistenceCapable(identityType = IdentityType.DATASTORE)
public class Mapping {
@PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
public Long id;
@Persistent
public String url;
}


As you can see, since the idea was to be as terse as possible, all imports were lumped into the same line and no JavaDocs. Also, I completely did away with getter and setter since that would have consumed more lines of code. :-)

Then comes the MappingManager, the piece that performs the I/O with the Datastore:

package info.shreeni.snip4java;import java.util.*;import javax.jdo.JDOHelper;import javax.jdo.PersistenceManager;
public class MappingManager {
private static PersistenceManager pm = JDOHelper.getPersistenceManagerFactory("transactions-optional").getPersistenceManager();
public static Long storeMapping(Mapping mapping) {
long existingMapping = getMappingByUrl(mapping.url);
return (existingMapping == -1)?pm.makePersistent(mapping).id:existingMapping;
}
public static Long getMappingByUrl(String url) {
Iterator i = ((List)(pm.newQuery("select from " + Mapping.class.getName() + " where url == '" + url + "'").execute())).iterator();
return (i == null || !i.hasNext())?-1:i.next().id;
}
public static String getLink(String urlId) {
Iterator i = ((List)(pm.newQuery("select from " + Mapping.class.getName() + " where id == " + Long.valueOf(urlId, 36)).execute())).iterator();
return (i == null || !i.hasNext())?null:i.next().url;
}
}



The first method stores the mapping, the second gets a url id based on the url (so that we don't keep bloating our DB with repeated URLs) and the third gets a URL based on the Id. The expression "Long.valueOf(urlId, 36)" converts the url id from Base 36 String to a long. Its the same approach as used by Sau Sheong's Snip. All exceptions and error handling is conveniently ignored so that it would end in a HTTP 500 at the end, which I think is a reasonable approach considering the limited amount of code we can write in 40 lines.

Then comes the servlet code which converts a URL into a short URL and print it to the user:

package info.shreeni.snip4java;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.*;
public class Snip extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Mapping mapping = new Mapping();
mapping.url = req.getPathInfo().substring(1);
resp.getWriter().print(req.getRequestURL().toString().replaceAll(req.getPathInfo(), "/").replace("/s/", "/r/") + Long.toString(MappingManager.storeMapping(mapping), 36));
}
}



Quite fascinatingly, the first two lines in the doGet method serve very little, but the third pretty much does all the business logic, or storing the url to db, getting the id, converting to Base 36, constructing the URL and printing it.

Now, the last class which does the real work or taking a short URL and redirecting it to the correct URL stored in the DB:

package info.shreeni.snip4java;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.*;
public class Redirect extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect(MappingManager.getLink(req.getPathInfo().substring(1)));
}
}



Right then, we are done. Thats exactly 40 lines of Java Code. The only thing left is the Web.xml and the appengine-web.xml which are XML required for running all this Java code (first required by any Servlet container and the second required by Google AppEngine.) Being XML, each of those can be compressed into 1 line each, but I am not going to bother presenting them here since they are both too trivial. I put in a index.html just for those users who may not know how to use it (again, this can be packaged into one line). Even if we were to include these three, it would come to only 43 lines.

Thats it, go ahead and try SnipOnJava. Examples: Yahoo.com, Sau Sheong's Snip blog entry.

Administrative note

All tech posts have now moved to http://tech.shreeni.info or you may subscribe to its RSS feed. In due course, I shall be moving it out completely, so if you follow my tech posts, please shift to following that blog.