Content Negotiation Sample Application Guide

Version 1.0.0

Overview

What if your application needs to return both XML and JSON representations of a resource? What are some ways to implement that in JAX-RS while supporting clients? How can HTTP itself help the client negotiate what the response will be like from the server?

Content negotiation is a core concept within the HTTP protocol. A user fetches data from a URL and content is returned back. In some cases, the response's representation such as the data format (a.k.a. content media type) is static and pre-determined. For instance, if the user HTTP GETs a resource like "index.html", more than likely, the user will always be getting a HTML page (the resource's representation with a text/html media type). In more advanced cases, the response's representation is negotiated between the client and the server by various HTTP Accept* headers. The content type, language, character set, and encoding can be negotiated between the client and the server.

Hardcoded Data Formats in URLs and Parameters

Some RESTful services today use hard coded URL schemes to ensure that clients can request content in the format they want. For instance, some put the format information in the URL path so "example.com/collection/resource.json" and "example.com/collection/resource.xml" would only return content in JSON and XML respectively. Some other services make clients specify the response content type in a parameter like "example.com/collection/resource?format=xml". Depending on how advanced your client applications are, this may be all you need. However, this could lead to long URLs as clients need to add in information like encoding, language, character sets, etc. (i.e. some URL like "example.com/collection/resource?format=xml&lang=en&encoding=gzip&charset=UTF-8"). The client application developer would also have to know all of the possible parameters.

Content Negotiation with HTTP Headers

Instead of hardcoding URLs with content representation information, you could create uniform URLs and put the content negotiation information in the HTTP request headers. So in essence, on the HTTP request, a client can use a single URL (i.e. "example.com/collection/resource") but add an Accept: HTTP header and specify that the client prefers application/json and then text/xml (or vice versa). Using HTTP's built-in content negotiation methods reduces the possible URLs and uses standard HTTP headers which are uniform across services (if services choose to acknowledge them).

In clients, complexity can be reduced. For instance, if collections only have to list one URL for each resource (as opposed to one URL for each resource in each data format), then there are potential savings in bandwidth and processing time. A client would find a single URL for a resource, add in a few standard HTTP headers, and then issue the request. There would be no need for additional path or query parameters.

There are potential hazards to using HTTP headers for content negotiation. While most proxies and caches respect HTTP headers, a few implementations may not do what is expected. Clients must also be able to set HTTP headers. The vast majority of clients can set HTTP headers (including JavaScript libraries). Finally, client application developers must understand what HTTP headers they can set for the request.

The rest of this document explains how to implement content negotiation in the JAX-RS server-side service.

The Sample

Data Formats

This sample will support JSON, XML, and plain text. The data is a simple score between two teams. Examples of each format are given:

JSON

{
"homeTeam" : "Blue Team",
"homeScore" : 1,
"visitorTeam" : "Red Team",
"visitorScore" : 2
}

XML

<score>
<homeTeam>Blue Team</homeTeam>
<homeScore>1</homeScore>
<visitorTeam>Red Team</visitorTeam>
<visitorScore>2</visitorScore>
</score>

Text Plain

Home Team: Blue Team ; Home Score: 1 ; Visitor Team: Red Team ; Visitor Score: 2

Hardcoded URLs with Multiple Methods

A simple way to support multiple content types is via the hardcoded URLs mentioned earlier and the following code snippet illustrates one way to do that.

Visiting http://<your host</<context root>/multiplemethods/score/1234.json would return a JSON representation of the resource. Visiting http://<your host</<context root>/multiplemethods/score/1234.xml would return a XML representation of the resource. Finally, visiting http://<your host</<context root>/multiplemethods/score/1234.text would return a text representation of the resource.

@GET
@Path("multiplemethods/score/{gamenum}.json")
@Produces("application/json")
public Response processRequestWithJSON(@PathParam("gamenum") String gameNum) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	JSONObject jsonObj = new JSONObject();
	jsonObj.put("homeTeam", s.getHomeTeam());
	jsonObj.put("homeScore", s.getHomeScore());
	jsonObj.put("visitorTeam", s.getVisitorTeam());
	jsonObj.put("visitorScore", s.getVisitorScore());
	return Response.ok(jsonObj).build();
}

@GET
@Path("multiplemethods/score/{gamenum}.xml")
@Produces("text/xml")
public Response processRequestWithXML(@PathParam("gamenum") String gameNum) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	/* Score is a JAXB annotated class so can return directly */
	return Response.ok(s).build();
}

@GET
@Path("multiplemethods/score/{gamenum}.text")
@Produces("text/plain")
public Response processRequestWithText(@PathParam("gamenum") String gameNum) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	String entity = "Home Team: " + s.getHomeTeam() + " ; Home Score: " + s.getHomeScore() + " ; Visitor Team: " + s.getVisitorTeam() + " ; Visitor Score: " + s.getVisitorScore();
	return Response.ok(entity).build();
}

