SerfJ provides an MVC architecture, but it doesn't do anything with models, its main characters are controllers, those controllers are managed by REST requests.
Controllers represent application's resources, so the way to send messages to those resources is through REST-like requests. When a request is attended by the main servlet (net.sf.serfj.RestServlet), a resource (controller) is searched and is asked if it's able to answer the request. In that case, an action is executed (a controller's method), and a response is served to the client. The response can be a page, a serialized object, or an HTTP status code 204 that means no content.
Since REST requests control the flow of SerfJ's applications, it's very important to read this section. However, concepts explained below are simple, so everything will be easy to understand.
The HTTP methods supported by SerfJ are:
For example, if you want to present a page that has a form for updating or creating resources, you need to send a GET request, not a PUT request. But the submit button (whose intention is to update some information) will have to send a PUT request.
The patterns for REST requests are:
Notice that resource's name must be plural, and that identifiers must start with a number. But SerfJ is able to parse URLs with nested resources:
If you finish your URL with an extension, then the result won't be a page but a serialized object. Thus, depending on the extension used, you can receive an object serialized as JSON, XML or so:
Depending on the HTTP's method used the same URL will call different controller's methods:
HTTP Method | URL | Controller's method | View | Meaning |
---|---|---|---|---|
GET | /accounts | index() | index | Show every resource |
POST | /accounts | create() | create | Create a new resource |
GET | /accounts/1 | show() | show | Show a resource with ID 1 |
PUT | /accounts/1 | update() | update | Update a resource with ID 1 |
DELETE | /accounts/1 | delete() | delete | Delete a resource with ID 1 |
GET | /accounts/1/newResource | newResource() | new | Show a form to create a new resource |
GET | /accounts/1/edit | edit() | edit | Show a form to update a resource with ID 1 |
Controllers are the main character in SerfJ, REST requests are dispatched to them, and they answer those requests. Answers could be a page, a serialized object, or nothing (an HTTP code).
There are two ways to write a controller (there will be more in next SerfJ versions), extending net.sf.serfj.RestController class, or even writing a JavaBean. Last case is weird because the controller will not be able to do some actions like getting parameters from the request, or redirect to another page, or send objects to JSP pages.
For now, extending RestController class is the best way to write a controller. Methods that attend requests musn't have parameters, but can return objects and throw exceptions. For example, if we need a controller to attend request for /accounts, we have to write a class like this:
public class Account extends RestController { }
There are several annotations that tell the framework which HTTP method is accepted by a controller's method.
Also, there is another one, @DoNotRenderPage that tells the framework that after executing a controller's method, no page will be rendered, but a HTTP 204 code will be answered. Since a method that returns an object doesn't render a page as a result, it doesn't need to be annotated with @DoNotRenderPage. However, a method that doesn't return anything (a void method) but developer doesn't want to render a page as a result, does have to be annotated.
Obiously, the class we wrote before won't do anything, it won't answer any request. Let's write more stuff. If we need a method for updating some information from an account (PUT /accounts/1), we could write a method like this:
public class Account extends RestController { @PUT public void updateAccount() { String accountId = this.getId(); // Do something to update the account } }
We see that the method can recover account's identifier from the request with getId(String) method. Let's see how we get others identifiers if requests have nested resources (PUT /banks/1/accounts/2).
public class Account extends RestController { @PUT public void updateAccount() { String accountId = this.getId(); String bankId = this.getId("bank"); // Do something to update the account from the bank received } }
If you put some parameters in the request, the method can get them too. Params can travel in the query string, or within the request.
public class Account extends RestController { @PUT public void updateAccount() { String accountId = this.getId(); String someInfo = this.getStringParam("some_info_param_name"); // Do something to update the account } }
But what about receiveing objects that are not strings?.
public class Account extends RestController { @PUT public void updateAccount() { String accountId = this.getId(); String someInfo = this.getStringParam("some_info_param_name"); Balance balance = (Balance) this.getParam("balance")); } }
Well, now we know how to get parameters from requests, but sometimes we'll need to send objects to a JSP, for example. Obviously those objects must implement java.io.Serializable.
public class Account extends RestController { @PUT public void updateAccount() { Account account = // some code to get an account this.putParam("my_object_param_name", account); } }
It will be very common that methods return objects. As we have seen in section 1.1, those methods must be called with REST URLs that end with an extension. The extension will point the framework what serializer must be used to make the response. SerfJ provides three different serializers (XML, JSON or Base64) for three different extensions (.xml, .json, .base64), but developers can write their own serializers(see section 3).
For example, a method that responds to URLs like /accounts/1/balance.xml could be written in two ways. Let's see the first way to do it:
public class Account extends RestController { @GET public Balance balance() { Balance balance = new Balance(); return balance; } }
This method will always try to return an object, how the object is serialized depends on the extension used. But may be we need a method that returns an object when an extension is received, or render a page in other cases:
public class Account extends RestController { @GET public void balance() { Balance balance = new Balance(); if (this.getSerializer() != null) { this.serialize(balance); } else { // This is optional, we need it only if we want to send the object to the page this.putParam("balance", balance); this.renderPage("balance"); } } }
In this case, if there is any object serialized, the framework will write it in the response.
Controller's methods will always render a page after their execution unless the method has a returning object or is annotated with @DoNotRenderPage. The page it will try to render must be in a subdirectory, whose name is the resource's name, also must be under the directory defined in views.directory property (by default it's views). Pages must have .jsp, .html or .htm extensions. For example:
Controller | Method | View |
---|---|---|
Account | void index() | views.directory/account/index |
Account | void show() | views.directory/account/show |
Account | void newResource() | views.directory/account/new |
Bank | void edit() | views.directory/bank/edit |
Car | void update() | views.directory/car/update |
Account | void create() | views.directory/account/create |
Account | void delete() | views.directory/account/delete |
Account | void myMethod() | views.directory/account/myMethod |
Account | Object myMethod() | Returns a serialized object |
Account | @DoNotRenderPage void myMethod() | Returns an HTTP 204 code |
That's how the framework renders pages by default, but there are ways to render other pages. There are three methods to render pages:
URLs may end with different extensions. The default extensions are .xml, .json, .base64 and .file. If an URL ends with an extension, the framework will try to serialize the response in the format specified using a Serializer (read section 3). Some examples:
HTTP Method | URL | Controller's method | View | Meaning |
---|---|---|---|---|
GET | /accounts.xml | index() | N/A | Send every account serialized in XML |
GET | /accounts/1.xml | show() | N/A | Sends account 1 in XML |
GET | /documents/1.file | show() | N/A | Download document with ID 1 |
GET | /songs/1.mp3 | play() | N/A | Download a MP3 song with ID 1 (you'd need to implement a Mp3Serializer extending FileSerializer) |
As of version 0.4.0 is possible to serve files using the extension .file in the requests. But you must set the location for the file in order to get SerfJ reading it.
@GET @DoNotRenderPage public void download() { // Set the file location this.getResponseHelper().setFile(new File("path_to_a_file/avatar_sabreman.png")); // SerfJ will download the file }
The default implementation set as content type application/octect-stream so if you need to send a different one using the same .file extension, you must set the content type along with the file:
@GET @DoNotRenderPage public void download() { // Set the file location this.getResponseHelper().setFile(new File("path_to_a_file/avatar_sabreman.png")); this.getResponseHelper().setContentType("audio/mpeg3"); // SerfJ will download the file }
On the other hand if you want to use a different extension like .mp3, .txt, .pdf or so on, you must implement your own file serializer. For example, an implementation for .mp3 extension would be:
package net.sf.serfj.serfj_sample.serializers; import net.sf.serfj.serializers.FileSerializer; /** * Serializer for MP3 audio files.
*/ public class Mp3Serializer extends FileSerializer { /** * Content type that will be used in the response. */ public String getContentType() { return "audio/mpeg3"; } }
Although SerfJ tries to avoid that controllers have dependencies with javax.servlet package, sometimes the developer could be limited by the framework functionality, so as of version 0.4.0, net.sf.serfj.RestController has a way to access javax.servlet.ServletContext, javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse.
ServletContext context = this.getResponseHelper().getContext(); HttpServletRequest request = this.getResponseHelper().getRequest(); HttpServletResponse response = this.getResponseHelper().getResponse();
When a REST request arrives, if it has an extension before the query string, a serializer capable to serialize the response is searched (read section 5 to learn how resources are searched). SerfJ provides serializers for XML, JSON, Base 64 and FILES (.xml, .json ,.base64 or .file extensions), but developers can make their owns, and can make others to treat different extensions.
It's very easy to develop new serializers, you only need to implement net.sf.serfj.serializers.ObjectSerializer interface, that is so simple that it isn't needed more explanations than its own Javadoc:
package net.sf.serfj.serializers; /** * Interface for Serializers. * * @author Eduardo Yáñez */ public interface ObjectSerializer { /** * Serialize an object in the format that the implementation requires. * * @param object * Object to serialize. * @return a String with the object serialized. */ public String serialize(Object object); /** * Deserialize an object from the format that the implementation requires to * Java Object. * * @param string * String representation of the object to deserialize. * @return an Object. */ public Object deserialize(String string); /** * Content type that will be used in the response. */ public String getContentType(); }
For downloading files there is a net.serfj.serializers.FileSerializer that serves any file with a content type of "application/octect-stream". Also you can define your own files serializers extending the class and overriding the FileSerializer.getContentType() method. For example, a serializer for serving MP3 files could be:
package net.sf.serfj.serfj_sample.serializers; import net.sf.serfj.serializers.FileSerializer; /** * Serializer for MP3 audio files.
*/ public class Mp3Serializer extends FileSerializer { /** * Content type that will be used in the response. */ public String getContentType() { return "audio/mpeg3"; } }
The class' name must start with the extension that the serializer is made for, followed by the resource's name, and must end with Serializer:
Other option is having the serializers within a 'package.hierarchy.serializers' package. Then the class' name must start with the extension that the serializer is made for, and must end with Serializer. This way you can implement generic serializers for every model:
This framework tries to follow the concept of Convention over Configuration, so to use it's almost unnecessary to configure it. Of course it needs to be configured, but only a little bit. However, if developers want to get it running better, they can set up several configuration properties in order to avoid SerfJ to do predictions to find some resources.
SerfJ has only one configuration file serfj.properties that has to be at /config directory within the classpath. It only needs a main.package property to work. This property must point to the package where SerfJ will look for controllers and serializers, but it doesn't mean that all controllers and serializers must be in that package, the way the framework look for resources is explained in the next section.
Here you have an example for a configuration file:
# Main package where looking for classes (controllers, serializers) main.package=net.sf.serfj.test # Directory with the JSP, HTML, etc... # views.directory=src/test/webapp/WEB-INF/views # Packages style. # There are 3 types of styles: # # FUNCTIONAL/functional # All classes in the same package by functionality # net.sf.serfj.controllers.Bank # net.sf.serfj.controllers.Order # net.sf.serfj.serializers.JsonBankSerializer # net.sf.serfj.serializers.XmlOrderSerializer # # FUNCTIONAL_BY_MODEL/functional_by_model # Classes by model and functionality # net.sf.serfj.bank.controllers.Bank # net.sf.serfj.bank.serializers.JsonSerializer # net.sf.serfj.order.controllers.Order # net.sf.serfj.order.serializers.JsonSerializer # # MODEL/model # All classes in the same package by model # net.sf.serfj.bank.Bank # net.sf.serfj.bank.JsonSerializer # net.sf.serfj.order.Order # net.sf.serfj.order.JsonSerializer # # OFF/off/Leave blank # Leave blank or don't define it. The library will try different ways to find # the classes in the following order: FUNCTIONAL, FUNCTIONAL_BY_MODEL, MODEL # This method is less efficient, so it's better defininig some one. #packages.style= # You can change the name of controllers' package if there is one. # Default is 'controllers'. #alias.controllers.package=controllers #alias.controllers.package=ctrl #alias.controllers.package=controllers.main # You can change the name of serializers' package if there is one. # Default is 'serializers'. #alias.serializers.package=utils #alias.serializers.package=utils.serializers # Suffixes used # If you don't want any suffix set OFF/off as value # For controllers default is 'OFF' #suffix.controllers=Controller # Controller class must named net.sf.serfj.bank.BankController instead of net.sf.serfj.bank.Bank # For serializers defaults is 'Serializer' #suffix.serializer=
There are three ways in which SerfJ look for resources, they are named:
If no one is defined in the configuration file, then the framework uses each one in that order to find resources. So if a developer wants to tell SerfJ how it must search for resources, he must define the packages.style property.
If it is looking for a controller, then the qualified class name will be:
main.package + "." + alias.controllers.package + "." + capitalized(singularized(resource name)) + suffix.controllers.
So having this configuration:
The framework will look for a controller with the next qualified name:
net.sf.serfj.tests.controllers.BankCtrl
If the resource searched is a serializer, then a prefix is used for the class name. The prefix is not configurable, it'll be the extension of the request capitalized (Xml, Pdf, Json, File, etc.). For example, having this configuration:
Looking for a Pdf serializer for accounts, the qualified class name will be:
net.sf.serfj.tests.serializers.PdfAccountSerializer
Looking for a Pdf serializer for whatever model, the qualified class name will be:
net.sf.serfj.tests.serializers.PdfSerializer
Note: Generic serializers only are able to be defined within the Functional style.
In this strategy, resource name is singularized and appended to the main.package property. If it is looking for a controller, then the qualified class name will be:
main.package + "." + singularized(resource name) + "." + alias.controllers.package + "." + capitalized(singularized(resource name)) + suffix.controllers.
So having this configuration properties defined:
The framework will look for a controller with the next qualified name:
net.sf.serfj.tests.bank.ctrl.Bank
If the resource searched is a serializer, then a prefix is used for the class name. The prefix is not configurable, it'll be the extension of the request capitalized (Xml, Pdf, Json, Base64, etc.). For example, having this configuration properties defined:
Looking for a Pdf serializer for accounts, the qualified class name will be:
net.sf.serfj.tests.account.serial.PdfAccountSerializer
In this strategy, resource name is singularized and appended to the main.package property, but the alias is not used. If it is looking for a controller, then the qualified class name will be:
main.package + "." + singularized(resource name) + "." + capitalized(singularized(resource name)) + suffix.controllers.
So having this configuration properties defined:
The framework will look for a controller with the next qualified name:
net.sf.serfj.tests.bank.Bank
If the resource searched is a serializer, then a prefix is used for the class name. The prefix is not configurable, it'll be the extension of the request capitalized (Xml, Pdf, Json, Base64, etc.). For example, having this configuration:
Looking for a CSV serializer for accounts, the qualified class name will be:
net.sf.serfj.tests.account.CsvAccountSerializer
SerfJ provides a class net.sf.serfj.client.Client in order to do REST requests. It has four public methods (get, post, put and delete) used to do requests. The interface is very simple, so the Javadoc should be enough to use this class.
Copyright © 2010-2012 Eduardo Yáñez Parareda, Licensed under the Apache License, Version 2.0. Apache and the Apache feather logo are trademarks of The Apache Software Foundation.