In the last post we have registered a new RequestHandler class for the request key “/api”. But the handler doesn’t know, which actions should be executed for an incoming request.
We need some resource classes (ERRest called it Controller), which define endpoints (URIs). If the request contains an URI path like such an endpoint, the handler must instantiate the matching resource class and execute the matching method.
The method of the class must be declared for the used HTTP verb (like GET, POST, PUT…), so the handler would choose it to execute. We use also the “Accept” header of the request, to find a matching class method. The method should declare, that it can generate such media type. If not, we will use another method or return “Not Acceptable” as HTTP error code (406).
Some requests will send data within the request body (content). This content has a “Content-Type” header, which should also match the media type the method can handle. There must be a matching method parameter, which awaits this data structure from the request body. If not, we will use another method or return the HTTP status “Unsupported Media Type” (415)
HTTP verb
The HTTP verb of the request we can extract from the given WORequest object. This will be the first step:
protected String getHttpVerbFromMethod(Method method) {
for (Annotation a : method.getAnnotations()) {
if ((a instanceof GET) || (a instanceof POST) || (a instanceof PUT) || (a instanceof DELETE)) {
return a.annotationType().getSimpleName();
}
}
return null;
}
// clazz is the Class object of the resource
for (Method method : clazz.getMethods()) {
String verb = getHttpVerbFromMethod(method);
// do something
}
The verbs can be enhanced, if necessary (like OPTIONS or HEAD). For the moment we only use the defined four verbs.
Media Types
To declare these media types we will use the annotations from JSR311. There is a @Produces annotation, which defines the media types the method can generate and send back to the client (should match the requested “Accept” header). There is a special media type “*/*” which match all media types. If the method defines explicitly a @Produces annotation, it can implicitly also answer all requests for Accept=*/*.
The method can also declare a @Consumes annotation, which defines the media type the method can handle (must match the request’s “Content-Type”). The “Content-Type” can also be null (no header defined). If the @Consumes has been declared, but no explicit media type has been set, it assumes implicitly “*/*”. If there is a “Content-Type=*/*” within the request, all methods should match, if there is no “Content-Type”, we could also execute every method. The matching method parameter would contain in this case the value “null”. In all other cases “Content-Type” must exact match @Consumes, to execute the method.
@POST
@Produces("application/vnd.info.phosco.rest.Location.v1+xml")
@Consumes("application/vnd.info.phosco.rest.Location.v1+xml")
public LocationMessage newLocation(LocationMessage xml) {
// store the new Location into database
}
In the example above we await an “application/vnd.info.phosco.rest.Location.v1+xml” (@Consumes) within the request body (as XML). These data contain an incomplete Location, which we use to create a complete new Location within our database backend. If we have create the entity, we will return this newly created entity within the HTTP response (also as XML). It will also have the content type “application/vnd.info.phosco.rest.Location.v1+xml” (@Produces). The class method newLocation() will only be called on POST requests (@POST).
To pre-process the @Produces/@Consumes information, we extract the data during the registration step of the resource class:
protected Set<String> getConsumeMediaTypeFromClassOrMethod(Class<? extends BaseRestResource> clazz, Method method) {
Consumes consumes = method.getAnnotation(Consumes.class);
if (consumes == null) {
consumes = clazz.getAnnotation(Consumes.class);
}
Set<String> mediaTypes = new HashSet<String>();
if (consumes != null) {
mediaTypes.addAll(Arrays.asList(consumes.value())); // remove double entries
return new ArrayList<String>(mediaTypes);
}
mediaTypes.add("*/*");
return new ArrayList<String>(mediaTypes);
}
protected Set<String> getProduceMediaTypeFromClassOrMethod(Class<? extends BaseRestResource> clazz, Method method) {
Produces produces = method.getAnnotation(Produces.class);
if (produces == null) {
produces = clazz.getAnnotation(Produces.class);
}
Set<String> mediaTypes = new HashSet<String>();
if (produces != null) {
mediaTypes.addAll(Arrays.asList(produces.value()));
}
mediaTypes.add("*/*");
return new ArrayList<String>(mediaTypes);
}
// clazz is the Class object of the resource
for (Method method : clazz.getMethods()) {
Set<String> consumes = getConsumeMediaTypeFromClassOrMethod(clazz, method);
Set<String> produces = getProduceMediaTypeFromClassOrMethod(clazz, method);
// do something
}
We will use the declared media types of the method, or if there isn’t one defined, will use the class media types. Both annotations allow multiple media types, so we must prevent double entries.
Resource Path
To differ the resource classes we define the path for the URI with the @Path annotation on every class. This annotation is also defined within JSR311. We can define these annotation on class level or method level.
@Path("/location-list/{location-id}
public class LocationResource extends BaseRestResource {
}
The example defines a path “/location-list/{location-id}” for the whole class. So the request handler will use this resource class for an URI like:
http://127.0.0.1:10002/cgi-bin/WebObjects/api/location-list/24565
The URI will match the @Path after “/api”. Parts of the @Path which are enclosed into “{}” are dynamic parts. They will be set by the client according to the necessary data. We will use them as Id, in the example “24565” will be the Id of the request Location entity. The resource class must query the database for this specific Location and must return the data within the response body. You can directly use the Ids from the database, but it is also possible to hide these information, i.e. by using UUIDs.
Every method can have parameters, which the handler should fill with the correct request-specific values. JSR311 defines a lot of annotations, which define sources for the parameters:
@HeaderParam
It defines a header key name, so the handler should read the HTTP header value and store it into the annotated parameter variable.
@CookieParam
It defines a cookie key name, so the handler should read the cookie value and store it into the annotated parameter variable.
@QueryParam
The annotation declares a name of a query-string parameter (the part after the “?” within the requested URI). The associated value should be stored into the annotated variable. The query string is URLencoded, so we have to decode the value within the handler class.
@PathParam
Here we can define the name of a “dynamic part” of the declared URI (within @Path annotation). In the example above we could use “location-id” as parameter. The name of the @PathParam must match the name of a “{}” part within @Path (case-sensitive!). For the example the annotated variable must i.e. contain “24565”.
@DefaultValue
Every parameter can be annotated with a second annotation, which defines a default value. If the parameter cannot be filled by the request handler (i.e. the header key defined by @HeaderParam doesn’t exist within the current request object), the variable should then contain the declared default value.
The variables can have different types. JSR311 allows primitive types like int, long, boolean, double and so on. Allowed are also classes, which have a constructor with a single String parameter. Classes with a static method valueOf(String) are also possible and List<String> classes. According to the declared method parameter type, the handler will instantiate the correct class.
During the register workflow we cannot store values for the most of the parameters, because they are only known at the request time. But we can pre-process the path parameters. If we find @PathParam annotation at request time, we must look into the @Path annotation to find the right place of the parameter and must extract the matching characters from the request URI. This we could do with Regular Expressions. We would generate a pattern for the @Path declaration of every class/method and the “dynamic parts” would be replaced by RegEx groups. So we can say, that the “location-id” would be the group number 1 and if we use the pattern for the requested URI, we will get the matching substring for the group:
protected PathRegEx buildRegularExpressionFromPath(String path) {
Map<String, Integer> params = new HashMap<>();
int groupNumber = 0;
String str = trim(path, '/');
String res = "";
for (String part : str.split("/")) {
if (part.startsWith("{") && part.endsWith("}")) {
params.put(trim(trim(part, '{'), '}'), ++groupNumber);
part = "(\\d+)"; // allow only numbers as Id, enhance this?
}
res += ("\\/" + part);
}
res = "^" + res.substring(2) + "$";
return new PathRegEx(res, params);
}
protected String getPathFromClassOrMethod(Class<? extends BaseRestResource> clazz, Method method) {
Path path = method.getAnnotation(Path.class);
if (path == null) {
path = clazz.getAnnotation(Path.class);
}
return path == null ? null : trim(path.value(), '/');
}
// clazz is the Class object of the resource
for (Method method : clazz.getMethods()) {
String path = getPathFromClassOrMethod(clazz, method);
PathRegEx regex = buildRegularExpressionFromPath(path);
// do something
}
First we extract the @Path annotation for every class method. If there is no one, we will use the annotation on class level. If the class has also no annotation, we cannot execute an action for this class. The path can contain trailing and leading slashes, so we have to remove them (the request path won’t have them too).
From the path we generate a RegEx pattern. Every “dynamic part” will be replaced with “(\d+)”, so we await only numbers for a path parameter. JSR311 defines more characters ([0-9a-zA-Z], underscore and minus). If we need that, we can enhance it.
“(\d+)” is a group of the regular expression. So if we use a Matcher, we can access the parameters by the group index. Group 0 is always the full match (complete pattern), group 1 is the first parameter, group 2 the second parameter and so on. Depending on the depth of the relations between the objects, there can be more than one group:
/person-list/{person-id}/function-list/{function-id}/location-list/{location-id}
will be:
^person-list/(\d+)/function-list/(\d+)/location-list/(\d+)$
Group 1 matches {person-id}, group 2 matches {function-id} and group 3 matches {location-id}. The example would return the function of a person on a specific location: a physician (person) can be the chief doctor (function) in a hospital (a location) but only an expert witness (function) on a laboratory (other location).
The method buildRegularExpressionFromPath() (see Java code above) stores every parameter name together with its group index within a Map. So we can later find the index to a given the name (the name comes from @PathParam annotation).
Return value
WODirectActions should always return WOActionResults, which is an interface to WOResponse. But we will return the matching message class, which the framework JAXB will transform into an XML representation (response content). This representation will added to the WOResponse together with an HTTP status of 200 (OK). This code we could move into a super class, to prevent a lot of boilerplate code within the resource class.
Preprocessing
How we can handle all these metadata of a class? I think we should extract them during the registering of the resource classes on the request handler. We must announce the resources to the handler within the Application class constructor (btw. we could also automatically scan the WebObjects classpath for resource classes), so we can store the resource classes into a list within the handler. But it is not enough. Take a look on the questions we have to answer during the Request/Response-Loop:
- Does the request URI match the @Path?
- Does the Accept header match the @Produces?
- Does the Content-Type header match the @Consumes?
- Does the HTTP verb match the annotation?
- Does the method use a return value, which JAXB can translate into the media type?
- Does the method have a parameter, which can be filled with the JAXB-translated content of the request?
To speed up the execution of a request on the server, we should pre-process as much as we can during the registration time on application initialization.
The examples above show you, that we should store the metadata for every method. An action method is the smallest entity, the request handler will call. We need the following data:
Class name
Method of the class
Path
Regular expression and group indexes
consuming media type
producing media type
Because every method can define more than one media types, we should store only one media type per registration and have to duplicate the registration for other declared media types.
To find the registered resource very fast we use Map<String, Set<ResourceDescription>>, where String contains a key to find a quick answer to the 6 questions above. So we store a set of all methods, which define the @GET annotation for a key “GET”. This allows us to answer the 4th question very quick:
Set<ResourceDescription> get = registerdVerb.get("GET");
We can get for every question a Set<ResourceDescription> and to find the matching ResourceDescription we build the intersection of the sets. To get a list of all GET methods which also match the requested URI we can do:
Set<ResourceDescription> exp = new HashSet<ResourceDescription>();
for (String regex : this.registeredRegEx.keySet()) {
if (!Pattern.matches(regex, uri)) {
continue;
}
exp.addAll(this.registeredRegEx.get(regex));
}
Set<ResourceDescription> get = registeredVerb.get("GET");
exp.retainAll(get);
The example matches the URI from the request against the registered RegEx pattern, which we have pre-processed from the @Path annotation. If the pattern will match, we store the ResourceDescription within a Set and then we build the intersection with the set of registered resources for the GET verb. The result is a set of ResourceDescriptions which will match both properties, the HTTP verb and the URI.
The next steps build further intersections with the media types of @Produces and @Consumes. At the end we get a small set of ResourceDescription. Actually it should contain only one. We choose the first ResourceDescription and have enough information within to instantiate the resource class and call the action method.
To prevent double registrations of resource classes we implement equals() and hashCode() on ResourceDescription. The implementation of the Set class will do the rest for us.
The advantage of this workflow is the speed. We can find the resource very quick. The disadvantage is the amount of memory we need. We have to handle multiple Map<String, Set<ResourceDescription>>. But the description classes are always the same (immutable), only the memory for the Set/Map classes is necessary.