Skip to main content

Jersey feat. jQuery + JSONP

Posted by felipegaucho on February 25, 2010 at 5:45 AM PST

One of the most controversial but yet powerful techniques for integrating distributed systems is the JSONP format, a javascript function that bypasses the same origin policy of the browser. Jersey provides built-in support for JSONP and this blog demonstrates how to benefit from this feature.

When JSONP is a good option?

The controversial around JSONP is the fact that using JSONP your application bypasses the Same Origin Policy, facilitating the fearful cross-site scripting (XSS) [1, 2]. Despite that obvious risk, there are certain scenarios where JSONP makes sense and solves a lot of problems which would require a more expensive solution without the usage of cross site scripts.

Consider you need to integrate some data between two web applications hosted by third party companies. Suppose those companies have no previous agreement so in order to share data between the companies you have two choices:

  1. To establish a secure channel between the servers (think about the business costs here).
  2. To expose the application data using JSONP, and then one application can consume the data of the other without any change in the network (the technical effort to support the JSONP format is minimum).

The other aspect to consider in using the traditional is the problem of bottlenecking the web. So, if a server is the only data source of its web-application, does it really inspect the third party servers data before to dispatch to the clients? If not, it is just an unnecessary bottleneck, making the web slower without any benefit (it is nor more neither less secure if the server just forward third party data) - take a couple of minutes to think about that.

xss.png

This topic is much more controversial [1, 2, 3] and detailed than my simple analogies, but I hope you can see the benefits of using JSONP in controllable environments. Otherwise, just consider that Google Adsense wouldn't be feasible without JSONP - a good example on how JSONP unleashes the web. In summary:

 

"JSONP is a prime choice when you trust or when you can control the servers consumed by your web-applications."

 

 

JSONP in practice with Jersey & Glassfish V3

As example of how to enable JSONP in your web-services, I will demonstrate some features of the Arena PUJ project. One of the Arena project goals is to facilitate the consume of its public information about competitions and competitors. So the quest is how to allow the developers to quickly build a client of the Arena server?. The goal is to make Arena HTTP Server as friendly as the Twitter server for example. Fortunately the Jersey framework treats JSONP as first class technology and makes it extremely easy to expose HTTP resources in such format.

JSONP support in Jersey

In order to enable JSONP in a web-application based on the Jersey Framework, you need to care about three basic steps:

  1. To change code on the server-side to return the data wrapped as GenericEntity attributes of the type JSONWithPadding.
  2. To configure Jersey to use the JSON natural notation instead of the default JAXB format.
  3. To change the clients code to understand your data structure formatted as JSONP.

The first two steps are pure Jersey code, and are actually pretty simple steps. The third step can be done with plain javascript but to facilitate my life I will use the JSONP features of the jQuery Library. So, let's start with the server side.

Loading the homeworks of the PUJCE-09 Competition.

PUJ is an academic competition where undergraduate students submit their homeworks for the evaluation by senior Java professionals. A basic use case of web-applications exposing the PUJ data is to offer the downloading of the homeworks of the competitors. You can see such a feature in action in one of the following PUJ Clients:

The above web-applications consume the information from the same server, the Arena PUJ Web-Service. The image of the competitors links to their Java code, just click on it to download the homework. Despite all of them are hosted in the same domain, the DWR and the HTML5 applications use JSONP and are ready to be deployed in other server if necessary. The JSF 2.0 application connects directly in the persistence layer, a set of @Stateless EJBs injected in the JSF ManagedBeans. So we have from the same server two distinct types of data access: regular HTTP Requests with the client deployed in the same domain as the server and JSONP HTTP Request done from applications deployable anywhere (more freedom, more cloud oriented).

