How do you integrate GWT with a JAX-RS/RESTEasy server using Errai?
Asked Answered
T

2

6

I'd like to call a REST service created using RESTEasy and JAX-RS from a GWT client application. What is the best process for using Errai to use a single code base for both server and client?

Trincomalee answered 6/6, 2012 at 7:10 Comment(0)
T
27

We all love REST. It’s vendor, platform and language neutral; it’s simple to debug, implement and access; and it provides a solid back end to your cloud, browser, mobile and desktop apps.

Java developers can use libraries that support JAX-RS, like RESTEasy, to get a REST server up and running in just minutes. Then, using JAX-RS clients, these JAX-RS REST servers can be called from Java client applications with just a few lines of code.

But despite the fact that GWT shares much in common with Java, calling REST services from GWT can be a painful experience. Using the RequestBuilder class involves specifying the correct HTTP method, encoding your URL, and then either decode the response or create Overlay objects to represent the data being sent back by the REST server. This may not be a large overhead for calling one or two REST methods, but it does represent a lot of work when integrating GWT with a more complicated REST service.

This is where Errai comes in. Errai is a JBoss project that, among other things, implements the JAX-RS standard within GWT. In theory this means you can share your JAX-RS interface between your Java and GWT projects, providing a single source that defines the functionality of your REST server.

Calling the REST server from Errai involves only a few simple steps. First, you need the REST JAX-RS interface. This is a JAX-RS annotated Java interface that defines the methods that will be provided by your REST server. This interface can be shared between your Java and GWT projects.

@Path("customers")
public interface CustomerService {
  @GET
  @Produces("application/json")
  public List<Customer> listAllCustomers();

  @POST
  @Consumes("application/json")
  @Produces("text/plain")

  public long createCustomer(Customer customer);
}

The REST interface is then injected into your GWT client class.

@Inject
private Caller<CustomerService> customerService;

A response handler is defined.

RemoteCallback<Long> callback = new RemoteCallback<Long>() {
  public void callback(Long id) {
    Window.alert("Customer created with ID: " + id);
  }
};

And finally the REST method is called.

customerService.call(callback).listAllCustomers();

Pretty simple huh?

You may be led to believe from this example that Errai will provide a drop in solution to your current JAX-RS infrastructure, but unfortunately this simple example doesn’t touch on some of the complications that you are likely to see when trying to combine your GWT and Java REST code base. Here are some of the gotchas to be aware of when using Errai and JAX-RS.

You’ll need to implement CORS

Typically when implementing a GWT JAX-RS client, you will be debugging your GWT application against an external REST server. This won’t work unless you implement CORS, because by default the browser hosting the GWT application will not allow your JavaScript code to contact a server that is not running in the same domain. In fact you can even be running the REST server on your local development PC and still run into these cross domain issues, because calls between different ports are also restricted.

If you are using RESTEasy, implementing CORS can be done with two methods. The first is done using the MessageBodyInterceptors interface. You provide the write() method, and annotate your class with the @Provider and @ServerInterceptor annotations. The write() method is then used to add the “Access-Control-Allow-Origin” header to responses to any simple requests (“simple” requests don’t set custom headers, and the request body only uses plain text).

The second method handles the CORS preflight requests (for HTTP request methods that can cause side-effects on user data - in particular, for HTTP methods other than GET, or for POST usage with certain MIME types). These requests use the HTTP OPTIONS method, and expect to receive the “Access-Control-Allow-Origin”, “Access-Control-Allow-Methods” and “Access-Control-Allow-Headers” headers in the reply. This is demonstrated in the handleCORSRequest() method below.

Note

The REST interface below allows any and all CORS requests, which may not be suitable from a security standpoint. However, it is unwise to assume that preventing or restricting CORS at this level will provide any degree of security, as setting up a proxy to make these requests on behalf of the client is quite trivial.

@Path("/1")
@Provider
@ServerInterceptor
public class RESTv1 implements RESTInterfaceV1, MessageBodyWriterInterceptor
{
    @Override
    public void write(final MessageBodyWriterContext context) throws IOException, WebApplicationException
    {   context.getHeaders().add(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
        context.proceed();      
    }

    @OPTIONS
    @Path("/{path:.*}")
    public Response handleCORSRequest(@HeaderParam(RESTInterfaceV1.ACCESS_CONTROL_REQUEST_METHOD) final String requestMethod, @HeaderParam(RESTInterfaceV1.ACCESS_CONTROL_REQUEST_HEADERS) final String requestHeaders)
    {
        final ResponseBuilder retValue = Response.ok();

        if (requestHeaders != null)
            retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);

        if (requestMethod != null)
            retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_METHODS, requestMethod);

        retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");

        return retValue.build();
    }

}

With these two methods in place, any call to your REST server will provide the appropriate responses to permit cross origin requests.

You’ll need to accept and respond with simple POJOs

The introduction illustrated a simple REST interface that responded with a Long. Both the Java and GWT implementations of JAX-RS know how to serialize and deserialize primitives and simple classes like the java.util collections.

In a real world example, your REST interface is going to respond with more complicated objects. This is where the different implementations can clash.

For a start, JAX-RS and Errai use different annotations to customize the marshalling of objects between JSON and Java objects. Errai has annotations like @MapsTo and @Portable, while RESTEasy (or Jackson, the JSON marshaller) uses annotations like @JsonIgnore and @JsonSerialize. These annotations are mutually exclusive: GWT will complain about the Jackson annotations, and Jackson can’t use the Errai annotations.