Hardcoded URLs with a Single Method

Now let's try to use one method to process all of the requests. The only functional difference in the snippet is that "multiplemethods" above in the URL paths is replaced with "singlemethod" in the code snippet below.

@GET
@Path("singlemethod/score/{gamenum}.{type}")
public Response processRequest(@PathParam("gamenum") String gameNum, @PathParam("type") String type) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	if ("json".equals(type)) {
        JSONObject jsonObj = new JSONObject();
       	jsonObj.put("homeTeam", s.getHomeTeam());
		jsonObj.put("homeScore", s.getHomeScore());
		jsonObj.put("visitorTeam", s.getVisitorTeam());
		jsonObj.put("visitorScore", s.getVisitorScore());
		return Response.ok(jsonObj).type(MediaType.APPLICATION_JSON).build();
	} else if ("xml".equals(type)) {
		/* Score is a JAXB annotated class so can return directly */
		return Response.ok(s).type("text/xml").build();
	} else if ("text".equals(type)) {
		String entity = "Home Team: " + s.getHomeTeam() + " ; Home Score: " + s.getHomeScore()
			+ " ; Visitor Team: " + s.getVisitorTeam() + " ; Visitor Score: "
			+ s.getVisitorScore();
		return Response.ok(entity).type(MediaType.TEXT_PLAIN_TYPE).build();
	} else {
		/* return a HTTP status code */
		return Response.status(406).build();
	}
}

HTTP Accept Header

Instead of adding the ".{type}" to the URL in the above code snippets, let's use the HTTP Accept header for content negotiation. The HTTP Accept request header allows the client to specify what media type (aka data format) the response should be in. For instance, if the client adds Accept: application/json to the HTTP request, then the client is telling the server that it would only like to receive the response in JSON format. If the client instead had Accept: text/xml, then the client would only accept XML as the response.

The Accept header could specify multiple media types to indicate that the client is willing to receive content in any of the media types specified. For example, Accept: application/json, text/xml would indicate that both JSON and XML are acceptable representations. Note that you get only one actual representation back, so the response entity would only be in JSON or XML; the client would not receive the same content in multiple formats.

Clients can also add a quality factor parameter to the media type. This allows clients to specify that they would accept any single format from a list of multiple data types but in a particular preference order. For example, Accept: application/json; q=0.4, text/xml;q=0.9 indicates that XML is preferred but JSON is acceptable as well if XML is not available.

See the HTTP specification for the full details on the Accept header.

@Context javax.ws.rs.core.HttpHeaders interface

The Accept header is fairly powerful in that it allows clients to not only let the server know multiple response media types are acceptable but a preferred ordering. However, this could lead to a lot of code that just tries to process the request Accept header. JAX-RS implementations must provide a javax.ws.rs.core.HttpHeaders injectable object which allows developers to retrieve a pre-sorted list of acceptable responses.

javax.ws.rs.core.HttpHeaders objects are injected by the JAX-RS runtime. Annotate the HttpHeaders type with @javax.ws.rs.core.Context as shown in the following code snippet.

@GET
@Path("httpheader/score/{gamenum}")
@Produces(value={"application/json", "application/xml", "text/xml", "text/plain"})
public Response processRequestWithHTTPHeaders(@PathParam("gamenum") String gameNum, @Context HttpHeaders headers) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	
	List<MediaType> acceptableMediaTypes = headers.getAcceptableMediaTypes();
	for (MediaType responseMediaType : acceptableMediaTypes) {
		if(MediaType.APPLICATION_JSON_TYPE.isCompatible(responseMediaType)) {
			JSONObject jsonObj = new JSONObject();
			jsonObj.put("homeTeam", s.getHomeTeam());
			jsonObj.put("homeScore", s.getHomeScore());
			jsonObj.put("visitorTeam", s.getVisitorTeam());
			jsonObj.put("visitorScore", s.getVisitorScore());
			return Response.ok(jsonObj).type("application/json").build();
		} else if (MediaType.TEXT_XML_TYPE.isCompatible(responseMediaType)
			|| MediaType.APPLICATION_XML_TYPE.isCompatible(responseMediaType)) {
			/* Score is a JAXB annotated class so can return directly */
			return Response.ok(s).type(responseMediaType).build();
		} else if (MediaType.TEXT_PLAIN_TYPE.isCompatible(responseMediaType)) {
			String entity = "Home Team: " + s.getHomeTeam() + " ; Home Score: " + s.getHomeScore() + " ; Visitor Team: " + s.getVisitorTeam() + " ; Visitor Score: " + s.getVisitorScore();
			return Response.ok(entity).type("text/plain").build();
		}
	}
	
	/* return a HTTP status code */
	return Response.status(406).build();
}

