Implementing and testing server-driven content negotiation for your REST resources with WebSphere sMash

Content negotiation is a key aspect to RESTful design. Here are some techniques you can use for content negotiation and how you can implement them using IBM® WebSphere® sMash.

Introduction

Content negotiation is the idea that a single resource can have multiple data representations. In content negotiation, the resource representation returned is selected by the client through the use of several "Accept" headers, as defined in the HTTP specification. The headers most frequently used for content negotiation are:

  • Accept: The Accept request-header field can be used to specify certain media types responses that are acceptable to the client. Accept headers can be used to indicate that the request is specifically limited to a small set of desired types, as in the case of a request for an in-line image. Examples include: application/json, application/atom+xml, and text/html. (See this list of media types.)
  • Accept-Charset: The Accept-Charset request-header field can be used to indicate in which character sets the response should be represented that are acceptable to the client. This field enables clients capable of understanding more comprehensive or special-purpose character sets to signal that capability to a server that is capable of representing documents in those character sets.
  • Accept-Encoding: The Accept-Encoding request-header field is similar to Accept, but restricts the content-codings. For example, you can use this field to indicate compression. Example values: compress;q=0.5, gzip;q=1.0.
  • Accept-Language: The Accept-Language request-header field is similar to Accept, but restricts the set of natural languages that are preferred as a response to the request. Examples include: en for English, es for Spanish.

Another header that contains the word "Accept" is Accept-Ranges; however, this is used for responses and is not a request header. The server uses Accept-Ranges to tell the client which ranges are acceptable.

These other headers are often used to enhance content negotiation:

  • The User-Agent header can also be used for content negotiation. The User-Agent request-header field contains information about the user agent originating the request. This information is for statistical purposes, for tracing protocol violations, and for automated user agent recognition so you can tailor responses to avoid particular user agent limitations.
  • Quality values (qvalue) are used as well. HTTP content negotiation uses short floating point numbers to indicate the relative importance, or weight, of various negotiable parameters. A weight is normalized to a real number in the range 0 through 1, where 0 is the minimum and 1 the maximum value. If a parameter has a quality value of 0, then content with this parameter is "not acceptable" for the client.

The HTTP specification defines several techniques for content negotiation. This article addresses what is referred to as server-driven content negotiation. The specification also discusses two other types of content negotiation:

  • With agent-driven negotiation, the user agent selects the best representation for a response after it receives an initial response from the origin server. Selection is based on a list of the available representations of the response included within the initial response’s header fields or entity-body, with each representation identified by its own URI. Selection from among the representations can be performed automatically (if the user agent is capable of doing so) or manually by the user, selecting from a generated (possibly hypertext) menu. HTTP/1.1 defines the 300 (Multiple Choices) and 406 (Not Acceptable) status codes for enabling agent-driven negotiation when the server cannot provide a varying response using server-driven negotiation.
  • Transparent negotiation is a combination of both server-driven and agent-driven negotiation. When a form of the list of available response representations (as in agent-driven negotiation) is supplied to the cache, and the cache completely understands the dimensions of variance, then the cache becomes capable of performing server-driven negotiation on behalf of the origin server for subsequent requests on that resource.

This article focuses on server-driven negotiation of media types using the Accept header, but the questions and techniques posed here also really apply to other Accept headers as well. Additionally, media type is the one application-to-application that users deal with the most.

A client to a RESTful resource uses the Accept header to communicate intent. There is another header called the Content-Type header which is used to communicate the media type of the actual entity body. For example, an HTTP GET request might contain a list of media types supported by the client in the Accept header. The server then returns the resource representation as the specified type (if it can), and populates the Content-Type header in the response with the actual media type delivered to the client.


Accept headers vs. URI parameters

It is worth mentioning that content negotiation is sometimes non-normatively done using the URI. There exists no specification or standard to negotiate resource representations by altering the resource URI. URIs are convenient because it often enables you to test in a browser. For example, /document?format=atom would return an Atom feed. There are several techniques that have emerged as conventions that prove useful:

  • Using a <dot Notation>, like this: /document.html or /document.json.
  • Using a query parameter, like this: /myResource?format=json