First of all let's consume the data in the safe traditional way, using a pure HTTP Request. When you access the URL http://fgaucho.dyndns.org:8080/arena-http/homework?competition=PUJCE-09, you are accessing the below Server code fragment (you can check the complete code here). Notice that the supported response types are XML and JSON, so your data is only accessible from a client launched from the server where this code is running.

  @GET
  @Produces( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
  public Collection<PujHomeworkEntity> readAll(
      @QueryParam(EntityFacadeConstants.PARAM_START) @DefaultValue(EntityFacadeConstants.PARAM_START_DEFAULT_VALUE) int start,
      @QueryParam(EntityFacadeConstants.PARAM_MAX) @DefaultValue(EntityFacadeConstants.PARAM_MAX_DEFAULT_VALUE) int max,
      @QueryParam("acronym") String acronym,
      @QueryParam("title") String title,
      @QueryParam("competition") String competition) {
    Map<String , Serializable> parameters = new ConcurrentHashMap<String, Serializable>();
    if (acronym != null) { parameters.put("acronym", acronym); }
    if (title != null) { parameters.put("title", title); }
    if (competition != null) {
      try {
        parameters.put("competition", competitionFacade.read(
            PujCompetitionEntity.class, competition));
      } catch (Exception error) {
        return new ArrayList<PujHomeworkEntity>();
      }
    }
   
    return facade.findByCriteria(parameters, start, max); // using JPA2 criteria API
  }

Step #1 - using JSONWithPadding

Now you want to expose the same data using JSONP, allowing any HTTP client to access your server data. The only change you need to do in your code is to wrap the collection as a javax.ws.rs.core.GenericEntity and then to return a com.sun.jersey.api.json.JSONWithPadding instead of the collection itself. Notice also the response MIME type, defined as application/x-javascript and also as XML and JSON. This is a special hint about Jersey: you can have only one method returning several different MIME types. I've chosen to have a separated method for JSONP because it is experimental but I believe soon I will review that decision and unify all methods around the same code - keep that in mind if you are designing your own API.

  @GET
  @Path("jsonp")
  @Produces( { "application/x-javascript", MediaType.APPLICATION_JSON,
      MediaType.APPLICATION_XML })
  public JSONWithPadding readAllP(
      @QueryParam("jsoncallback") @DefaultValue("fn") String callback,
      @QueryParam(EntityFacadeConstants.PARAM_START) @DefaultValue(EntityFacadeConstants.PARAM_START_DEFAULT_VALUE) int start,
      @QueryParam(EntityFacadeConstants.PARAM_MAX) @DefaultValue(EntityFacadeConstants.PARAM_MAX_DEFAULT_VALUE) int max,
      @QueryParam("acronym") String acronym,
      @QueryParam("title") String title,
      @QueryParam("competition") String competition) {
    Collection<PujHomeworkEntity> competitions = readAll(start, max,
        acronym, title, institution, competition);
    return new JSONWithPadding(
        new GenericEntity<Collection<PujHomeworkEntity>>(competitions) {
        }, callback);
  }

You can test this method using CURL:

curl -XGET http://fgaucho.dyndns.org:8080/arena-http/homework/jsonp\?competition=PUJCE-09

Step #2 - using JSON natural notation with Jersey

Jersey is one of most friendly API in the Glassfish portfolio, but there is a tweak that bothers me since the first day I tried to serialize JAXB annotated objects in JSON format. If you use attributes in your JAXB objects, the JSON notation will receive an @ symbol in front of it, a non-standard javascript notation that will brake libraries like jQuery. The workaround is provided by the Jersey itself, in format of a ContextResolver class. So, in order to have natural JSON notation in your web-service you need to create a class like the one below. You can find the real code used in the Arena here, and you can also check more information about ContextResolver and JSON natural notation in Jersey here.

import javax.ws.rs.ext.*;
import javax.xml.bind.JAXBContext;
import com.kenai.puj.arena.model.*;
import com.sun.jersey.api.json.JSONConfiguration; import com.sun.jersey.api.json.JSONJAXBContext;

@Provider
public class MyJAXBContextResolver implements ContextResolver<JAXBContext> {

  private JAXBContext context;
  private Class<?>[] types = { PujAdvertisementEntity.class,
      PujCompetitionEntity.class, PujLinkEntity.class,
      PujHomeworkEntity.class };

  public MyJAXBContextResolver() throws Exception {
    this.context = new JSONJAXBContext(JSONConfiguration.natural().build(),
        types);
  }

  public JAXBContext getContext(Class<?> objectType) {
    for (Class<?> c : types) {
      if (c.equals(objectType)) {
        return context;
      }
    }
    return null;
  }
}

You just need to have a ContextResolver class in the same package of your Jersey resources, and the container will load that. The server side is done, now you need a smart client to consume that.

Step #1 - consuming JSONP with jQuery

The special feature that motivated me to use jQuery for loading the JSONP data of Arena was a function called jQuery.getJSON. It basically returns for your javascript code the content of any JSONP enabled server, and jQuery does that using asynchronous calls (AJAX).

<div id="homeworks"></div>

<script type="text/javascript"
  src="http://code.jquery.com/jquery-latest.js"></script>
<script>
  jQuery.noConflict();
  jQuery.ajaxSetup ({ cache: true });
    jQuery(document).ready(function(){
        loadHomeworks("PUJCE-09"); /* loading three different competitions */
        loadHomeworks("PUJCE-08");
        loadHomeworks("PUJCE-07");
        function loadHomeworks(competition) {
          var div = document.createElement('div');
          div.className="history";
          div.id = competition;
          var url = "http://fgaucho.dyndns.org:8080/arena-http/homework/jsonp?competition="+competition+"&jsoncallback=?";
          jQuery.getJSON(url,
          function(data){
              var oUL = document.createElement('ul');
              oUL.className="history2";
              div.appendChild(oUL);
             
            jQuery.each(data, function(i, fn) {
                  var avatar = document.createElement('img');
                  for ( var a in fn.author )
                  {
                      var author = fn.author[a];
                      for ( var r in author.role )
                      {
                       if(author.role[r].name == 'STUDENT') {
                        avatar.src = author.avatar + '?s=48&r=r';
                        avatar.title = author.name;
                        break;
                       }
                      }
                      if(avatar.src) {
                          break;
                      }
                  }
                  var anchor = document.createElement('a');
                  anchor.href = fn.url;
                  anchor.appendChild(avatar);
                  var p = document.createElement('p');
                  p.appendChild(anchor);
                  var item = document.createElement('li');
                  item.appendChild(p);
                  oUL.insertBefore(item, oUL.firstChild);
                 
              });
          });
          var outterDiv = document.getElementById("homeworks");
          outterDiv.appendChild(div);
        }
       
    });
</script>

The result of the above script is demonstrated in the below image, a set of images for each of the PUJCE competitions.

competitors.png

Summary

The script presented here can be embedded in any web-application and I suggest you to try that in your local machine as a cloud computing experiment. Just create a webapp containning an HTML page with the above script and deploy it to your preferred web-server, a Tomcat or a Glassfish for example. Access it and notice that it will load the data from my server without any problem. Isn't it cool? You can consume the data of CEJUG PUJ competitions without any integration effort, without the need of any extra code, you don't need to ask permission for me to load such data or even make a formal agreement to interchange data between the two servers.

JSONP is a silver bullet?

Not at all, JSONP is just an alternative for scenarios where you can have diverse HTTP clients consuming diverse data from diverse HTTP servers in a free way - no need of contracts or web bottlenecks. The clients consume the data from where it is generated, and the servers expose just part of the data required for the clients to complete the information required by the end users. It is cloud computing oriented and it explores the best of the Internet capabilities.

How to control the security and the dynamic of the data? For security there is only one feature: trust, when you code the client software you should consume data from server you know or from servers you trust. And about the changes in data models, you need just one more special feature from the REST style of architecture: HATEOAS. I will talk about that in another blog, for now I believe we already have a good topic to discuss - I am just waiting your feedback.