dev@jsr311.java.net

URI-based conneg - take 2

From: Marc Hadley <Marc.Hadley_at_Sun.COM>
Date: Wed, 05 Mar 2008 17:13:39 -0500

Proposal take 2:

- We only offer automatic support for content types and language,
nothing for charset or encoding negotiation.

- The feature is controlled by a combination of annotation and config:
   * A property of ApplicationConfig defines the default (enabled/
disabled).
   * A "style" property of the @Path annotation can be used to
override the ApplicationConfig defined default. A value of 'platonic'
means that the path should be treated as part of a platonic URI and
that automated extension mapping should be enabled. A value of
'distinct' means that the path is distinct and disables extension
mapping. The default property value of 'default' defers to an
application wide default.

- When the feature is enabled:

   * A request URI that ends with one or more extensions is matched as
if those extensions were not present. E.g. @Path("widgets") would
match requests for widgets, widgets.xml, widgets.xml.en,
widgets.en.xml and widgets.json.

   * If a URI template ends in a variable then the variable value is
injected without the extension. E.g. @Path("widgets/{id}") with
request for widgets/1.xml.en and @PathParam("id") String id would
result in a value of "1" for id.

   * Each extension is compared to the keys in
ApplicationConfig.getMediaTypesMap (Map<String, MediaType) and
ApplicationConfig.getLanguageMap (Map<String, String). If a match is
found the corresponding Accept or Accept-Language header value is
replaced with the value for the matching key.

- Existing UriInfo methods are not affected, any extensions in the URI
are included in the path returned by any of the methods. For
convenience we add:

   * UriInfo.getPathExtension() that returns the extensions as a
String or null if there isn't one,
   * UriInfo.getPlatonicRequestUriBuilder which returns a URI builder
for the request minus the extensions
   * UriBuilder.extension(String) that adds the supplied extension to
the current final path segment.

An example:

@Path("widgets")
public class WidgetsResource {

   @Context UriInfo uris;
   @Context Request req;

   @GET
   @ProduceMime({"application/xml", "application/json")
   public WidgetList getWidgets() {
       ....
   }

   @POST
   @ConsumeMime({"application/xml", "application/json"})
   public Response addWidget(Widget input) {
       Widget w = createWidgetEntry(input);
       URI platonic = uris.getBaseUriBuilder()
           .path(WidgetResource.class)
           .build(w.getId());
       URI distinct = uris.getBaseUriBuilder()
           .path(WidgetResource.class)
           .extension(uris.getExtension())
           .build(w.getId());
       return Response.created(platonic)
           .contentLocation(distinct);
    }
}

@Path("widgets/{id}")
public class WidgetResource {
   ...
}

Assume you have an app config that maps "xml" to application/xml,
"json" to application/json, en to en and fr to fr. Also assume you
have the required msg body readers and writers for XML and JSON.

GET /widgets.xml will get you XML, GET /widgets.json will get you
JSON, GET /widgets will get whichever matches your accept header most
closely. GET /widgets.xml.en or GET /widgets.en.xml will get you XML
and cause the Accept-Language header to be replaced with Accept-
Language: en. Application code will need to process this as usual.

With the code above, if you POST to a distinct URI you'll get a
platonic location and a distinct content location. If you post to a
platonic you'll get platonic location and content location.

Issues:

(i) Because matching is controlled on a per-resource basis we can't
just preprocess the request URI and then use that for matching. It
would be simpler but less flexible without the per-resource control.
Is that a tradeoff folks would consider ?
(ii) A result of (i) is that for efficiency we essentially treat a URI
with any extensions as matching a platonic URI. E.g.
widgets.foo.bar.fr.xml would match WidgetsResource above and be
treated the same as widgets.fr.xml or widgets.xml.fr. Rejecting URIs
with extensions that don't match one of the configured extensions on a
per-resource basis would be expensive, rejecting them globally would
mean you couldn't have a distinct resource path that had a non-
conforming extension.

Thoughts, comments ?
Marc.

---
Marc Hadley <marc.hadley at sun.com>
CTO Office, Sun Microsystems.