Generated app explained
Once you have tried out the generated app, now it’s time to understand its sources.
Intro
First let’s have a look at the lines of code:
$ cloc .
10 text files.
10 unique files.
2 files ignored.
http://cloc.sourceforge.net v 1.55 T=0.5 s (16.0 files/s, 278.0 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Java 5 14 7 73
XML 2 3 0 35
YAML 1 0 0 7
-------------------------------------------------------------------------------
SUM: 8 17 7 115
-------------------------------------------------------------------------------
As you can see the generated code is minimal, and shouldn’t be too difficult to understand as you will see below.
Resource definition
The resource definition is done in the class <your.main.package>.rest.HelloResource
:
@Component @RestxResource
public class HelloResource {
@GET("/message")
public Message sayHello(String who) {
return new Message().setMessage(String.format(
"hello %s, it's %s",
who, DateTime.now().toString("HH:mm:ss")));
}
}
Let’s decompose its content:
The @Component
annotation declares this class as an injectable component, and the @RestxResource
declares it as a resource.
To get more information on RESTX dependency injection mechanism, check RESTX Factory reference documentation.
Then the sole method defines a resource endpoint thanks to its @GET
annotation (similar annotations are available for other HTTP verbs). The parameter "/message"
tells that this endpoint is mounted on /message relative to RESTX base path (/api
in this case, this is defined in the web.xml
).
The parameter String who
defines a query parameter (a parameter that will be provided after the ?
in the URL).
The content of the method is called when a matching request is received, and constructs a Message
object and returns it, using joda time to get current date / time.
`Message` is not part of RESTX API, it could be any class that Jackson is able to map to JSON. See below to have details on that class in this example.
You can set a breakpoint in this method and run your app in debug to see when this is called. You can also use the open call hierarchy
action of your IDE to see the caller of the method. Here is the generated code:
new StdEntityRoute<Void, hello.domain.Message>("default#HelloResource#sayHello",
readerRegistry.<Void>build(Void.class, Optional.<String>absent()),
writerRegistry.<hello.domain.Message>build(hello.domain.Message.class, Optional.<String>absent()),
new StdRestxRequestMatcher("GET", "/message"),
HttpStatus.OK, RestxLogLevel.DEFAULT) {
@Override
protected Optional<hello.domain.Message> doRoute(RestxRequest request, RestxRequestMatch match, Void body) throws IOException {
securityManager.check(request, open());
return Optional.of(resource.sayHello(
/* [QUERY] who */ checkPresent(request.getQueryParam("who"), "query param who is required")
));
}
}
As you can see the code generated by annotation processing is readable, with comments on the type of parameters. The reader/writer registries are used to determine which reader should be used to process request body into an object and writer is used to convert returned Object into a stream in the response body.
Writing routes manually is also possible, though most of the time using annotation processing is fine, it's good to know that you can always fall back to a more low level code. All you need to do for that is declare a component implementing the RestxRoute interface.
To get more information on RESTX REST endpoint definitions, check RESTX REST endpoints reference documentation.
Domain class: Message
The Message
class is part of the application domain (it’s also called an entity):
public class Message {
private String message;
public String getMessage() {
return message;
}
public Message setMessage(String message) {
this.message = message;
return this;
}
@Override
public String toString() {
return "Message{" +
"message='" + message + '\'' +
'}';
}
}
This is a plain Java bean: the toString
method is not mandatory, and using fluent setter (which returns this
) is not mandatory either.
Binding to JSON is done using the jackson library, check their docs to see how to configure JSON mapping.
You can also use Bean Validation (JSR 303) / Hibernate Validator annotations to add validation to your beans when they are used as body parameters.
Resource Spec
What is called a spec in RESTX is a yaml file describing a resource behaviour, or a set of behaviours chained in a scenario.
In the generated app, you can check the should_say_hello.spec.yaml
file in src/test/resources/specs/hello
:
title: should say hello
given:
- time: 2013-03-31T14:33:18.272+02:00
wts:
- when: GET message?who=xavier
then: |
{"message":"hello xavier, it's 14:33:18"}
The notation follows BDD terminology given
when
then
(wts
stands for When ThenS).
In the given section the state of the system before the HTTP requests is described. In this case we only specify the time in ISO format.
Then a list of when
then
pairs follows, the when
specify HTTP request, the then
HTTP response.
This spec is used for 2 things:
- example in the API docs
- integration test
Because RESTX app follows REST principles, the server has no conversation state. Therefore any HTTP request can be tested in isolation.
The principle of scenario is there mainly to avoid repeating the `given` part too frequently, or also to be able to verify that the system state change after an HTTP request, for example issue a `GET` after a `PUT` to verify that the new resource representation has been stored.
To get more information on RESTX spec concept and related features, check RESTX Specs reference documentation.
Resource Spec Test
To actually be able to run this spec as a test, it is necessary to write a JUnit test to run it:
@RunWith(RestxSpecTestsRunner.class)
@FindSpecsIn("specs/hello")
public class HelloResourceSpecTest {
/**
* Useless, thanks to both @RunWith(RestxSpecTestsRunner.class) & @FindSpecsIn()
*
* @Rule
* public RestxSpecRule rule = new RestxSpecRule();
*
* @Test
* public void test_spec() throws Exception {
* rule.runTest(specTestPath);
* }
*/
}
As you can see this code is very basic thanks to provided runner and @FindSpecsIn
annotation. The comments also show how to use a JUnit rule to do pretty much the same thing but more programmatically.
In either cases the test will start a server on a free port, and verify the spec (i.e. issue HTTP requests and verify the results) against it.
To get more information on RESTX spec concept and related features, check RESTX Specs reference documentation.
AppServer
The AppServer
class is the class used to run the app as a standard Java app.
public class AppServer {
public static final String WEB_INF_LOCATION = "src/main/webapp/WEB-INF/web.xml";
public static final String WEB_APP_LOCATION = "src/main/webapp";
public static void main(String[] args) throws Exception {
int port = Integer.valueOf(Optional.fromNullable(System.getenv("PORT")).or("8080"));
WebServer server = new JettyWebServer(WEB_INF_LOCATION, WEB_APP_LOCATION, port, "0.0.0.0");
/*
* load mode from system property if defined, or default to dev
* be careful with that setting, if you use this class to launch your server in production, make sure to launch
* it with -Drestx.mode=prod or change the default here
*/
System.setProperty("restx.mode", System.getProperty("restx.mode", "dev"));
System.setProperty("restx.app.package", "hello");
server.startAndAwait();
}
}
All it does is launch an embedded server (Jetty in this particular case, but Tomcat and SimpleFramework server are also supported).
If you prefer to run your JavaEE web container of choice separately and use standard deploy mechanism, no problem, the generated app is already configured to be packaged as a standard war.
AppModule
The AppModule
class is defined like this:
@Module
public class AppModule {
@Provides
public SignatureKey signatureKey() {
return new SignatureKey("4f768f23-703e-4268-9e9e-51d2e052b6a1 4082747839477764571 MyApp myapp".getBytes(Charsets.UTF_8));
}
@Provides
@Named("restx.admin.password")
public String restxAdminPassword() {
return "qwerty";
}
@Provides
@Named("app.name")
public String appName() {
return "MyApp";
}
}
This class is mandatory to provide at least a SignatureKey
used to sign content sent to the clients. The string is used as salt, it can be any content, but make sure to keep it private.
The @Module
annotation indicates that this class is used as a RESTX module, able to define a set of components.
The @Provides
annotation on the signatureKey
method is a way to define a component instanciation programmatically. This kind of method can take arbitrary parameters, injected by the RESTX factory.
To get more information on RESTX dependency injection mechanism, check RESTX Factory reference documentation.
In the AppModule
, you will be able to define lots of Application scoped objects, such as :
- An admin password, which will be used to authenticate on the ` RESTX Administration Console`
- An application name, which will be used in different ways, particularly by suffixing RESTX
Cookies names
web.xml
This file is the standard JavaEE web descriptor. It’s used to configure the RESTX servlet:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0" metadata-complete="true">
<servlet>
<servlet-name>restx</servlet-name>
<servlet-class>restx.servlet.RestxMainRouterServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>restx</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
</web-app>
This file is not needed if you use SimpleFramework integration rather than JavaEE web container.
RESTX module descriptor
RESTX uses its own module descriptor format, which is build tool agnostic. The file is called md.restx.json
:
{
"module": "myapp:myapp:0.1-SNAPSHOT",
"packaging": "war",
"properties": {
"java.version": "1.7",
"restx.version": "1.0.0"
},
"dependencies": {
"compile": [
"io.restx:restx-core:${restx.version}",
"io.restx:restx-security-basic:${restx.version}",
"io.restx:restx-core-annotation-processor:${restx.version}",
"io.restx:restx-factory:${restx.version}",
"io.restx:restx-factory-admin:${restx.version}",
"io.restx:restx-monitor-admin:${restx.version}",
"io.restx:restx-server-jetty:${restx.version}",
"io.restx:restx-apidocs:${restx.version}",
"io.restx:restx-specs-admin:${restx.version}",
"io.restx:restx-admin:${restx.version}",
"ch.qos.logback:logback-classic:1.0.13"
],
"test": [
"io.restx:restx-specs-tests:${restx.version}",
"junit:junit:4.11"
]
}
}
This file is used at build time only, its the source used by RESTX to:
- generate Maven POM or Ivy files
- download dependencies (with
restx deps install
or forrestx app run
) - manage app dependencies (with
restx deps install
command)
The module descriptor isn't used at runtime, you can get rid of it if you prefer to manage building, running and deploying your app on your own.
The file format is pretty straightforward to understand if you are familiar with Maven, Ivy or similar build / dependency management tools.
logback.xml
This file is used to configure logging. This is maybe one of the more complex generated files, depending on your experience with logback configuration.
Feel free to adjust it to your own needs, but if you are not confortable with log configuration it provides a reasonnable configuration both for development and production.
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<property name="LOGS_FOLDER" value="${logs.base:-logs}" />
<appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOGS_FOLDER}/errors.log</File>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>%d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGS_FOLDER}/errors.%d.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<if condition='p("restx.mode").equals("prod")'>
<then>
<!-- production mode -->
<appender name="appLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOGS_FOLDER}/app.log</File>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGS_FOLDER}/app.%d.log</fileNamePattern>
<maxHistory>10</maxHistory>
</rollingPolicy>
</appender>
<appender name="debugFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOGS_FOLDER}/debug.log</File>
<encoder>
<pattern>%d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${LOGS_FOLDER}/debug.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>50MB</maxFileSize>
</triggeringPolicy>
</appender>
<root level="INFO">
<appender-ref ref="debugFile" />
<appender-ref ref="appLog" />
</root>
</then>
<else>
<!-- not production mode -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="appLog" class="ch.qos.logback.core.FileAppender">
<File>${LOGS_FOLDER}/app.log</File>
<encoder>
<pattern>%d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="appLog" />
</root>
</else>
</if>
<!-- clean up container logs -->
<logger name="org.eclipse.jetty.server.AbstractConnector" level="WARN" />
<logger name="org.eclipse.jetty.server.handler.ContextHandler" level="WARN" />
<logger name="org.eclipse.jetty.webapp.StandardDescriptorProcessor" level="WARN" />
<logger name="org.hibernate.validator.internal.engine.ConfigurationImpl" level="WARN" />
<logger name="org.reflections.Reflections" level="WARN" />
<logger name="restx.factory.Factory" level="WARN" />
<!-- app logs - set DEBUG level, in prod it will go to a dedicated file -->
<logger name="test43" level="DEBUG" />
<root level="INFO">
<appender-ref ref="errorFile" />
</root>
</configuration>