Developers can also get any other request HTTP header from the HttpHeaders object.

@Context javax.ws.rs.core.Request interface

An alternative to using the HttpHeaders is to use the injectable javax.ws.rs.core.Request interface. JAX-RS provides javax.ws.rs.core.Request#selectVariant(java.util.List variants) for determining what media type, language, and encoding to use in the response.

javax.ws.rs.core.Request objects are injected by the JAX-RS runtime just like the HttpHeaders.

The following snippet builds a list of possible response variations and then uses the injected Request object to select the best representation.

@GET
@Path("acceptheader/score/{gamenum}")
@Produces(value = {"application/json", "application/xml", "text/xml", "text/plain" })
public Response processRequest(@PathParam("gamenum") String gameNum, @Context Request requestObj) {
	Score s = Scoreboard.getScoreForGame(gameNum);
	
	/* build a list of possible response representation variants */
	/* in this case, only the media types are uesd */
	List<Variant> possibleVariants = Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.TEXT_XML_TYPE, MediaType.TEXT_PLAIN_TYPE).add().build();
	
	/* select the best matching variant based on the request HTTP headers */
	Variant responseVariant = requestObj.selectVariant(possibleVariants);

	/* get the media type that the JAX-RS runtime believes is the best possible variant */
	MediaType responseMediaType = responseVariant.getMediaType();
	Locale responseLocale = responseVariant.getLanguage();
	
	/* ...there would be some code to get a Locale specific translation... */
	String homeTeamTranslated = s.getHomeTeam(responseLocale);
	String visitorTeamTranslated = s.getVisitorTeam(responseLocale);
	
	if(MediaType.APPLICATION_JSON_TYPE.isCompatible(responseMediaType)) {
		JSONObject jsonObj = new JSONObject();
		jsonObj.put("homeTeam", homeTeamTranslated);
		jsonObj.put("homeScore", s.getHomeScore());
		jsonObj.put("visitorTeam", visitorTeamTranslated);
		jsonObj.put("visitorScore", s.getVisitorScore());
		return Response.ok(jsonObj).build();
	} else if (MediaType.TEXT_XML_TYPE.isCompatible(responseMediaType)
		|| MediaType.APPLICATION_XML_TYPE.isCompatible(responseMediaType)) {
		/* Score is a JAXB annotated class so can be return directly */
		return Response.ok(s).type(responseMediaType).build();
	} else if (MediaType.TEXT_PLAIN_TYPE.isCompatible(responseMediaType)) {
		String entity = "Home Team: " + homeTeamTranslated + " ; Home Score: " + s.getHomeScore() + " ; Visitor Team: " + visitorTeamTranslated + " ; Visitor Score: " + s.getVisitorScore();
		return Response.ok(entity).build();
	}
	
	/*
     * the Request object should return the best response type among the 3
	 * given so the following code should never execute
	 */
	return Response.serverError().build();
}

The list of possible variants can also include language and encoding permutations. Use the javax.ws.rs.core.Variant.VariantListBuilder to build up the possible list.

It should be noted that each JAX-RS runtime may produce different results depending on how the method is implemented (i.e. some may value the Accept-Language header more than the Accept header or vice versa).

Apache HTTP Client Using Accept Headers

Apache HTTP client can easily add request headers. This sample is with Apache HTTP Client 3.1.

HttpClient client = new HttpClient();

GetMethod getMethod = new GetMethod("http://example.com/path/to/resource");

/* set the Accept request header preferring xml over json */
getMethod.setRequestHeader("Accept", "application/json; q=0.5, text/xml;q=0.8");
        
client.executeMethod(getMethod);
        
/* print the content */
System.out.println(getMethod.getResponseBodyAsString());

Dojo AJAX HTTP Request Using Accept Headers

Dojo JavaScript library AJAX requests can also incorporate HTTP headers by setting a headers property. Dojo does use the handleAs property so you may have to directly access the XmlHttpRequest object and check the Response Content-Type header to determine what format was actually returned.


var requestHeaders = {
	"Accept" : "text/xml;q=0.8, application/json;q=0.2"
};
			
dojo.xhrGet( {
	url: "http://example.com/path/to/resource",
	handleAs: "text",
	headers: requestHeaders, 
	load: function(data, ioArgs) {
		/* process response */
	},
	error: function(data, ioArgs) {
		/* got error */
	}
});

Conclusion

Content negotiation can be done by invoking different URLs, using parameters in the URL, or using HTTP headers. Each have their advantages and disadvantages.