-
Notifications
You must be signed in to change notification settings - Fork 6
rest
Fathom-REST is an opinionated & injectable scaffolding for the Pippo Micro Webframework.
Pippo is a small framework for writing RESTful web applications. It provides easy-to-use filtering, routing, template engine integration, localization (i18n), Webjars support, and pretty-time datetime rendering.
Fathom-REST supports Pippo 0.8.x.
Add the Fathom-REST artifact.
<dependency>
<groupId>com.gitblit.fathom</groupId>
<artifactId>fathom-rest</artifactId>
<version>${fathom.version}</version>
</dependency>
<dependency>
<groupId>com.gitblit.fathom</groupId>
<artifactId>fathom-rest-test</artifactId>
<version>${fathom.version}</version>
<scope>test</scope>
</dependency>
YourApp
└── src
├── main
│ ├── java
│ │ ├── conf
│ │ │ └── Routes.java
│ │ └── controllers
│ │ ├── EmployeeController.java
│ │ └── ItemController.java
│ └── resources
│ ├── conf
│ │ ├── messages.properties
│ │ ├── messages_en.properties
│ │ └── messages_ro.properties
│ ├── public
│ │ └── css
│ │ └── custom.css
│ └── templates
│ ├── base.ftl
│ ├── employee.ftl
│ ├── employees.ftl
│ ├── index.ftl
│ └── login.ftl
└── test
└── java
├── conf
│ └── RoutesTest.java
└── controllers
└── ApiControllerTest.java
!!! Note
This module depends on the value of the application.package
setting. If you have specified an application package then your Routes class must be ${package}/conf/Routes.java
.
The standard Pippo settings are directly configured through your conf/default.conf
resource file.
application {
# The prefix to use for internal cookies generated by Fathom.
cookie.prefix = "FATHOM"
# ISO Language Code, optionally followed by a valid ISO Country Code.
# e.g. application.languages=en,de,es,fr,ru
languages = [en, de, es, fr, ro, ru]
}
By default, Fathom-REST will serve all requests to /
. You may specify a different basepath in your conf/default.conf
resource file.
servlets {
fathom.rest.RestServlet = "/mypath"
}
By default, Fathom-REST will log detailed information about your registered content-type engines and routes. You may control this logging.
rest {
# List content-type engines in the log
engines.log = true
routes {
# List ordered routes in the log
log = true
# Include the controller method or handler name
logHandlers = true
# Specify the max length of the logged route. If any logged route exceeds
# this limit, all routes will be logged on two lines instead.
maxLineLength = 120
}
}
Almost everything you can read in the official Pippo documentation applies to Fathom-REST.
Fathom-REST takes a slightly different approach to URL mapping compared to upstream Pippo.
In Pippo, the Application class is the core component and is used to register everything.
In Fathom-REST the Pippo Application is an internal component and not meant to be accessed directly. Filters, Routes, and Controllers are registered through the conf/Routes.java
class.
Many of the tricks in Fathom-REST have been moved into upstream Pippo and that process will continue for any feature that makes sense to be upstream.
Route registration in Fathom-REST is almost identical to the process in Pippo Routes.
package conf;
public class Routes extends RoutesModule {
@Inject
EmployeeDao employeeDao;
@Override
protected void setup() {
// add a filter for all requests to stamp some diagnostic headers
ALL("/.*", (ctx) -> {
ctx.setHeader("app-name", getSettings().getApplicationName());
ctx.setHeader("app-version", getSettings().getApplicationVersion());
});
// add a handler for the root page and report it's usage through Metrics
GET("/", (ctx) -> {
int count = employeeDao.getAll().size();
ctx.text().send("Employee Phonebook\n\nThere are {} employees.", count);
});
// add all annotated controllers found in the controller package
addControllers();
// add all controllers found in the following packages
addControllers(V1Controller.class.getPackage(),
V2Controller.class.getPackage(),
V3Controller.class.getPackage());
}
}
You may name your routes. This information is used in the registered routes table displayed at startup and during runtime logging of route dispatching.
GET("/", (ctx) -> ctx.text().send("Hello World!")).named("Home Page");
It's very easy to collect Metrics data about your routes.
GET("/", (ctx) -> ctx.text().send("Hello World!")).meteredAs("HelloWorld!");
You may want to register a route only for one or more runtime modes.
ALL("/.*", (ctx) -> {
ctx.setHeader("app-name", getSettings().getApplicationName());
ctx.setHeader("app-version", getSettings().getApplicationVersion());
}).modes(Mode.DEV, Mode.TEST);
You can optionally declare the response content-type with a URI suffix by using the contentTypeSuffixes
or requireContentTypeSuffixes
methods.
The suffixes are determined by parsing the content-type declarations of your registered ContentTypeEngines.
// Register a route that optionally respects a content-type suffix
// e.g. /executive/54
// /executive/54.json
// /executive/54.xml
// /executive/54.yaml
GET("/employee/{id: [0-9]+}", (ctx) -> ctx.send(employee))
.contentTypeSuffixes("json", "xml", "yaml").;
// Register a route that requires a content-type suffix
// e.g. /executive/54.json
// /executive/54.xml
// /executive/54.yaml
GET("/executive/{id: [0-9]+}", (ctx) -> ctx.send(employee))
.requireContentTypeSuffixes("json", "xml", "yaml").;
!!! Note
If you specify your parameter without a regex pattern (e.g. {name}
) the value of name will include any suffix unless you require the suffix.
Fathom-REST can be used like standard controllers in Pippo but they can also be used in a more declarative fashion.
package controllers;
// To be discoverable, a controller must be annotated with @Path.
@Path("/employees")
@Consumes({Consumes.HTML, Consumes.FORM, Consumes.MULTIPART})
@Produces({Produces.JSON, Produces.XML})
public class MyController extends Controller {
@Inject
EmployeeDao employeeDao;
@GET("/?")
@Produces(Produces.HTML)
public void index() {
List<Employee> list = employeeDao.getAll();
getResponse().bind("employees", list).render("employees");
}
@GET("/all")
@Return(code=200, onResult=List.class)
public List<Employee> getAll() {
List<Employee> list = employeeDao.getAll();
return list;
}
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
public Employee getEmployee(int id) {
// The method parameter name "id" matches the url parameter name "id"
Employee employee = employeeDao.get(id);
if (employee == null) {
getResponse().notFound().send("Failed to find employee {}!", id);
} else {
return employee;
}
}
}
The Consumes annotation declares the content-types which may be accepted by the controller method. You are not required to specify a Consumes annotation but it may make the intenr of your controller methods more clear.
If a Controller method declares Consumes then these types are enforced. One valuable use-case of Consumes is to clearly indicate which methods accept POSTed forms.
The Produces annotation declares the content-types which may be generated by the controller method. You are not required to specify a Produces annotation but it may make the intent of your controller methods more clear and may be a requirement for use of other modules like Fathom-REST-Swagger.
If a controller method Produces multiple types, the first specified Content-Type is used as the default. A negotiation process will automatically occur to determine and attempt to use the Accept-Type preferred by the Request. If one of the preferred Accept-Types match one of the Produces types, the matching Accept-Type will be used for Response generation. If none of the Accept-Types match the Produces types, then the default Content-Type as declared by the Produces annotation will be used for the Response.
You may also declare Produces on the controller class and it will apply, by default, to all methods unless the method specifies it's own Produces annotation.
If your controller method can @Produce
more than one content-type then you may allow overriding the negotiated content-type by the presence of a request URI suffix.
When @ContentTypeBySuffix
is detected, Fathom-REST will automatically append an appropriate regular expression suffix to your method URI for the @Produces
content-types.
For example, if our controller method @Produces({"application/json", "application/x-yaml"})
and we specify @ContentTypeBySuffix
then a request uri of /fruit/1.yaml
would force the Request accept-type to be application/x-yaml
. Standard content-type negotiation will proceed with the result that the YAML ContentTypeEngine will marshall the Response object.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
@Produces({"application/json","application/x-yaml"})
@ContentTypeBySuffix
public Employee getEmployee(int id) {
}
Fathom-REST controllers make use of the -parameters
flag of the Java 8 javac compiler. This flag embeds the names of method parameters in the generated .class files. By default Java 8 does not compile with this flag set so you must specify the -parameters
compiler argument for your build system and your IDE.
!!! Note
You are not required to use this feature. However, if you choose to skip specifying the -parameters
flag then you must annotate each controller method parameter with @Param("name")
. Obviously this will make your controller method signatures more verbose and you will also be double-maintaining parameter name mappings so use of the -parameters
compiler flag is recommended.
Argument Extractors allow you to specify an annotation which instructs Fathom-REST how to provide the value of an argument to your controller method.
Fathom-REST includes the following argument extractors:
Annotation | Use-Case |
---|---|
@Bean |
Extract url parameters, query parameters, and/or form fields to a Java object |
@Body |
Marshall a Request body to a Java type via a Content Type Engine (e.g. JSON, XML) |
@Header |
Extract a Request header to a standard Java type |
@Local |
Extract a temporary value stored locally within the Request/Response Context |
@Param |
Extract an url parameter, query parameter, or form field to a standard Java type |
@Int |
Extract a Request integer parameter with a default value |
@Long |
Extract a Request long parameter with a default value |
@Float |
Extract a Request float parameter with a default value |
@Bool |
Extract a Request boolean parameter with a default value |
Fathom-REST supports implied and explicit argument validation.
- Implicit validation by regular expression pattern matching of the Route path parameters
- Implicit
@Required
validation for@Body
parameters - Explicit
@Required
validation for all other argument types - Explicit
@Min
,@Max
, &@Range
validation for numeric argument types
@POST("/{id: [0-9]+}")
public void renameEmployee(@Min(1) @Max(10) int id, @Required String name) {
}
If you are using annotated controller discovery with @Path
annotations then you are at the mercy of the JVM for registration order of your controller methods.
If you need deterministic controller method registration order you may specify the @Order(n)
annotation on some or all controller methods. Lower numbered methods are registered first.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
@Order(5)
public Employee getEmployee(int id) {
}
You may name your controller routes. This information is used in the registered routes table displayed at startup and during runtime logging of route dispatching. It may also be used by other modules like Fathom-REST-Swagger.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
@Named("Get employee by id")
public Employee getEmployee(int id) {
}
It's very easy to collect Metrics data about your controllers. Simply annotate the methods you want to instrument.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
@Metered
public Employee getEmployee(int id) {
}
You can indicate that a controller response should not be cached by specifying the @NoCache
annotation.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
@NoCache
public Employee getEmployee(int id) {
}
You may use the @Return
annotation to briefly declare method-specific responses.
This has a few benefits.
- it allows you to implement the logic of your controller without directly relying on the request/response/context objects.
- it makes the intended results of your controller method execution clear
- it facilitates api documentation generators or other static analysis tools
- it allows you to send localized messages on error responses based on the
Accept-Language
of the request - it allows you to declare returned headers, validate them, and optionally inject default values
@GET("/{id}")
@Return(code=200, description="Employee retrieved", onResult=Employee.class)
@Return(code=400, description="Invalid id specified", onResult=ValidationException.class)
@Return(code=404, description="Employee not found", descriptionKey="error.employeeNotFound")
@Return(code=404, description="Employee not found", descriptionKey="error.employeeNotFound")
public Employee getEmployee(@Min(1) @Max(5) int id) {
Employee employee = employeeDao.get(id);
return employee;
}
@POST("/login")
@Return(code=200, description="Login successful", headers={ApiKeyHeader.class})
public void login(@Form String username, @Form @Password String password) {
Account account = securityManager.check(username, password);
getContext().setHeader(ApiKeyHeader.NAME, account.getToken());
}
If your controller method declares a non-void return type you must declare a @Return
annotation for this response.
@GET("/{id: [0-9]+}")
@Return(code=200, onResult=Employee.class)
public Employee getEmployee(int id) {
return anEmployee
}
Your controller class may need one or more settings to function and you may specify them as annotated requirements.
Each required setting must be present in the runtime profile configuration and must have a non-empty value otherwise the controller class will not be registered.
@Path("/secret")
@RequireSetting("allow.secret")
public SecretController extends Controller {
}
Your controller method may need one or more settings to function and you may specify them as annotated requirements.
Each required setting must be present in the runtime profile configuration and must have a non-empty value otherwise the controller method will not be registered.
@GET("/secret")
@Produces(Produces.HTML)
@RequireSetting("allow.secret")
public void secret() {
getResponse().render("secret_page");
}
You might only want to register a controller class in a particular runtime mode. This is easily accomplished by using one or more of the mode-specific annotations: @DEV
, @TEST
, and @PROD
.
@Path("/debug")
@DEV @TEST
public DebugController extends Controller {
}
You might only want to register a controller method in a particular runtime mode. This is easily accomplished by using one or more of the mode-specific annotations: @DEV
, @TEST
, and @PROD
.
@GET("/debug")
@Produces(Produces.HTML)
@DEV @TEST
public void debug() {
getResponse().render("debug_page");
}
All standard Pippo template engines are supported.
Engine | Artifact |
---|---|
Freemarker | ro.pippo:pippo-freemarker |
Jade | ro.pippo:pippo-jade |
Pebble | ro.pippo:pippo-pebble |
Trimou | ro.pippo:pippo-trimou |
Groovy | ro.pippo:pippo-groovy |
Velocity | ro.pippo:pippo-velocity |
All standard Pippo content-type engines are supported.
Content-Type | Engine | Artifact |
---|---|---|
XML | JAXB | ro.pippo:pippo-jaxb |
XML | XStream | ro.pippo:pippo-xstream |
JSON | GSON | ro.pippo:pippo-gson |
JSON | FastJSON | ro.pippo:pippo-fastjson |
JSON | Jackson | ro.pippo:pippo-jackson |
YAML | SnakeYAML | ro.pippo:pippo-snakeyaml |
CSV | CommonsCSV | ro.pippo:pippo-csv |