The REST framework
Requirements
The purpose of eXo REST framework is to make eXo services (i.e. the components deployed inside eXo Container) simple and transparently accessible via HTTP in RESTful manner. In other words those services should be viewed as a set of REST Resources - endpoints of the HTTP request-response chain, we call those services ResourceContainers. image: Simplified working model Taking into account HTTP/REST constraints, it is considered that Resources (naturally mapped as Java methods) contained in ResourceContainer conform with the following conditions:- they are uniquely identifiable in the same way as it is specified for HTTP i.e. using URI
- they can accept data from an HTTP request using all possible mechanisms, i.e. as a part of an URL, as URI parameters, as HTTP headers and body
- they can return data to an HTTP response using all possible mechanisms, i.e. as HTTP status, headers and body
- the framework should be as much JSR-311 compatible as possible for the time the Specification is in draft and fully compatible after the standard's release
- the ResourceContainer components should be deployable and accessible in the same way as an ordinary service
Implementation
In our REST architecture implementation, an HTTPServlet is the front-end for the REST engine. In this RESTful framework, endpoints for HTTP request are represented by java classes (ResourceContainers). All ResourceContainers are run as components of an ExoContainer, so they are configured within the same configuration file. ResourceBinder and ResourceDispatcher are two other important components of the REST engine. ResourceBinder deals with binding and unbinding ResourceContainer. ResourceDispatcher dispatches REST requests to ResourceContainer. ResourceBinder and ResourceDispatcher are also components of the ExoContainer.ResourceContainer
A ResourceContainer is an annotated java class. Annotations must at least describe URLs and HTTP methods to be served by this container. But they may also describe other parameters, like produced content type or transformers. Transformers are special Java classes, used to serialize and deserialize Java objects. A very simple ResourceContainer may look like this:@HTTPMethod("GET") @URITemplate("/test1/{param}/test2/") public Response method1(@URIParam("param") String param) { //... //... Response resp = Response.Builder.ok().build(); return resp; }
A client sends a request, after some operations, the HTTP request is parsed into three parts: the HTTP request stream, which consists of the body of the request, HTTP headers and query parameters. Then ResourceDispatcher gets this request. During the start of ExoContainers ResourceBinder collects all available ResourceContainers and after the start passes the list of ResourceContainers to the ResourceDispatcher. When ResourceDispatcher gets the request from a client it tries to search for a valid ResourceContainer. For this search ResourceDispatcher uses @HTTPMethod, @URITemplate and @ProducedMimeType annotations. The last one is not always necessary, if it is not specified this is set in / (all mime types). When ResourceDispatcher found the "right" ResourceContainer it tries to call a method with some parameters. ResourceDispatcher will send only parameters which ResourceContainer requests. In the code example above the parameter is a part of the requested URL between "/test1/" and "/test2/". ResourceContainer methods can consist only of some well-defined parameters.
See the next rules:
- Only one parameter of a method can be not annotated. This parameter represents the body of the HTTP request.
- All other parameters should be of java.lang.String types or must have a constructor with the java.lang.String parameter and must have annotation.
- If the parameter which represents the HTTP request body is present, then in the annotation @InputTransformer a java class must be defined that can build the requested parameter from java.io.InputStream. About transformation see below. After request processing ResourceContainer creates Request. Request is a special Java object which consists of HTTP status, a response header, an entity, a transformer. The entity is a representation of the requested resource. If Response has an entity it also must have OutputEntityTransformer. OutputEntityTransformer can be set in a few different ways.
Response
Response is created by Builder. Builder is an inner static Java class within Response. Builder has some preset ways to build Response, for example (see code example above). The method ok() creates Builder with status 200 (OK HTTP status) and ok() returns again a Builder object. In this way a developer can again call other Builder methods. For example://... Document doc = .... Response response = Response.Builder.ok().entity(doc, "text/xml").transformer(new XMLOutputTransformer()).build();
//... @HTTPMethod("GET") @URITemplate("/test/resource1/") @InputTransformer(XMLInputTransformer.class) @OutputTransformer(XMLOutputTransformer.class) public Response method1(Document inputDoc) { //... Document doc = .... Response response = Response.Builder.ok(doc, "text/xml").build(); return response; }
Transformer
All transformers can be created by EntityTransformerFactory. Transformers can be divided in two groups. Transformers from the first one extend the abstract class InputEntityTransformer. InputEntityTransformer implements the interface GenericInputEntityTransformer, and GenericInputEntityTransformer extends the interface GenericEntityTransformer. This architecture gives the possibility to use one class EntityTransformerFactory for creating both types of transformers (input and output). At first we will speak about InputEntityTransformer.InputEntityTransformer
All classes which extend the abstract class InputEntitytransformer must override the method ObjectreadFrom(InputStream entityDataStream). This method must return an object with the same type as ResourceContainer requires in method parameters (one not annotated parameter). This mechanism works in the following way. For example, a class which extends InputEntityTransformer will be called SimpleInputTransformer. So, SimpleInputTransformer must have a simple constructor (without any parameters). SimpleInputTransformer also has two methods void setType(Class entityType) and Class getType(). Those methods are described in InputEntityTransformer. And, as we said above, SimpleInputTransformer must override the abstract method Object readFrom(InputStream entityDataStream). So, a developer needs to create a class which can build a Java Object from InputStream and then create annotation to ResourceContainer or some method in ResourceContainer. This annotation must define the type of InputEntityTransformer. Here is the code of the annotation InputTransformer.//...
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface InputTransformer {
Class<? extends InputEntityTransformer> value();
}//... InputEntityTransformer transformer = (InputEntityTransformer) factory_.newTransformer(resource.getInputTransformerType()); transformer.setType(methodParameters[i]); params[i] = transformer.readFrom(request.getEntityStream()); //... Response response = (Response) resource.getServer().invoke(resource.getResourceContainer(), params);
OutputEntityTransformer
OutputEntityTransformer can be used to serialize the Object which represents the requested resource to OutputStream. OutputEntityTransformer, in future we will call it SimpleOutputTransformer, can be defined in a few ways. Now some more words about transformation. The RESTful framework has two multi-purpose input/output transformers. One of them is JAXB(Input/Output)Transformer, and another one is Serializable(Input/Output)Transformer. The first one uses JAXB API for serializing and deserializing an object. Serializable (Input/Output)Transformer uses the own methods of an entity for serialization and deserialization. Here is an example://... public class SimpleSerializableEntity implements SerializableEntity { String data; public SimpleSerializableEntity() { } public void readObject(InputStream in) throws IOException { StringBuffer sb = new StringBuffer(); int rd = -1; while((rd = in.read()) != -1) sb.append((char)rd); data = sb.toString(); } public void writeObject(OutputStream out) throws IOException { out.write(data.getBytes()); } }
Binding and unbinding components (ResourceContainers)
ResourceBinder takes care of binding and unbinding components which represent ResourceContainer. All ResourceContainers must be defined as ExoContainer components in configuration files, for example:<component>
<type>org.exoplatform.services.rest.samples.RESTSampleService</type>
</component>public class SimpleResourceContainer implements ResourceContainer { @HTTPMethod("GET") @URITemplate("/services/{id}/service1/") @InputTransformer(StringInputTransformer.class) @OutputTransformer(StringOutputTransformer.class) public Response method1(String data, @URIParam("id") String param) { // ... } }
ResourceDispatcher
Now let's talk about the features of ResourceDispatcher. ResourceDispatcher is the main part of the RESTful engine. Above we said some words about transformation and the role of ResourceDispatcher in this process. Now we are ready to talk about annotated method parameters. In one of the code examples above you could see the next constructionpublic Response method1(String data, @URIParam("id") String param) {
@Target(value={PARAMETER})
@Retention(RUNTIME)
public @interface URIParam {
String value();
}<component> <type>org.exoplatform.services.rest.ResourceDispatcher</type> <init-params> <properties-param> <name>context-params</name> <property name="test" value="test_1"/> </properties-param> </init-params> </component>
...method(@ContextParam("test") String p) { System.out.println(p); }
public static final String CONTEXT_PARAM_HOST = "host"; public static final String CONTEXT_PARAM_BASE_URI = "baseURI"; public static final String CONTEXT_PARAM_REL_URI = "relURI"; public static final String CONTEXT_PARAM_ABSLOCATION = "absLocation";
//...
for (int i = 0; i < methodParametersAnnotations.length; i++) {
if (methodParametersAnnotations[i] == null) {
InputEntityTransformer transformer = (InputEntityTransformer) factory
.newTransformer(resource.getInputTransformerType());
transformer.setType(methodParameters[i]);
params[i] = transformer.readFrom(request.getEntityStream());
} else {
Constructor<?> constructor = methodParameters[i].getConstructor(String.class);
String constructorParam = null;
Annotation a = methodParametersAnnotations[i];
if (a.annotationType().isAssignableFrom(URIParam.class)) {
URIParam u = (URIParam) a;
constructorParam = request.getResourceIdentifier().getParameters().get(u.value());
} else if (a.annotationType().isAssignableFrom(HeaderParam.class)) {
HeaderParam h = (HeaderParam) a;
constructorParam = request.getHeaderParams().get(h.value());
} else if (a.annotationType().isAssignableFrom(QueryParam.class)) {
QueryParam q = (QueryParam) a;
constructorParam = request.getQueryParams().get(q.value());
} else if (a.annotationType().isAssignableFrom(ContextParam.class)) {
ContextParam c = (ContextParam) a;
constructorParam = contextHolder_.get().get(c.value());
}
if (methodParameters[i].isAssignableFrom(String.class)) {
params[i] = constructorParam;
} else {
params[i] = (constructorParam != null) ? constructor.newInstance(constructorParam) : null;
}
}
}
Response resp = (Response) resource.getServer().invoke(resource.getResourceContainer(), params);
//...
on 30/05/2008 at 08:49