The <dot notation> works well for static resources. For example, if you have a document.html and document.pdf stored in your Web server, these are two different resources.

However, using the <dot notation> as a means for asking for a different format of the same resource can cause confusion if the resource is dynamic. This is because, in the static case, the <dot notation> means two different resources, while in the dynamic case, it is the same resource. In addition, media type is just one content negotiation type. If you combine that with the others discussed earlier, then you could end up with funny looking URLs. For example, if you wanted an isso-8859 JSON version of a resource in English, then you would end up looking for a URI like this: /document.json.en.iso-8859-5.

Query parameters are often used for content negotiations. The reason this happens is because developers often use the browser to quickly test and see results. Adding query parameters often provide a quick way to do this. Query parameters do not give the illusion that you are dealing with a different resource.

However, dynamic RESTFul services are usually built to be used by client applications, either from a browser using Ajax or from another server application. Altering the URI in order to accomplish content negotiation is convenient for sharing a link to a specific resource representation or testing in a browser. However, applications should rely on standard HTTP headers first and selected URI conventions second. Query parameters are usually meant to provide input to a service, such as filtering criteria, sorting, and other business level details. Using headers for content negotiation separates IT concerns from business ones. In addition, requests usually pass through firewalls, proxies, and other servers. These HTTP proxies often understand the standard HTTP header and might provide caching and other nonfunctional requirements.

As a compromise, you can provide a query parameter for development time and then disable it when deploying the application.

In general, application clients should use the simplest technique. Many of the HTTP headers were designed with networking in mind, and helping browsers and intermediate proxies to automate exchange of information. Business applications often do not need this level of sophistication.


Implementing content negotiation with WebSphere sMash

With some background on content negotiation, let’s look at an example that implements content negotiation with IBM WebSphere sMash. There are several ways you can do this, and two techniques are shown later.

The example provided here is a simple resource called "document." It is a hardcoded data structure that contains some fields. The point, of course, is to demonstrate content negotiation. This example is a demonstration using the Accept header. The client will pass in the desired formats via the Accept header. The RESTful Service will then render the hardcoded document based on the best possible match. If none is found, the application returns the appropriate HTTP Response code: 406 Not Acceptable. This is a very common approach for business applications.

Downloading WebSphere sMash and running the App Builder

This article includes a WebSphere sMash application that you can download and test. To view and run the application, you need to download the following:

WebSphere sMash Developer Edition V1.1 includes the App Builder, which is an integrated, Web-based tool for developing WebSphere sMash applications, and a stable runtime to support development, testing, and limited deployment of applications. The command line interface (CLI) contains the base support for developing and running applications. Additional runtime libraries are retrieved as needed from a module repository on ProjectZero.org.

To set up the command line interface:

  1. Download the zero.zip file and unzip it to any directory. This results in a subdirectory called "zero" that contains the commands for running the CLI.
  2. Add the zero directory to the user's PATH environment variable.
  3. Add the bin directory under the JDK installation directory to the user's PATH environment variable.

You can use the WebSphere sMash App Builder to examine the application. To start the App Builder, navigate to the zero directory (where the CLI was installed) and run appbuilder open.

Figure 1. Run App Builder
Figure 1. Run App Builder

Loading the example

The example application is included with this article as a .zip file. Download this file, then unzip it to any empty directory. You can then open the application using the App Builder. Click Open existing application (Figure 2), navigate to the DocumentsSMashApp application where you unzipped the download file, and click Open (Figure 3). The application should then appear on the application list (Figure 4).

Figure 2. Open sample application
Figure 2. Open sample application
Figure 3. Locate sample application
Figure 3. Locate sample application
Figure 4. Application list
Figure 4. Application list

Examining and running the JUnit test with zero.test

With the application now loaded, you can examine the test cases. Test-driven development is an Agile development technique in which developers code test cases before implementing the service. There are many advantages to this approach. My favorite is that it gives you an understanding of the problem, driving you to think first about how the service is used.