The simple solution is to have use set of simple POJOs with your rest interface. By simple I mean classes that have no-args constructors, and have only getter and setter methods that directly relate to properties that will be present in the JSON object as it travels over the wire. Simple POJOs can be marshalled by both Errai and Jackson with their default settings, removing the need to juggle incompatible annotations.

Errai and Jackson also source the names for the resulting properties in the JSON string from different places. Jackson will use the names of the getter and setter methods, while Errai will use the names of the instance variables. So make sure that your instance variables and getter/setter methods names are exactly the same. This is ok:

public class Test
{
    private int count;
    public int getCount() {return count;}
    public void setCount(int count) {this.count = count;}
}

This will cause problems:

public class Test
{
    private int myCount;
    public int getCount() {return myCount;}
    public void setCount(int count) {this.myCount = count;}
}

Secondly, it is tempting to add additional methods to these REST data objects to implement some business functionality. However, if you do this it won’t take long before you try and use a class that is not supported by GWT, and you might be surprised at what GWT doesn’t support: date formatting, cloning arrays, converting a String to a byte[]... The list goes on. So it’s best to stick to the basics in your REST data objects, and implement any business logic completely outside of the REST data object inheritance tree using something like composition or a component based design.

Note

Without the @Portable annotation, you will need to manually list any classes used Errai when calling the REST interface in the ErraiApp.properties file.

Note

You’ll also want to stay away from Maps. See this bug for details.

Note

You can't used nested parameterized types in the object hierarchy returned by your JSON server. See this bug and this forum post for details.

Note

Errai has issues with byte[]. Use a List instead. See this forum post for more details.

You’ll need to debug with Firefox

When it comes to sending large amounts of data over a REST interface using GWT, you will have to debug your application with Firefox. In my own experience, encoding even a small file into a byte[] and sending it over the network caused all manner of errors in Chrome. A variety of different exceptions will be thrown as the JSON encoder attempts to deal with corrupted data; exceptions that are not seen in the compiled version of the GWT application, or when debugging on Firefox.

Unfortunately Google has not managed to keep their Firefox GWT plugins up to date with Mozilla’s new release cycles, but you can quite often find unofficial released by Alan Leung in the GWT Google Groups forums. This link has a version of the GWT plugin for Firefox 12, and this link has a version for Firefox 13.

You’ll need to use Errai 2.1 or later

Only Errai 2.1 or later will produce JSON that is compatible with Jackson, which is a must if you are trying to integrate GWT with RESTEasy. Jackson marshalling can be enabled using

RestClient.setJacksonMarshallingActive(true);

or

<script type="text/javascript">
  erraiJaxRsJacksonMarshallingActive = true;
</script>

You'll need to create separate JAX-RS interfaces for advanced features

If your JAX-RS REST interface returns advanced objects, like ATOM (or more to the point, imports classes like org.jboss.resteasy.plugins.providers.atom.Feed), you'll need to split your REST interface across two Java interfaces, because Errai doesn't know about these objects, and the classes probably aren't in a state that can be easily imported into GWT.

One interface can hold your plain old JSON and XML methods, while the other can hold the ATOM methods. That way you can avoid having to reference the interface with the unknown classes in your GWT application.

Note

Errai only supports JSON mashalling at this point, although in future you may be able to define custom marshallers.

Trincomalee answered 6/6, 2012 at 7:10 Comment(1)
That is a really complete answer !Valetudinarian
E
2

In order to implement CORS (so I could get cross-site requests, of course), as I'm using RESTEasy, I followed the classes suggested by the accepted answer and needed to change it a bit to work. Here's what I used:

//@Path("/1")
@Path("/") // I wanted to use for all of the resources
@Provider
@ServerInterceptor
public class RESTv1 implements RESTInterfaceV1, MessageBodyWriterInterceptor
{

    /* Enables the call from any origin. */
    /* To allow only a specific domain, say example.com, use "example.com" instead of "*" */
    @Override
    public void write(final MessageBodyWriterContext context) throws IOException, WebApplicationException
    {   context.getHeaders().add(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        context.proceed();      
    }

    /* This is a RESTful method like any other.
    The browser sends an OPTION request to check if the domain accepts CORS.
    It sends via header (Access-Control-Request-Method) the method it wants to use, say 'post',
    and will only use it if it gets a header (Access-Control-Allow-Methods) back with the intended
    method in its value.
    The method below then checks for any Access-Control-Request-Method header sent and simply
    replies its value in a Access-Control-Allow-Methods, thus allowing any method to be used.

    The same applies to Access-Control-Request-Headers and Access-Control-Allow-Headers.
    */
    @OPTIONS
    @Path("/{path:.*}")
    public Response handleCORSRequest(
        @HeaderParam(RESTInterfaceV1.ACCESS_CONTROL_REQUEST_METHOD) final String requestMethod,
        @HeaderParam(RESTInterfaceV1.ACCESS_CONTROL_REQUEST_HEADERS) final String requestHeaders)
    {
        final ResponseBuilder retValue = Response.ok();

        if (requestHeaders != null)
            retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);

        if (requestMethod != null)
            retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_METHODS, requestMethod);

        retValue.header(RESTInterfaceV1.ACCESS_CONTROL_ALLOW_ORIGIN, "*");

        return retValue.build();
    }
}

Take care, as it will allow requests from any origin (ACCESS_CONTROL_ALLOW_ORIGIN_HEADER is set to "*" in both methods).

The values for those constants are as follows:

public interface RESTInterfaceV1 {
    // names of the headers
    public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
    public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
    public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
}

Thatss it!

--A

Exhort answered 6/2, 2013 at 4:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.