Workarounding Cross-domain Restriction With Jersey

Having a JSON generating REST resource, you can consume provided
data in your web page using JavaScript pretty easily. To access data
at your own site you can obviously make a HttpRequest from JavaScript code.
To access data from another site, you will need to workaround
a cross-domain restriction somehow. Two possible approaches are described
at Dan Theurer's blog entries here and here.

In this entry i will show how to add the JavaScript representation option to your Jersey based REST resource, so that besides


{ some JSON data}

when your client asks for http://.../myResource.json,
you will be able to return also something like

myFunc({ some JSON data})

when it asks for http://.../myResource.js?callback=myFunc

Then the technique basically works like follows:


  • You have a callback function myFunc defined in JavaScript, which processes provided JSON data

  • In your page you dynamically create following HTML code: <script src="http://.../myResource.js?callback=myFunc"/>, which automatically invokes your callback function on the returned data

Mime-Type in URI


The key thing here is, that you do not
want to specify desired mime-type (JavaScript) in anywhere but the URI itself.
Otherwise you would not be able to simply add the script tag to your page.

To achieve this in Jersey, you will need to add some media-type mappings to your ResourceConfig.

For a servlet container you can extend
existing ServletContainer like this:


public class MyServletContainer extends ServletContainer {
@Override
public void initiate(ResourceConfig rc, WebApplication wa ) {
super.initiate(rc, wa);
rc.getMediaTypeMappings().put("json", MediaType.valueOf("application/json"));
rc.getMediaTypeMappings().put("xml", MediaType.valueOf("application/xml"));
rc.getMediaTypeMappings().put("js", MediaType.valueOf("text/javascript"));
}
}

And use it in your web.xml:


<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>my.container.MyServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/resources/\*</url-pattern>
</servlet-mapping>

For other containers (http/grizzly) you can just pass your ResourceConfig directly to ContainerFactory

JavaScript Message Bode Writer Provider

Now you will need to have a custom MessageBodyWriter provider
to serialize JAXB beans to the above mentioned JavaScript format.
You can use the following code:


@Provider
@ProduceMime("text/javascript")
public class JavaScriptWriterProvider implements MessageBodyWriter<Object> {
private static Map<Class, JAXBContext> jaxbContexts =
new WeakHashMap<Class, JAXBContext>();
@Context private ContextResolver<JAXBContext> cr;
@Context UriInfo uriInfo;
protected final JAXBContext getJAXBContext(Class type) throws JAXBException {
if (cr != null) {
JAXBContext c = cr.getContext(type);
if (c != null) return c;
}
synchronized (jaxbContexts) {
JAXBContext context = jaxbContexts.get(type);
if (context == null) {
context = JAXBContext.newInstance(type);
jaxbContexts.put(type, context);
}
return context;
}
}
public boolean isWriteable(Class<?> c, Type t, Annotation[] as) {
return (c.getAnnotation(XmlRootElement.class) != null)
|| JAXBElement.class.isAssignableFrom(c);
}
public long getSize(Object arg0) {
return -1;
}
public void writeTo(Object o, Class<?> clazz, Type type, Annotation[] as,
MediaType mediaType, MultivaluedMap<String, Object> headers,
OutputStream entityStream) throws IOException, WebApplicationException {
boolean isJaxbElementCase = JAXBElement.class.isAssignableFrom(clazz);
try {
JAXBContext context = getJAXBContext(isJaxbElementCase ? ((JAXBElement)o).getDeclaredType() : o.getClass());
Marshaller marshaller = context.createMarshaller();
String fcName = uriInfo.getQueryParameters().getFirst("callback");
if (fcName == null) {
fcName = "myFunction";
}
entityStream.write((fcName + "(").getBytes());
if (marshaller instanceof JSONMarshaller) {
marshaller.setProperty(JSONJAXBContext.JSON_ENABLED, Boolean.TRUE);
marshaller.setProperty(JSONJAXBContext.JSON_ROOT_UNWRAPPING, Boolean.TRUE);
marshaller.marshal(o,
new OutputStreamWriter(entityStream));
} else {
marshaller.marshal(o, new JsonXmlStreamWriter(
new OutputStreamWriter(entityStream), true));
}
entityStream.write(")".getBytes());
} catch (JAXBException cause) {
throw ThrowHelper.withInitCause(cause,
new IOException(ImplMessages.ERROR_MARSHALLING_JAXB(o.getClass()))
);
}
}
}

REST Resources

Finally you can implement your resources. The implementation could be as simple as this:


@Path("js-test")
@ProduceMime({"text/javascript", "application/json", "application/xml"})
public class JavaScriptResource {
@GET @Path("jaxb-bean")
public SimpleJaxbBean getJAXBBean() {
return new SimpleJaxbBean();
}
@GET @Path("jaxb-element")
public JAXBElement<SimpleBean> getJAXBElement() {
return new JAXBElement<SimpleBean>(new QName("wrapper"), SimpleBean.class, new SimpleBean());
}
}

Where SimpleBean and SimpleJaxbBean are (for sake of simplicity ) implemented like follows:


public class SimpleBean {
public String value = "jaxb element";
public SimpleBean() {
}
public SimpleBean(String v) {
this.value = v;
}
}


@XmlRootElement
public class SimpleJaxbBean {
public String value = "jaxb bean";
public SimpleJaxbBean() {
}
public SimpleJaxbBean(String v) {
this.value = v;
}
}

It shows, that even POJO could be handled this way and transformed into JSON and JavaScript.

A small sample application with source code and bundled Jersey libs is available here.
You can also download a NetBeans zipped project files here. After deploying the application, the following requests are equivalent. Please note that the JavaScript callback function name is customizable (see the callback query parameter in the second request).


%curl -i -HAccept:text/javascript http://localhost:11750/JavaScriptApp/resources/js-test/jaxb-bean
HTTP/1.1 200 OK
X-Powered-By: Servlet/2.5
Server: Sun Java System Application Server 9.1_01
Content-Type: text/javascript
Transfer-Encoding: chunked
Date: Mon, 26 May 2008 12:39:32 GMT
myFunction({"value":"jaxb bean"})


%curl -i "http://localhost:11750/JavaScriptApp/resources/js-test/jaxb-bean.js?callback=myFunction
HTTP/1.1 200 OK
X-Powered-By: Servlet/2.5
Server: Sun Java System Application Server 9.1_01
Content-Type: text/javascript
Transfer-Encoding: chunked
Date: Mon, 26 May 2008 12:41:00 GMT
myFunction({"value":"jaxb bean"})

Look at Dan Theurer's blog entry for information how the client-side JavaScript could look like.

Comments:

Thanks for sharing!

Posted by Alex on November 11, 2009 at 04:28 AM CET #

This section:

public void initiate(ResourceConfig rc, WebApplication wa ) {

super.initiate(rc, wa);

rc.getMediaTypeMappings().put("json", MediaType.valueOf("application/json"));

rc.getMediaTypeMappings().put("xml", MediaType.valueOf("application/xml"));

rc.getMediaTypeMappings().put("js", MediaType.valueOf("text/javascript"));

}

doesn't work, unless the 'super' call is made at the very end.

Posted by Michael on December 11, 2009 at 05:02 PM CET #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Jakub Podlesak

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today