WebSphere sMash V1.1 introduces a module called zero.test, which makes it easy for you to run your unit test within WebSphere sMash applications, or to run WebSphere sMash within your own test harness. In also provides extra test utilities. (See the unit testing section of the developer’s guide for more on the zero.test module.)

Here is a test case for you to examine. First, click on the DocumentsSMashApp link in the application list (Figure 4), then click the Explorer link (Figure 5).

Figure 5. Application options
Figure 5. Application options

The Explorer view (Figure 6) enables you to explore the project structure and examine artifacts. Navigate to /app/scripts, as shown in Figure 6. Open the script called ContentNegotiationTest.groovy.

Figure 6. Explorer view
Figure 6. Explorer view

The first section of the test case is shown below. This is a simple JUnit test written in Groovy:

  • The class shown in Listing 1 defines a few class variables. The Abdera and Parser members are part the Apache Abdera API which is distributed with WebSphere sMash. The code will use the classes to validate if correct Atom has been returned.
  • The next variable is a list of URIs. The two URIs represent the two different techniques demonstrated here. Both URLs will use the same UnitTest.
  • Then there is a special utility method titled "callResource," which uses the WebSphere sMash API to issue an HTTP GET to a URL that is passed in. It also populates the HTTP Accept header if it is present.
Listing 1
public class ContentNegotiationTest 
{ 
    Abdera abdera = new Abdera(); 
    Parser parser = abdera.getParser(); 

    def URIS = ["http://localhost:8080/resources/document",
                "http://localhost:8080/custom/document"];      

    private Connection.Response callResource(uri,acceptHeader)throws  Exception 
    { 
Connection conn = new    Connection(uri,Connection.Operation.GET); 
      if (acceptHeader) conn.addRequestHeader("Accept", acceptHeader); 
      return conn.getResponse(); 
      }

There are three test methods. The first two methods test the two desired media types, Atom and JSON. The third tests the 406 error condition. Below is the test case for Atom:

  • There is a list of media type values for the Accept header that should return Atom. Since the Accept header can contain a comma-separated list of desired types by the client, it is important to test the various types. Here are examples of when Atom should be returned:

  • The test case (Listing 2) first loops through each URI that implements the service.

    Listing 2
    	@Test 
    void testAtomXml() 
     { 
         try { 
             def acceptHeaders = ["application/xml", 
                                  "application/atom+xml", 
                                  "application/xml,application/json", 
                                  "application/atom+xml,application/json", 
                                   null, //Default 
                                  "application/atom+xml;type=feed", 
                                  "text/html,application/atom+xml"];             
             for(uri in URIS) 
             { 
                     
                 for(acceptHeader in acceptHeaders) 
                 { 
                     System.out.println(); 
                     System.out.println(uri + " with Accept set to " + acceptHeader); 
                     Connection.Response resp = callResource(uri,acceptHeader); 
                     Document<Feed> xmlDoc = parser.parse(resp.getResponseBodyReader()); 
                     Feed feed = xmlDoc.getRoot(); 
                     assertNotNull(feed); 
                     assertEquals(resp.getResponseHeader
    				("Content-Type")[0],"application/atom+xml"); 
                     Writer xmlAtomWriter = 
    				abdera.getWriterFactory().getWriter("prettyxml"); 
                     StringWriter stringWriter = new StringWriter(); 
                     feed.writeTo(xmlAtomWriter, stringWriter); 
                     System.out.println(stringWriter.toString()); 
                     System.out.println(); 
                 } 
             } 
         } catch (Exception e) { 
             System.out.println(e.getMessage()); 
             System.out.println(); 
             fail(e.getMessage()); 
         } 
     }
  • The inner loop goes through each media type and invokes the callResource utility method.

Similarly, the JSON test case (Listing 3) lists a set of media types that should return JSON; namely, application/json. A list that has both JSON and Atom supported is also provided here, but JSON is now listed first. The test case similarly loops through the URIs that implement the service and the supported media types. It then validates that it has proper type of JSON, and that the content-type response header is JSON.

Listing 3
@Test 
    void testJSON() 
    { 
        try { 
            def acceptHeaders = ["application/json", 
                                   "application/json,application/xml", 
                                   "application/json,application/atom+xml", 
                                   "text/html,application/json,text/xhtml"]; 
            for(uri in URIS) 
            { 
                for(acceptHeader in acceptHeaders) 
                { 
                    System.out.println(); 
                    System.out.println(uri + " with Accept set to " + acceptHeader); 
                    Connection.Response resp = callResource(uri,acceptHeader); 
                    def json = Json.decode(resp.getResponseBodyInputStream()); 
                    assertNotNull(json); 
                    assertEquals(resp.getResponseHeader
				("Content-Type")[0],"application/json"); 
                    System.out.println(json); 
                    System.out.println(); 
                } 
            } 
        } catch (Exception e) { 
            System.out.println(e.getMessage()); 
           System.out.println(); 
            fail(e.getMessage()); 
        } 
        }

It is very important that you test error conditions in your code. The last test case (Listing 4) creates a list of invalid media types. An invalid media type should return an HTTP response code of 406. Again, the test case similarly loops through the URIs and media types to test the service.

Listing 4
    @Test 
    void test406() 
    {    
        try { 
            def acceptHeaders = ["application/ftp", 
                                   "text/html", 
                                   "application/atom+xml;type=entry", 
                                   "junk", 
                                   "text/json"]; 
            for(uri in URIS) 
            { 
                for(acceptHeader in acceptHeaders) 
                { 
                    System.out.println(); 
                    System.out.println(uri + " with Accept set to " + acceptHeader); 
                    Connection.Response resp = callResource(uri,acceptHeader); 
                    assertEquals("406",resp.getResponseStatus()); 
                    System.out.println(resp.getResponseStatus()); 
                    System.out.println(); 
                } 
            } 
        } catch (Exception e) { 
            
           System.out.println(e.getMessage()); 
           System.out.println(); 
           fail(e.getMessage()); 
        } 
    } 
     
     }

Before examining the services themselves, you can run the test cases. To run unit tests inside the WebSphere sMash application, you need to add the zero.test module to the application. Also, since you need the Abdera APIs for Atom, you need to add the zero.atom module as well. The sample application provided with this article already declares these dependencies. Click on the Dependencies tab (Figure 7) and you will notice the core, atom, and test modules have been added.

Figure 7. Application dependencies
Figure 7. Application dependencies

Start the application by clicking the Start button on the upper right corner of the panel (Figure 8).

Figure 8. Start application
Figure 7. Application dependencies

The WebSphere sMash App Builder lets you to run the WebSphere sMash command line interface directly in the browser. Click on the Console tab (Figure 9) and type zero update. This will ensure that all the dependencies are loaded in your local repository. Next, type zero test. The test task will look for any script in the /app/scripts or Java™ class that ends in the name "Test" and execute it as a JUnit Test. You should get a success message as shown in Figure 10.

Figure 9. Command line interface
Figure 9. Command line interface
Figure 10. Success message
Figure 10. Success message

Scroll up and examine the system output statements to see the services being run and the negotiated responses.

Figure 11. System output messages
Figure 11. System output messages

Now that you have tested the services, let’s look at the two different implementations.

Content negotiation using resource handlers and scripting

The first technique is to use the RESTful resource handlers. One of the goals of WebSphere sMash is to provide conventions to make it easy to implement applications. One convention is the use of resource handlers. Resource handlers implement resources that follow a collection/member paradigm. Collections have member items that you can add, remove, update and delete. You can also get a list of members in the collection. For example, see Table 1.

Table 1
HTTP methodURIDescription
GET/documentReturns a list of documents.
POST/documentCreates a member document.
GET/document/myDocRetrieve the member document called myDoc.
PUT/document/myDocUpdates the member document myDoc.
DELETE/document/myDocDeletes the member document myDoc.

WebSphere sMash supports the collection model natively within the <apphome>/app/resources virtual directory. Each script within the resources directory represents a resource handler, which implements the collection and member operations. Resource handlers are accessed via a simple URL convention, based upon this pattern:

/resources/<collection name>[/<member identifier>[/<path info>]]

For example, consider a request to /resources/people/1 that has the collection name "people" and member identifier "1." The collection name identifies the resource handler. In this case, <apphome>/app/resources/people.groovy and the value of member identifier is provided as a request parameter with name <collection name>Id. Here, zget("/request/params/peopleId") == 1. The resource handler is a WebSphere sMash event handler, designed to handle resource CRUD (create, retrieve, update, and delete) events. These are operations that are commonly performed on data, or in this case, HTTP resources.

HTTP methods are mapped into collection and member events, as shown in Table 2.

Table 2
ResourceGETPUTPOSTDELETE
CollectionlistputCollectioncreatedeleteCollection
MemberretrieveupdatepostMemberdelete

Thus, to write a handler that lists the collection of documents with a URI of /resources/document, create a script named document.groovy in the /app/resources directory of your application. Since this simple sample needs only to respond to the HTTP GET method on the collection of documents, the script only requires the onList() method. (For more details on writing RESTFul resource handlers in WebSphere sMash, see the REST programming section of the Developer’s Guide and the Build RESTful services in your Web application tutorial.)

To look at your resource handler, return to the Explorer tab. Expand the /app/resources folder and open document.groovy (Figure 12). The code is shown in Listing 5.

Figure 12. Recource handler
Figure 12. Recource handler
Listing 5
def onList() 
{ 
    def mediaType = request.headers.in.Accept[]; 
     
    /* if Accept Header is missing, HTTP Spec says 
     * you should assume client supports all types of media types. 
     */ 
    if(!mediaType) mediaType = config.defaultMediaType[]; 
     
    // Media types can be a comma separated list 
    def acceptTypeHeaders = mediaType.split(","); 
    for (acceptTypeHeader in acceptTypeHeaders) 
    { 
        //Media types can have profiles such as application/atom+xml;type=entry 
        def profile = acceptTypeHeader.split(";"); 
        System.out.println (profile) 
        def contentType = profile[0]; 
        if(contentType == "application/atom+xml" || contentType == "application/xml") 
        { 
            if(profile.size() > 1) 
            { 
                /*Checking of client is asking for atom entry only,  
                but this resource can only render feeds*/ 
                def nameValue = profile[1].split("="); 
                def key = nameValue[0]; 
                def value = nameValue[1]; 
                 
 
                if(key == "type") if (value != "feed") continue; 
            } 
            request.status = HttpURLConnection.HTTP_OK; 
            request.view = 'atom' 
            request.atom.output = DocData.dummyPayload 
            render() 
            return; 
        } 
        else if (contentType == "application/json") 
        { 
            request.status = HttpURLConnection.HTTP_OK; 
            request.view = 'JSON' 
            request.json.output = DocData.dummyPayload 
            render() 
            return; 
        } 
    } 
 
    request.status = HttpURLConnection.HTTP_NOT_ACCEPTABLE; 
    request.view = 'error'; 
    render(); 
    return; 
}

WebSphere sMash enables easy access to HTTP headers, query parameter, payload, and other HTTP artifacts through default scripting variables that are automatically populated. In Listing 5:

  • The Accept header is accessed from the request zone of the global context. The global context maintains all state for your WebSphere sMash application. There are several zones which implement different life cycles for your data. For example, the request zone contains all the data for the current HTTP request. You accessed the Accept header like this: request.headers.in.Accept[]. This is one way you can access the global context. (See Resources for more information on global context.)
  • If the Accept header is null, this means that the client accepts any type, so the default is set. The default is accessed via the config zone of the global context like this: config.defaultMediaType[]. The config zone contains your configuration. It is populated from the config/zero.config file of your application. (See the configuration section of the Developer Guide.)
    Listing 6
    #Resource Handler Default Media Type 
    /config/defaultMediaType="application/atom+xml"
  • Next, the code parses the Accept header using the Groovy string manipulation. The code looks through the comma-separated list, finds the first match, and extracts the profile. Once the match is found, it takes the data structure and renders it using the default WebSphere sMash rendering framework. For example, if application/json is found, then the code in Listing 7 is run:
    Listing 7
    request.status = HttpURLConnection.HTTP_OK;
    request.view = 'JSON' 
    request.json.output = DocData.dummyPayload 
    render()
  • You will notice a render section for Atom as well. If nothing is found, then the code sets the NOT_ACCEPTABLE variable to the response status.

The advantage of this approach is you can be very precise and get exactly what you want. However, it can lead to a lot of unmanageable code. If you script it yourself, you can most likely create a reusable library and externalize the code.

Content negotiation using custom handlers and regular expressions

The second approach is to write a custom handler. A custom handler is created by configuring a rule in the zero.config file. Rather than having a single handler handle all the content negotiation logic, the zero.config file will match the best handler using Java regular expressions. Under the app/scripts directory, you will notice three handlers (Figure 13). Each one implements a specific renderer for the desired media type.

Figure 13. Custom handlers
Figure 13. Custom handlers

Figure 14 shows a handler with an onGET method to handle a GET event. It will render the data as Atom.

Figure 14. Custom handler renders data as Atom
Figure 14. Custom handler renders data as Atom

Similarly, the customJSONDocument.groovy file renders JSON (Figure 15).

Figure 15. Custom handler renders data as JSON
Figure 15. Custom handler renders data as JSON

Finally, a renderer for sending a 406 response code (Figure 16).

Figure 16. Custom handler renders error code
Figure 16. Custom handler renders error code

In order for the proper handler to be called, the zero.config file will have matching rules. Open the zero.config file under the config directory (Figure 17).

Figure 17. Open zero.config
Figure 17. Open zero.config

The zero.config file contains three configurations, one for each handler; handlers are configured under the config zone under Handlers. For handlers, you define the event (such as GET), the handler, and a condition. In each case, you match exactly on the path and HTTP GET method, and then you use the =~ to signify a match using Java regular expressions.

WebSphere sMash adds prefix (^) and suffix ($) line boundaries to the expressions. (See this tutorial on boundary matching.) The more complex expression is the Atom expression. It looks for either application/xml or application/atom+xml (as long as it is not followed by ;type=entry). It also checks to see if the Accept header is missing or null for the default case. There is also a check to see if application/json does not precede it using a negative look ahead. WebSphere sMash will only emit one event for an HTTP method, so if the Atom match is found, then it wins. If the Atom match falls through, then it will check for JSON. It does not need to check for a proceeding Atom entry because if the Atom match fell through, then the string does not contain one. The final check determines if the Accept header is populated. If so, then it is invalid and a 406 is returned.

Listing 8
#ATOM
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "customATOMDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  &&
      (/request/method == GET) && (/request/headers/in/Accept =~ 
	(.*)(?<!(application/json,))application/(atom\\+)??xml(?!;type=entry)
	(.*)) ||  !(/request/headers/in/Accept)" 
}] 
 
#JSON
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "customJSONDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  && 
	(/request/method == GET) && (/request/headers/in/Accept =~ 
	(.*)application/json(.*))" 
}] 
 
#406
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "notAcceptCustomDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  && 
	(/request/method == GET) &&  (/request/headers/in/Accept) " 
}]

Using custom handlers can simplify the logic for content negotiation, but you might end up with a lot of configuration stanzas, and you might have to handle patterns like collection/member on your own. In addition, you might not be able to be as precise as you want. For example, a negative look ahead can only be absolute. For example, application/json,application/xml will be handled correctly and return JSON, but application/json,application/html,application/xml will not; it will return Atom instead, since JSON does not directly precede it. In cases where you are able to dictate what to populate for the header, this might not be an issue.


Summary

Content negotiation is an important part of RESTful architecture. This article demonstrated ways in which you can build content negotiation into your WebSphere sMash applications.


Acknowledgements

Roland would like to thank Karl Bishop and Brandon Smith for reviewing this article.


Download

DescriptionNameSize
Code sampleDocumentsSMashApp.zip76 KB

Resources

Learn

Get products and technologies

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into WebSphere on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=WebSphere, Web development
ArticleID=366122
ArticleTitle=Implementing and testing server-driven content negotiation for your REST resources with WebSphere sMash
publish-date=01212009