By three methods we may learn wisdom: First, by reflection, which is noblest; Second, by imitation, which is easiest; and third by experience, which is the bitteREST.
1. The Wisdom Framework
1.1. Build Web applications with Java and JavaScript.
Wisdom is a lightweight framework enforcing a stateless, web-friendly architecture. It’s modular, and dynamic.
Wisdom is based on the NIO and asynchronous framework VertX. It is integrally built on top of OSGi, to enable modularity, and iPOJO, to make dynamism easy as pie.
With Wisdom, you web application is not monolithic anymore, but divided into a set of components deployable on the fly, and enabled / disabled dynamically, remotely or locally.
1.2. Simple development model, Fast turn-over
Wisdom proposes a very simple development model where you focus on your application logic and not on how things are working.
When developing an application, don’t spend you time to re-package and re-deploy your app. Save your files, hit refresh in your browser. Everything is done for you.
Wisdom relies on the Apache Maven build tool. Don’t run away. We made it very easy. It integrates everything you need from testing, to packaging and obviously natural continuous integration.
1.3. Modularity as a core value, dynamism everywhere
You may say: ‘well, what’s the difference with Grails or Play Framework’. Wisdom is intrinsically modular and dynamic. That means:
-
Your application is no longer monolithic, but modular
-
Each part of your application is autonomous, can be developed, tested and deployed individually
-
Your application is naturally dynamic, every part can come and leave at anytime, and more importantly can be updated without downtime
-
Wisdom itself is modular and dynamic, so extend it, replace technical services… on the fly
A full web stack for Java
Wisdom proposes a full web stack:
-
RESTful by default
-
Asset optimization, CoffeeScript, Less compilation, etc.
-
Data Validation
-
JSON
-
Web Sockets
-
HTML5 template system
-
HTML 5
Wisdom runs on the Java Virtual Machine. Wisdom applications can run on any Cloud provider supporting Java.
1.4. Why Wisdom?
We developed dozens of web applications in the past five years, using lots of different technologies: JavaEE, Grails, Lift, Play Framework (1 and 2), pure-JavaScript… We learned a lot from all these great frameworks, but for us, something was missing: the dynamism.
Our applications live in ever-changing (and expanding) environments, face ever-shifting (and increasing) demands, and must live up to ever-escalating expectations. As Niels Bohr said: “Prediction is very difficult, especially if it’s about the future”.
To face this situation, we need modularity and dynamism. Modularity to decompose applications into well-defined autonomous blocks, managed individually. Dynamism to manage the bindings between these components, their runtime environment and others services.
2. Getting started
2.1. Installing Wisdom
2.1.1. Prerequisites
To run Wisdom, you need:
-
Java 7+ (Java Development Kit)
-
Apache Maven. Wisdom requires a recent version of Maven (3.2.1+)
In production, Wisdom just requires the Java 7+ virtual machine.
| Java 8 is also supported. So you can use lambda expressions ! |
3. Your first Wisdom project
| Be sure you have a recent version of Maven (3.2.1`). |
3.1. Create a Wisdom project
Wisdom is based on the Apache Maven build tools. However don’t worry, Wisdom makes it easy. However, first, be sure you have Maven installed.
Once installed, you can create your first Wisdom project by launching:
mvn org.wisdom-framework:wisdom-maven-plugin:0.10.0:create \ -DgroupId=YOUR_GROUPID \ -DartifactId=YOUR_ARTIFACTID \ -Dversion=1.0-SNAPSHOT
On Windows use:
mvn.bat org.wisdom-framework:wisdom-maven-plugin:0.10.0:create -DgroupId="YOUR_GROUPID" -DartifactId="YOUR_ARTIFACTID" -Dversion="YOUR_VERSION"
You can also customize the generated package with -Dpackage=org.acme.wisdom
|
Once generated, you can already run your wisdom application. Navigate to the generated folder, and launch:
mvn wisdom:run
Then, open your browser to: http://localhost:9000.
That’s it you have your first Wisdom application.
3.2. Using the Maven Archetype
This section is only for Maven users willing to use an archetype. The very same project can be
generated using the
wisdom-default-project-archetype:
mvn org.apache.maven.plugins:maven-archetype-plugin:generate \ -DarchetypeArtifactId=wisdom-default-project-archetype \ -DarchetypeGroupId=org.wisdom-framework \ -DarchetypeVersion=0.10.0 \ -DgroupId=YOUR_GROUPID \ -DartifactId=YOUR_ARTIFACTID \ -Dversion=1.0-SNAPSHOT
The archetype can be used from your IDE to generate a new project directly.
3.3. What was generated?
The previous commands have generated a set of folders. Following the Maven conventions,
all files are placed in the src directory, while the generated artifacts are in target.
The src/main directory is structured as follows:
-
src/main/javacontains the Java sources of your application -
src/main/resourcescontains the assets and templates included in your application package -
src/main/configurationcontains the configuration file (application, runtime, logger…) -
src/main/assetscontains assets that are copied to Wisdom but not included in application package -
src/main/templatescontains assets that are copied to Wisdom but not included in application package
The src/test directory contains the tests (unit and integration tests).
3.4. You first controller
The generated application contains a controller. A controller is a Wisdom component containing actions, i.e. methods called using HTTP requests.
package sample;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.*;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller (1)
public class WelcomeController extends DefaultController { (2)
@View("welcome")
private Template welcome;
@Route(method = HttpMethod.GET, uri = "/") (3)
public Result welcome() {
return ok(render(welcome)); (4)
}
}
| 1 | First, declare the Java class as Wisdom’s controller |
| 2 | Extend DefaultController to inherit from the useful method |
| 3 | Specify the HTTP route connected to your action method |
| 4 | The body of your action, returning a Result object |
In the previous example, the controller is using a template (welcome), but things can be much simpler:
/**
* Your first Wisdom Controller.
*/
@Controller (1)
public class SampleController extends DefaultController { (2)
@Route(method = HttpMethod.GET, uri = "/hello") (3)
public Result hello() {
return ok("hello Wisdom !"); (4)
}
}
| 1 | First, declare the Java class as Wisdom’s controller |
| 2 | Extend DefaultController to inherit from the useful method |
| 3 | Specify the HTTP route connected to your action method |
| 4 | The body of your action, returning a Result object |
3.5. Wisdom API and Maven Plugin
-
The Wisdom JavaDoc is available on the API page.
-
The Wisdom-Maven-Plugin goals and configuration are described on the Maven Site.
4. Package, Test, Run your application
Wisdom’s build system is based on Apache Maven, so the build process is following the default Maven lifecycle.
4.1. Package your application
mvn package creates two files: a jar file containing your application, and a zip file containing the whole
distribution, ready to be deployed.
| The jar file is an OSGi bundle. |
4.2. Adding dependencies
To add a dependency to your application, just add a <dependency> in the pom.xml. Wisdom copies all dependencies
from the compile scope (default Maven scope):
<dependency>
<groupId>asm</groupId>
<artifactId>asm-all</artifactId>
<version>3.3.1</version>
</dependency>
However notice two important rules:
-
It does not copy transitive dependencies, so don’t forget to declare all your dependencies.
-
Only OSGi bundles are copied, so check that your dependencies are valid OSGi bundles.
| Most libraries are OSGi bundles already. |
4.3. Finding bundles
Unfortunately, you may want to use a library that is not a bundle. When facing this situation, you have 3 choices:
-
Trying to find a bundlelized version of the library
-
Use the non-OSGi dependency support of Wisdom - Check the Using non-OSGi dependencies section
-
Make your own bundle (that you can contribute to the project) - Check the Some bundelization rules section
You may want to check the bundles available on:
-
Service Mix Bundles - Apache Service Mix have created lots of bundles of all the dependencies they use
-
Spring Source OSGi Repository - Spring Source has also created a bunch of bundles you can find on their Enterprise Bundle Repository
4.4. Using non-OSGi dependencies
Unfortunately, not all libraries are OSGi bundles. Fortunately, Wisdom supports non-bundle dependencies. These
dependencies are copied to the libs directory of the Wisdom server and are called libraries. These dependencies
does not need to be OSGi bundles, and can be used by the Wisdom applications.
First add your Maven dependency as usual such as:
<dependency>
<groupId>org.pegdown</groupId>
<artifactId>pegdown</artifactId>
<version>1.4.2</version>
</dependency>
Then, you need to select explicitly the libraries from the set of dependencies. Only selected dependencies are copied. This selection is made as follows:
<plugin>
<groupId>org.wisdom-framework</groupId>
<artifactId>wisdom-maven-plugin</artifactId>
<version>0.10.0</version>
<extensions>true</extensions>
<configuration>
<!-- Defines the set of 'libraries' -->
<libraries>
<includes>
<!--
defines the set of dependencies to select as library (mandatory).
it uses the following syntax: groupId:artifactId:type[:classifier]:version
-->
<include>:pegdown</include>
</includes>
<!--
whether or not transitive dependencies of the selected libraries should also
be copied (default to true). only 'compile' dependencies are copied.
-->
<resolveTransitive>true</resolveTransitive>
<!--
whether or not the selected artifacts should be excluded from the 'application'
directory (default to false).
-->
<excludeFromApplication>true</excludeFromApplication>
<!--
allow to exclude some artifacts. This feature is useful when transitive support
is enabled to filter out undesirable artifacts.
-->
<excludes>
<exclude>:asm-tree</exclude>
</excludes>
</libraries>
</configuration>
</plugin>
The libraries element configure the selection and behavior. It must contains an includes element with at least
one include sub-element. If none are set, no dependencies are considered as libraries.
Using libraries allows you to rely on non-bundle dependencies, but it comes with a couple of limitations:
-
the set of libraries is static, so cannot change after the wisdom server has started.
-
there is no dependency resolution at runtime, meaning that all dependencies required by selected libraries must also be available from the 'libs' directory.
In other word, libraries are very useful when you can’t find a suitable OSGi bundle, but need to be used carefully to not break the modularity and dynamism of your applications.
The content of the jar files contained in the libs directory are exported by the OSGi framework.
|
| This feature only works when using Apache Felix. |
4.5. Some bundelization rules
If you want to create your own OSGi bundle, here are some rules:
-
Inspect the library to separate the public API from the private / implementation parts - this is particularly important to avoid exposing implementation details.
-
Detect ClassLoader.loadClass calls, ClassLoader.findResource, and more generally all
ClassLoaderaccess -
Check whether the code rely on the Thread Context ClassLoader, if so, wrap the calls with a TTCL Switch:
final ClassLoader original = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
// your code using the library.
} finally {
Thread.currentThread().setContextClassLoader(original);
}
-
Detect SPI usage, and try to see if you can create the instance manually and inject it
-
Last but not least, design a service interface, to avoid leaking the whole API - and if it’s possible, make the service interface independent of the library (meaning that you can substitute by another one someday)
4.6. Run tests
Tests are situated in the src/test/java directory.
mvn test executes all the unit tests following the Surefire convention. So, it executes all tests from classes
starting or finishing by Test, such as MyClassTest or TestMyClass.
mvn integration-test executes all the integration tests following the Surefire convention. So, it executes all tests
from classes starting or finishing by IT, such as MyComponentIT or ITMyComponent..
Unit tests are also executed in watch mode. It re-executes the unit tests when you change either a Java class or a test. By default, all test are re-executed. However, to reduce the amount of test, use:
<testSelectionPolicy>SELECTIVE</testSelectionPolicy>
With the SELECTIVE policy, only related tests are executed. Notice that, test failures are reported in the browser.
You can skip tests adding the -DskipTests flag to the Maven command line.
4.7. Watch mode
To run the application while developing, launch:
mvn wisdom:run
It packages your application and starts the Wisdom server. It also watches changes you make on your files and redeploys things automatically. For instance, if you edit a Java file, it compiles, packages and redeploys the application.
Modifying the pom.xml file relaunches the server automatically.
|
The wisdom watch mode allows you to use a remote Wisdom server. Using this feature, you can
watch several project at the same time. To enable this feature launch the watch mode with
-DwisdomDirectory=location.
So, imagine you have two projects, P1 and P2, and P2 depends on P1. You want to enjoy the
watch mode on both P1 and P2 projects, but, as P2 is your final / main project, you
also want to immediately copy the P1 output to P2. In this context, just launch the watch
mode twice as follows:
# P2 - regular watch mode (it starts the server we are going to use in P1) mvn wisdom:run
# P1 - watch mode with remote server mvn wisdom:run -DwisdomDirectory=../p2/target/wisdom
| Launch the main server first, if not, it may override the copied resources. |
When you launch the watch mode with the wisdomDirectory, some copy functionality
is disabled (configuration and external resources). So in our previous example, modifying the
P1 configuration, external resources, or templates do not copy them to P2. However, as P2
is launched without the parameter, all features are provided.
|
The watch mode is based on a File Alteration Monitor (FAM). The default polling period is set to 2 seconds. You
can configure it using -Dwatch.period=5. The set time is in seconds.
|
4.8. Debugging
In watch mode, you can enable the remote debugging by using the -Ddebug=port:
mvn wisdom:run -Ddebug=5005
Then, just launch the debug mode of your IDE using the set port (5005 in the previous example).
4.9. Run the application
The application is assembled in the target/wisdom directory. You can run it without the watch mode using:
./chameleon.sh
TIP:Why Chameleon? Because Chameleon is a kind of OSGi distribution with some additional features on which Wisdom relies.
4.10. Accessing the OSGi shell
If you are an OSGi expert and want to access the shell launch the application with:
./chameleon.sh --interactive
The provided shell is OW2 Shelbie.
4.11. The distribution
mvn package generates an OSGi bundle (a jar file) and a zip file. The jar file can be deployed into any other
wisdom application. The zip file contains the whole distribution and can be run using the same shell script as
previously explained.
4.12. Customizing applications packaging
Wisdom applications are packaged inside OSGi bundles. By default it:
-
includes all classes (from
src/main/java) and resources (fromsrc/main/resources) -
imports all required packages (deduced by analyzing the classes)
-
exports all packages containing
service(s),api(s),model(s),entityorentities
However, sometimes you want to customize this default packaging policy. Wisdom relies on BND to build bundles. BND is building the OSGi bundle by following a set of instructions such as:
Import-Package: com.library.*; version = 1.21
To customize the bundle of your application, create a osgi.bnd file in src/main/osgi/,
and write the BND instructions there.
For example, the Wisdom Hibernate Validation bundle has the following instructions:
Private-Package: org.wisdom.validation.hibernate;-split-package:=merge-first, \
org.jboss.logging.*, \
com.fasterxml.classmate.*, \
org.hibernate.validator*, \
com.sun.el*, \
javax.el*
Import-Package: javax.validation.*;version="[1.1.0,2.0.0)", \
!org.apache.log4j, \
org.jsoup*;resolution:=optional, \
javax.persistence*;resolution:=optional, \
org.jboss.logmanager*;resolution:=optional, \
*
Export-Package: org.hibernate.validator.constraints*
The osgi.bnd file can use Maven properties.
|
When used, the osgi.bnd file is merged with the default instructions used by Wisdom (Import-Package,
Export-Package (based on the package name), Private-Package, and Include-Resource). In other words, every
instructions specified in the osgi.bnd replaces default value provided by Wisdom for this instruction. For example,
if your osgi.bnd file contains Export-Package: my.package.to.be.exported, it won’t compute the default exported
packages, but use your value (and only your value).
You can completely disable the Wisdom defaults, by adding the following instruction in the osgi.bnd file:
-no-default: true
4.13. Embedding Maven dependencies inside the application package
You can declare a set of (Maven) dependency to be packaged within the application bundle. The Wisdom Maven Plugin
supports embedding of selected project dependencies inside the bundle by using the Embed-Dependency instruction:
Embed-Dependency: dependencies
where:
dependencies ::= clause ( ',' clause ) * clause ::= MATCH ( ';' attr '=' attribute | ';inline=' inline | ';transitive=' transitive) attr ::= 'scope' | 'type' | 'classifier' | 'optional' transitive ::= 'true' | 'false' inline ::= 'true' | 'false' | PATH ( '|' PATH ) * attribute ::= <value> ( '|' <value> ) * MATCH ::= <Maven Pattern Dependency Filter ([groupId]:[artifactId]:[type]:[version])> PATH ::= <Ant-style path expression>
The plugin uses the Embed-Dependency instruction to transform the project dependencies into Include-Resource and
Bundle-ClassPath clauses, which are then appended to the current set of instructions and passed onto BND.
Let’s see some examples:
# embed all compile and runtime scope dependencies Embed-Dependency: *;scope=compile|runtime # embed any dependencies with artifactId junit and scope runtime Embed-Dependency: junit;scope=runtime # embed all compile and runtime scope dependencies, except those with artifactIds in the given list Embed-Dependency: *;scope=compile|runtime;inline=false;exclude=:cli|:lang|:runtime|:tidy|:jsch # inline contents of selected folders from all dependencies Embed-Dependency: *;inline=images/**|icons/**
Normally the plugin only checks direct dependencies, but this can be changed to include the complete set of transitive dependencies with the following option:
Embed-Transitive: true
It can also be enabled using the transitive=true attribute.
| Be aware that Wisdom is using 1) a different syntax than the Maven-Bundle-Plugin to select the set of artifacts, 2) Inline artifacts by default |
4.14. Configuring the Java source and target
By default, Wisdom compiles your code using Java 1.7. This convention was made to stick with the Maven convention.
However you can override these settings by adding the following properties in your pom.xml:
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.testSource>1.8</maven.compiler.testSource>
<maven.compiler.testTarget>1.8</maven.compiler.testTarget>
</properties>
4.15. Adding instance configuration
You can add cfg file into the src/main/instances directory to add them to the wisdom/application directory.
cfg files let you configure iPOJO instances (instantiation, configuration and reconfiguration). This directory is
watched so changes are reflected immediately.
The file are filtered, so can rely on Maven properties.
The cfg file format is defined in the Configuration deployment section.
These files are packaged in the distribution but not in the bundle. So, if an application relies on a component
requiring a specific instance configuration, this configuration need to be copied. If the configuration is static,
you should use the @Configuration support of
iPOJO.
5. HTTP Programming
5.1. Actions, Controllers and Results
5.1.1. What is an Action?
Most of the requests received by a Wisdom application are handled by an action. An action is basically a Java
method that processes the received request parameters, and produces a Result to be sent to the client.
@Route(method = HttpMethod.POST, uri = "/")
public Result index() {
return ok("Got request " + request() + "!");
}
An action returns a org.wisdom.api.http.Result value, representing the HTTP response to send to the
web client. In this example ok constructs a OK (HTTP Code 200) response containing a text/plain response body.
Actions are declared with the org.wisdom.api.annotations.Route annotation (@Route) indicating the
HTTP method and uri used to invoke the action.
5.1.2. Controllers
Controllers are Wisdom components containing the application logic. A controller is nothing more than a class
annotated with the org.wisdom.api.annotations.Controller (@Controller) that groups several action
methods.
package snippets.controllers;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller
public class Simple extends DefaultController {
@Route(method= HttpMethod.GET, uri = "/works")
public Result index() {
return ok("Follow the path to Wisdom");
}
}
Be sure to import org.wisdom.api.annotations.Controller when you use the @Controller
annotation and not org.wisdom.api.Controller.
|
The simplest syntax for defining an action is a (non-static) method with no parameters that returns a Result value,
as shown above.
An action method can also have parameters:
package snippets.controllers;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Parameter;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller
public class Name extends DefaultController {
@Route(method= HttpMethod.GET, uri = "/hello/{name}")
public Result index(@Parameter("name") String name) {
return ok("Hi " + name + ", follow us on the Wisdom path");
}
}
These parameters (indicated with the @Parameter annotation) will be resolved by Wisdom and will be filled with
values from the request. The parameter values can be extracted from either the URL path or the URL query. The
annotation value, "name" in the previous example, indicates the name of the parameter.
5.1.3. Results
Let’s start with basic results: an HTTP result with a status code, a set of HTTP headers and a body to be sent to the
client. These results are defined by org.wisdom.api.http.Result, and the
org.wisdom.api.http.Results class provides several helpers to produce standard HTTP results,
such as the ok method we used in the previous examples:
package snippets.controllers;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller
public class Simple extends DefaultController {
@Route(method= HttpMethod.GET, uri = "/works")
public Result index() {
return ok("Follow the path to Wisdom");
}
}
Here are several examples that create various results:
Result ok = ok("Hello wisdom!");
Result page = ok(render(template));
Result notFound = notFound();
Result pageNotFound = notFound("<h1>Page not found</h1>").as(MimeTypes.HTML);
Result badRequest = badRequest(render(template, "error", errors));
Result oops = internalServerError("Oops");
Result exception = internalServerError(new NullPointerException("Cannot be null"));
Result anyStatus = status(488).render("Strange response").as(MimeTypes.TEXT);
All of these helpers can be found in the org.wisdom.api.http.Results class.
5.1.4. Redirects are simple results too
Redirecting the browser to a new URL is just another kind of simple result. However, these result types don’t have a response body.
@Route(method= HttpMethod.GET, uri="/redirect")
public Result redirectToIndex() {
return redirect("/");
}
The default is to use a 303 SEE_OTHER response type, but you can also specify a more specific
status code:
@Route(method= HttpMethod.GET, uri="/tmp")
public Result redirectToHello() {
return redirectTemporary("/hello/wisdom");
}
5.1.5. Using the request scope
The request scope is a map shared by all the entities participating to the request resolution (i.e. response computation). It includes filters, interceptors, action methods… So it let these entities to share data.
You can write or retrieve data from the request scope as follows:
int echo = (int) request().data().get("echo");
request().data().put("echo", echo + 1);
The request scope can contain any types of objects, and is cleared once the response is sent back to the client.
Objects from the request scope can be injected as action’s method parameters:
@Route(method=HttpMethod.GET, uri="/test")
public Result doIt(@HttpParameter("echo") int echo) {
// ...
}
The request scope must be used to avoid using Thread Local object. Thread Local do not conform to the
Wisdom execution model, and would not be available in async result processing. So, instead of using a Thread
Local, put the object into the request scope. By this way, you ensure the object to be available until the end of
the request processing.
== The HTTP request router
|
The router is the component that translates each incoming HTTP request to an action call. The HTTP request contains two major pieces of information:
-
the request path (such as
/foo/1234,/photos/), including the query string (the part after the?in the url). -
the HTTP method (GET, POST, …).
Routes are defined directly on the action method with the @Route annotation.
5.2. The @Route annotation
The @Route annotation contains two attributes: the HTTP method and the URI pattern determining on which URLs the action is associated.
Let’s see a couple of examples:
@Route(method=HttpMethod.GET, uri="/photos")
@Route(method=HttpMethod.GET, uri="/photos/{id}")
@Route(method=HttpMethod.POST, uri="/photos/")
@Route(method=HttpMethod.DELETE, uri="/photos/{id}")
5.3. The HTTP method
The HTTP method can be any of the valid methods supported by HTTP (mainly GET, POST, PUT,
DELETE). They are defined in org.wisdom.api.http.HttpMethod.
5.4. The URI pattern
The URI pattern defines the route’s request path. Some parts of the request path can be dynamic.
5.4.1. Static path
For example, to exactly match GET /photos incoming requests, you can define this route:
@Route(method=HttpMethod.GET, uri="/photos")
5.4.2. Dynamic parts
If you want to define a route that, say, retrieves a photo by id, you need to add a dynamic part, filled by the router:
@Route(method=HttpMethod.GET, uri="/photos/{id}")
URI patterns may have more than one dynamic part such as in /foo/{id}/{name}.
|
The default matching strategy for a dynamic part is defined by the regular expression defined as {id} will match
exactly one URI path segment.
5.4.3. Dynamic parts spanning several path segments
If you want a dynamic part to capture more than one URI path segment, separated by slashes, you can define a dynamic
part using the {path*} syntax:
@Route(method=HttpMethod.GET, uri="/assets/{path*}")
Here, for a request like GET /assets/stylesheets/style.css, the name dynamic part will capture the
stylesheets/style.css value.
{path*} scheme accepts empty input. To avoid this, use {path+}.
|
5.5. Controller’s path
Often the actions of one controller starts with a common prefix, such as /photos. To avoid the repetition,
you can use the @Path annotation indicating the common prefix prepended to all uris from the @Route.
package snippets.controllers;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Parameter;
import org.wisdom.api.annotations.Path;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller
@Path("/photo")
public class PhotoController extends DefaultController {
@Route(method= HttpMethod.GET, uri="/")
public Result all() {
return ok();
}
@Route(method= HttpMethod.POST, uri="/")
public Result upload() {
return ok();
}
@Route(method= HttpMethod.GET, uri="/{id}")
public Result get(@Parameter("id") String id) {
return ok();
}
// ...
}
5.6. Action Parameters
Action methods can receive parameters. Several types of parameters are supported:
-
parameter from the query part of the url or from the dynamic part of the uri. These parameters are annotated with
@Parameter,@QueryParameterand@PathParameter -
fields from form are annotated with
@FormParameter. It’s also used for file upload. -
object built from the request body (for
POSTrequests). These parameters are annotated with@Body -
@HttpParameterinjects an object extracted from the incoming request such as a header, request scope data, the HTTP context, the request and cookies. -
@BeanParameterinjects an instance of a class using the other annotations.
All action method parameters must be annotated with one of the above annotations.
5.6.1. Types
Before going further, let’s see what types are supported. The type is extracted from the
annotated argument (i.e. formal parameter). So in @Parameter("param") String p, it would be
java.lang.String, while in @Parameter("params") List<String> p, it would be a
java.util.List<String>.
Wisdom supports:
-
String
-
Primitive types
-
Classes having a constructor with a single String argument
-
Classes having a
valueOfstatic method with a single String argument (so support enumeration) -
Classes having a
fromstatic method with a single String argument -
Classes having a
fromStringstatic method with a single String argument -
Any type with a
org.wisdom.api.content.ParameterConverterservice handling it -
Array, List and Set containing previously listed types
Boolean’s values are extended. Are considered as 'true': "true", "on", "yes", "1". Other strings are considered as 'false'.
If you want to extend such support, you just have to implement org.wisdom.api.content.ParameterConverter and expose
it as a service.
5.6.2. @Parameter, @QueryParameter and @PathParameter
The @Parameter annotation indicates that the marked method parameter takes a value from either the query part of
the method, or a dynamic part of the URI.
In the following example, the id value is computed from the URL. For instance,
http://localhost:9000/parameters/foobar would set id to foobar.
@Route(method= HttpMethod.GET, uri="/{id}")
public Result get(@Parameter("id") String id) {
// The value of id is computed from the {id} url fragment.
return ok(id);
}
Using query parameter is also simple:
@Route(method= HttpMethod.GET, uri="/")
public Result get2(@Parameter("id") String id) {
// The value of id is computed from the id parameter of the query.
// In /?id=foo it would get the "foo" value.
return ok(id);
}
With the previous snippet, and the URL http://localhost:9000/parameters/?id=foo, id would be set to foo.
When bound to an array type, Wisdom collects the values of the given parameter name in the query part of the request. It builds an array injectable as follows:
@Parameter("ids") List<Integer> ids,
@Parameter("names") String ns[]
If the parameter has no values, an empty array or list is injected.
@Parameter can receive a default value (except if the parameter is deduced from the path):
@Parameter("id") @DefaultValue("0") int id,
@Parameter("ids") @DefaultValue("0, 1") int[] ids
The default value is a String representation of the value to use if there are no values. For collections and arrays, the default value is interpreted as a comma-separated list.
If you want to enforce the origin of the value you can use the @PathParameter and @QueryParameter annotation
to select the value respectively from the path or query.
@DefaultValue cannot be used with @PathParameter.
|
5.6.3. @FormParameter
the @FormParameter annotation retrieves values sent from HTML forms.
@Route(method= HttpMethod.POST, uri="/")
public Result post(@FormParameter("id") String id, @FormParameter("name") String name) {
// The values of id and names are computed the request attributes.
return ok(id + " - " + name);
}
In the previous snippet, two attributes can be received by the action method: id and name. A
form sending such data as application/x-www-form-urlencoded will be handled directly.
When bound to an array type or a collection, Wisdom collects the values of the given attribute. It builds an injectable object (array or collection) as follows:
@FormParameter("ids") List<Integer> ids,
@FormParameter("names") String ns[]
If the attribute has no values, an empty array or collection is injected.
@FormParameter can receive a default value:
@FormParameter("id") @DefaultValue("0") int id,
@FormParameter("ids") @DefaultValue("0, 1") int[] ids
The default value is a String representation of the value to use if there are no values. For collections and arrays, the default value is interpreted as a comma-separated list.
5.6.4. @Body
The @Body annotation lets you wrap the request into an object. This object must be either a
bean or have public attributes.
For instance, let’s imagine you need to manage an instance of the following class:
public class MyData {
public String name;
public String id;
public String toString() {
return "Data: " + id + " - " + name;
}
}
You don’t want to build your instance by yourself, luckily Wisdom handles that for you:
@Controller
public class BodyWrap extends DefaultController {
@Route(method= HttpMethod.POST, uri = "/wrap")
public Result index(@Body MyData data) {
return ok(data.toString());
}
}
The @Body annotation builds an object of the parameter’s type using the request content and
parameters. It becomes very handy when handling JSON data, or complex form.
Parsing from JSON and XML support parameterized type as: @Body List<String> list, or @Body Data<Person> data. However, parameterized types are not supported when retrieving a form.
|
5.6.5. @HttpParameter
The @HttpParameter annotation let you inject HTTP related data such as the Request, the Context, HTTP header’s
value…
@Route(method = HttpMethod.GET, uri = "/parameter/http")
public Result http(
@HttpParameter Context context, // HTTP Context
@HttpParameter Request request, // Incoming Request
@HttpParameter("X-header") String header, // Header value
@HttpParameter SessionCookie session, // Session
@HttpParameter Stuff stuff) { // Custom object (invoke a factory service)
//...
}
The type of the parameter determines the injected value:
-
the HTTP
Context-org.wisdom.api.http.Context -
the HTTP
Request-org.wisdom.api.http.Request -
a
Cookieobject (cookie’s name given in parameter) -org.wisdom.api.cookies.Cookie -
the
Sessionobject (Session cookie) -org.wisdom.api.cookies.SessionCookie -
the
Flashobject (Flash cookie) -org.wisdom.api.cookies.FlashCookie -
the invoked
Route-org.wisdom.api.router.Route -
a
Readerobject to read the request body
If the type does not match one of the previous case:
-
if a
org.wisdom.api.content.ParameterFactoryservice handling the parameter’s type is available, it invokes it. -
if a HTTP header has the name given in the annotation parameter, it injects the header value
-
otherwise it looks into the request scope to check whether a filter or interceptor has provided a value with the name given in the annotation parameter.
5.6.6. @BeanParameter
The last annotation lets you build a bean using any of the other annotations presented here. The bean’s class must:
-
have a no-args constructor, or a constructor using the other annotations.
-
have setter methods (starting with set), having only one parameter, and this parameter must be annotated with one of the other annotations (it can also use the
@DefaultValueannotation).
Let’s see an example of such a bean:
/**
* A bean injected using the {@link org.wisdom.api.annotations.BeanParameter} annotation.
*/
public class Bean {
private final String value;
/**
* You can add validation constraint on the injected value.
*/
private @NotNull String value2;
/**
* Values can be injected in the constructor, and combined with the
* {@link org.wisdom.api.annotations.DefaultValue} annotation.
*
* @param v the value
*/
public Bean(@DefaultValue("hello") @QueryParameter("q") String v) {
this.value = v;
}
/**
* A regular setter used to inject a second parameter.
*
* @param v the value
*/
public void setValue2(@QueryParameter("q2") String v) {
this.value2 = v;
}
public String getValue() {
return value;
}
public String getValue2() {
return value2;
}
}
An instance of this bean is going to be created when the following action method is invoked:
/**
* Shows how to use the {@link org.wisdom.api.annotations.BeanParameter} annotation.
*/
@Controller
public class BeanExample extends DefaultController {
/**
* The {@link org.wisdom.api.annotations.BeanParameter} is used to inject
* the parameter. It can be combined with the {@link javax.validation.Valid}
* annotation to ensure the validity of the resulting object.
*
* @param bean the instantiated bean
* @return the json form of the bean
*/
@Route(method = HttpMethod.GET, uri = "/bean")
public Result get(@Valid @BeanParameter Bean bean) {
return ok(bean).json();
}
}
The @BeanParameter also handles nested beans.
5.7. Validation
The action method parameter can be validated using the Bean Validation Annotations. It lets you express constraints on object models via annotations.
Here are some examples:
package snippets.controllers.validation;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Body;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Parameter;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Controller
public class ValidationController extends DefaultController {
@Route(method = HttpMethod.GET, uri = "/validation")
public Result actionWithValidatedParams(
@NotNull(message = "id must be defined") @Parameter("id") String id,
@Size(min = 4, max = 8) @Parameter("name") String name) {
return ok();
}
@Route(method = HttpMethod.POST, uri = "/validation/bean")
public Result actionWithValidatedBody(
@Valid @Body User user
) {
return ok();
}
public static class User {
@NotNull
@Size(min = 4, max = 8)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
Thanks to these annotation, you don’t have to validate your parameter manually. If the constraints a violated, Wisdom
returns a bad request JSON answer:
[{"message":"id must be defined","path":"actionWithValidatedParams.arg0"}]
or
[{
"message": "size must be between 4 and 8"
"invalid": "xx"
"path": "actionWithValidatedBody.arg0.name"
}]
5.7.1. Using Hibernate Validation constraints
Wisdom uses the Hibernate Validation implementation defining more constraints (such as @Email). You can rely on
these constraints by adding the following dependency in your pom.xml file:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.2.Final</version>
<scope>provided</scope>
</dependency>
5.7.2. Manual validation of forms
If you want to run the validation yourself and customize the response, you can decide to run the validation process yourself:
package snippets.controllers.validation;
import org.apache.felix.ipojo.annotations.Requires;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Body;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Set;
@Controller
public class ManualValidationController extends DefaultController {
@Requires
Validator validator;
@Route(method = HttpMethod.POST, uri = "/validation/bean/manual")
public Result actionWithValidatedBody(
@Body User user
) {
final Set<ConstraintViolation<User>> violations = validator.validate(user);
if (violations.isEmpty()) {
return ok();
} else {
return badRequest("Oh no, this is not right: "
+ violations.iterator().next().getMessage());
}
}
public static class User {
@NotNull
@Size(min = 4, max = 8)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
5.7.3. Internationalized validation message
Validation constraints have a message parameter that lets you specify the message. Instead of a message, you can
use a message key that will be picked among the available locales. Message keys are wrapped into {} as in:
@Route(method = HttpMethod.GET, uri = "/validation/i18n")
public Result actionWithValidatedParams(
@NotNull(message = "{id_not_set}") @Parameter("id") String id,
@Size(min = 4, max = 8, message = "{name_not_fit}") @Parameter("name") String name) {
return ok();
}
Then, creates the resource bundle files in src/main/resources/i18n such as:
id_not_set: The id must be set
name_not_fit: The name '${validatedValue}' must contain between {min} and {max} characters
for the default (english) languages, and files containing the translations as:
id_not_set: L'identifiant n'est pas spécifié
name_not_fit: Le nom '${validatedValue}' doit contenir entre {min} et {max} caractères
So, Wisdom is picking the right message according to the request languages (passed into the ACCEPT_LANGUAGE header).
As you can see, messages can use variables to make the error more informative. For instance, in English the message
is The name 'ts' must contain between 4 and 8 characters, while on a browser using the French language, the
message is Le nom 'ts' doit contenir entre 4 et 8 caractères. If the asked language is not available, default
(english) is used.
These files are part of the internationalization support. Check Internationalize your application for more details.
5.8. Declaring routes without @Route
If for some reasons, the @Route annotation does not meet your requirements, you can override the
routes() method. This method lets you define a set of routes. These routes are not prefixed
by the @Path prefix, and can target other controllers.
@Override
public List<Route> routes() {
return ImmutableList.of(
new RouteBuilder().route(HttpMethod.GET).on("/routes").to(this, "action"),
new RouteBuilder().route(HttpMethod.POST).on("/routes").to(this, "action")
);
}
Wisdom provides a RouteBuilder that lets you define Route easily.
5.9. Route Conflict
Because there is no central location where routes are defined (as it breaks modularity), conflict may happen. Wisdom tries to detect them when the application is deployed, however, the detection algorithm is limited. We recommend using different prefixes (one per application) to avoid conflicts.
5.10. Accessing the router
The Wisdom router is exposed as an OSGi service. Don’t worry it does not bite. Your controller can access it as follows:
@Requires
private Router router;
public void doSomethingWithTheRouter() {
org.wisdom.api.router.Route route = router.getRouteFor(HttpMethod.GET, "/");
}
The router lets you find routes by yourself, invoke them, check their parameters… In addition, it supports reverse routing.
| Wisdom is based on Apache Felix iPOJO. All iPOJO features are available in Wisdom. |
5.11. Reverse routing
The router can be used to generate a URL from within your code. Thus, refactoring is made easier.
@Route(method=HttpMethod.GET, uri="/reverse")
public Result reverse() {
// Get the url of another action method (without parameters)
String all = router.getReverseRouteFor(PhotoController.class, "all");
// Get the url of another action method, with id = 1
String get = router.getReverseRouteFor(PhotoController.class, "get", "id", 1);
return ok(all + "\n" + get);
}
Invoking the defined action returns:
/photo/ /photo/1
Reverse routing can let you generate redirect results on actions you don’t know the url:
@Route(method=HttpMethod.GET, uri = "/reverse/redirect")
public Result redirectToHello() {
String url = router.getReverseRouteFor(Name.class, "index", "name", "wisdom");
return redirect(url);
}
6. Manipulate HTTP Response
6.1. Changing the default Content-Type
The result content type is automatically inferred from the value you specify in the body of the produced result.
For example, in:
@Route(method = HttpMethod.GET, uri = "/manipulate/text")
public Result text() {
return ok("Hello World!");
}
Wisdom automatically sets the Content-Type header to text/plain, while in:
@Route(method = HttpMethod.GET, uri = "/manipulate/json")
public Result jsonResult() {
ObjectNode node = new ObjectMapper().createObjectNode();
node.put("hello", "world");
return ok(node);
}
it sets the Content-Type header to application/json.
This is pretty useful, but sometimes you want to change it. Just use the as(newContentType) method on a result to
create a new similar result with a different Content-Type header:
@Route(method = HttpMethod.GET, uri = "/manipulate/html2")
public Result htmlResult2() {
return ok("<h1>Hello World!</h1>").as(MimeTypes.HTML);
}
You can also use the methods html() and json() to set the content type respectively to HTML and
JSON.
6.2. Setting HTTP response headers
You can add (or update) any HTTP response header:
@Route(method = HttpMethod.GET, uri = "/manipulate/headers")
public Result headers() {
return ok("<h1>Hello World!</h1>")
.html()
.with(CACHE_CONTROL, "max-age=3600")
.with(ETAG, "xxx");
}
The previous action returns a result with the header set to the expected values:
Content-Length: 21 Connection: keep-alive Etag: xxx Cache-Control: max-age=3600 Content-Type: text/html; UTF-8
| Setting an HTTP header will automatically discard any previous values. |
6.3. Setting and discarding cookies
Cookies are just a special form of HTTP headers, but Wisdom provides a set method to manipulate them easily. You can easily add a Cookie to the HTTP response:
@Route(method = HttpMethod.GET, uri = "/manipulate/cookies")
public Result cookies() {
return ok("<h1>Hello World!</h1>")
.html()
.with(Cookie.cookie("theme", "github").build());
}
| You can configure all aspects of the built cookie such as the max age, domain, and path. |
Also, to discard a Cookie previously stored on the Web browser:
@Route(method = HttpMethod.GET, uri = "/manipulate/remove-cookies")
public Result withoutCookies() {
return ok("<h1>Hello World!</h1>")
.html()
.discard("theme");
}
Two methods are available to remove things from an existing Result. without(String)
removes a header while discard(String) removes a cookie. However, if without(String) does not
find a matching header, it will try to remove a cookie.
|
6.4. Specifying the character encoding for text results
For a text-based HTTP response it is very important to handle the character encoding correctly. Wisdom handles that
for you and uses UTF-8 by default. The encoding is used to both convert the text response to the
corresponding bytes to send over the network socket, and to add the proper ;charset=xxx
extension to the Content-Type header.
The encoding can be specified when you are generating the Result value:
@Route(method = HttpMethod.GET, uri = "/manipulate/charset")
public Result charset() {
return ok("<h1>Hello World!</h1>")
.html()
.with(Charset.forName("iso-8859-1"));
}
6.5. Response encoding
Wisdom used to have a "highly sophisticated" in-build mechanism to encode response content. We now rely on the engine to handle this task. Thus all previously used annotations and configurations have been removed. This change has reduced the flexibility, but the benefits in performance justify it. Let us know if this feature was critical for you.
Wisdom can automatically encode response content according to the client Accept-Encoding header.
6.5.1. Vert.x encoding
Wisdom rely on the Vert.X HTTP Server (used by Wisdom) for encoding. It will automatically encode the response.
You can disable this feature globally by setting the following configuration :
vertx.compression: false
You can disable this feature on a specific Result by setting the header X-Wisdom-Disabled-Encoding to true or by using the helper function :
Result.withoutCompression()
7. Content negotiation
Content negotiation is a mechanism that makes it possible to serve different representation of a same resource (URI).
It is useful e.g. for writing Web Services supporting several output formats (XML, JSON, etc.). Server-driven
negotiation is essentially performed using the Accept* requests headers. You can find more information on content
negotiation in the HTTP specification.
7.1. Language
You can get the list of acceptable languages for a request using the request().languages() method that
retrieves them from the Accept-Language header and sorts them according to their quality value. The return array
of locale is sorted from the most 'wanted' language to the less 'wanted' one.
7.2. Content
Similarly, the request().mediaTypes() method gives the list of acceptable result’s MIME types for a
request. It retrieves them from the Accept request header and sorts them according to their quality factor.
The most preferred type can be retrieved using request().mediaType(). In addition, you can test if a
given MIME type is acceptable for the current request using the request().accepts() method:
public Result list() {
if (request().accepts("text/html")) {
return ok(result).html();
} else {
return ok(result).json();
}
}
If you don’t specify the content type of your result, Wisdom tries to serialize your result to an ACCEPT-ed content
type. For instance, in the following example, the result form depends on the ACCEPT header value of the request.
If it’s application/json, result is serialized to JSON, if it’s application/xml, then the XML serializer is
applied.
public Result list() {
return ok(result);
}
7.3. Configure Routes using Accepts and Produces
Each route can specifies the media type is accepts and the media types it produces. These metadata are used by the router to select the right route to invoke for a specific request.
The following snippet shows how routes can specify the media types it produces:
@Route(method= HttpMethod.GET, uri="/", produces = "application/xml")
public Result asXML() {
return ok(map).xml();
}
@Route(method= HttpMethod.GET, uri="/", produces = "application/json")
public Result asJson() {
return ok(map).json();
}
The route is selected according to the client’s ACCEPT header. If none matches, a NOT ACCEPTABLE result is
returned.
Routes can also specify the mime types it can consume:
@Route(method= HttpMethod.POST, uri="/consume",
accepts = "application/xml")
public Result fromXML(@Body Data form) {
return ok(form).xml();
}
@Route(method= HttpMethod.POST, uri="/consume",
accepts = "application/json")
public Result fromJson(@Body Data form) {
return ok(form).json();
}
The route is selected according to the client’s CONTENT-TYPE header. If none matches, an UNSUPPORTED MEDIA TYPE
result is returned.
Routes can combine both parameters and provides several values to each of them:
@Route(method= HttpMethod.POST, uri="/consprod",
accepts = {"application/json", "application/xml"},
produces = "application/json")
public Result fromJsonAsJson(@Body Data form) {
return ok(form).json();
}
Notice that the accepts parameter can receive wildcards such as text/*.
Wisdom adds the VARY header to indicate on which criteria the route has been selected.
|
7.4. Using Negotiation methods
Negotiation can become very complex if you have more than two possibilities. Fortunately, Wisdom provides helper
methods to ease the development of action methods producing several results. The following example
shows how to produce different types of results depending on the Accept HTTP header from the
request.
@Route(method = HttpMethod.GET, uri = "/negotiation/accept")
public Result negotiation() {
return Negotiation.accept(
ImmutableMap.of(
MimeTypes.JSON, ok("{\"message\":\"hello\"}").json(),
MimeTypes.HTML, ok("<h1>Hello</h1>").html()
)
);
}
Notice that the accept method generates a 406 response if the are no suitable possibilities.
To avoid computing all the results, it’s recommended to use async results:
|
@Route(method = HttpMethod.GET, uri = "/negotiation/accept")
public Result negotiation() {
return Negotiation.accept(
ImmutableMap.of(
MimeTypes.JSON, async( () -> ok("{\"message\":\"hello\"}").json(); ),
MimeTypes.HTML, async( () -> ok("<h1>Hello</h1>").html(); )
)
);
}
8. Using Templates
8.1. Template as a Service
Just to makes you understand how the template system works, you should read these small paragraphs.
Wisdom allows the integration of any template engine. By default, Thymeleaf is provided.
Each template is exposed as a service and can be injected into a controller using the @View
annotation. This pattern is used regardless of the template engine you use.
The View annotation is looking for a template with the specified name. It is equivalent to a regular @Requires
with a filter.
8.2. The Thymeleaf Template Engine
Thymeleaf is an XML / XHTML / HTML5 template engine (extensible to other formats) that can work both in web and non-web environments. It is really well suited for serving XHTML/HTML5 at the view layer of web applications, but it can also process any XML file even in offline environments.
The main goal of Thymeleaf is to provide an elegant and well-formed way of creating templates. It allows you to create powerful natural templates, that can be correctly displayed by browsers and therefore work also as static prototypes. One of the main interests of Thymeleaf is a smooth collaboration with a Web Designer. By using specific attributes on HTML elements, you never interfere with the work of the web designers. They create the HTML page with whatever structure, and CSS tricks, and you just identify the parts where you inject the data.
It looks like this. As you can see, it’s a pretty straightforward syntax supporting all the features you expect from a template engine: variables, internationalization, iterations, layouts, fragments, object navigation, and much more.
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod : ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price,1,2)}">0.99</td>
</tr>
</tbody>
</table>
The complete documentation of Thymeleaf is available here.
8.2.1. My First Template
Thymeleaf templates are HTML files with the .thl.html extension, for example: mytemplate.thl.html. The
"mytemplate" is referred as the template name. Templates can be placed in:
-
src/main/resources/templatesand are embedded in the application’s Jar. -
src/main/templatesand are in the distribution but not in the application’s Jar file.
Whether or not your application is intended to be reused in several 'bigger' application determines the place of your template files.
Let’s look at the following template, named welcome.thl.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Follow the Wisdom Path</title>
</head>
<body>
<h1 th:text="${welcome}">Hello</h1>
</body>
</html>
The th:text attribute instructs the template engine to replace the content of the element by the welcome
value. This value is given as parameter to the template engine.
So now in your controller, use the @View annotation to inject the template:
package snippets.controllers.templates;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.annotations.View;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
import org.wisdom.api.templates.Template;
@Controller
public class WelcomeController extends DefaultController {
@View("doc/welcome") (1)
Template template; (2)
@Route(method = HttpMethod.GET, uri = "/templates/welcome")
public Result welcome() {
return ok(render(template, "welcome", "Welcome on the Path to Wisdom")); (3)
}
}
| 1 | Use the @View annotation and indicate the template name |
| 2 | The injected field must use the Template type |
| 3 | Use the render(template, …) methods to render the template |
You can render the template using the render methods:
-
render(Template template)asks the template engine to render the template without input variables -
render(Template template, Map<String, Object> params)inserts the given parameters to the template -
render(Template template, Object… params)proposes an easier way to pass parameters. However, one parameter out of two must be a string (the parameter name), while the following is the value:
return ok(render(template, "welcome", "hello", "age", 1, "colors", new String[] {"red", "blue"}));
The render method is wrapped within an ok method indicating the type of response. Obviously,
you can use any other type of result.
Some values are automatically given to the template engine:
-
the parameters of the request
-
the values stored in the session
-
the value stored in the flash scope
8.2.2. Some Thymeleaf constructions
Now we have seen how template can be used, let’s see some general construction patterns we use in templates:
Variable:
<p th:text="${variable}">this will be replace by the variable</p>
<p th:utext="${variable}">this will be replace by the variable (unescaped so won't remove the
contained HTML elements)
</p>
Internationalized variable:
<p th:text="#{variable}">this will be replaced by the variable using the request locale</p>
| Internationalized values are directly retrieved from the internationalization service from Wisdom. |
Iteration:
You can iterate on collections, arrays, maps…
<!-- Iterate over a list of products -->
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
Conditionals:
<td th:text="${prod.inStock}? 'true' : 'false'">
true or false according to the prod.isStock value
</td>
<a href="#" th:if="${not #lists.isEmpty(prod.comments)}">display this link comments if not empty</a>
Static Methods:
<p th:utext="${@controllers.template.Utils@add(1, 2)}"></p>
<p th:utext="${@org.ietf.jgss.GSSManager@getInstance().toString()}"></p>
| Check the Thymeleaf documentation to find more. |
| Thymeleaf uses the OGNL expression language. Refer to the OGNL language guide. |
8.2.3. Fragments
Templates are generally used to create layouts. Thymeleaf allows creation of fragments, and to include them in the main template.
Fragments are an identified part of a template. For example, the following snippet defines two
fragments: my-content
and sub-content.
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title>This is a fragment</title>
</head>
<body>
<div th:fragment="my-content">
<h1>Hello <span th:text="${username}">my name</span></h1>
</div>
<div th:fragment="sub-content">
<p>This is the first step to Wisdom, follow us...</p>
</div>
</body>
</html>
Fragments can be included either using:
-
th:include: include the fragment 'under' the element using the directive -
th:replace: replace the element using this directive with the fragment content- The value of theses directive is formed by `name_of_the_template_defining_the_fragments
-
fragment_name`. For example, if the previous fragment are defined in
content.thl.html, including them would look like:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8"/>
<title>My Main Page</title>
</head>
<body>
<!-- th:replace replaces the element with the fragment content -->
<div th:replace="content :: my-content"></div>
<!-- th:include injects the fragment as content of the element (so an element below the 'div') -->
<div th:include="content :: sub-content"></div>
<footer>
<!-- something else here -->
</footer>
</body>
</html>
TIPS: fragments can use any variable given to the template engine.
8.2.4. Layout
Thymeleaf also supports layout. This feature is useful to give to your application a common shape. To use this
feature, create a first template (called master) that is the layout, so contains the basic structure of your pages
(headers, footers…):
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8"/>
<link rel="shortcut icon" href="/assets/images/owl-small.png"/>
<script src="/libs/jquery.min.js"></script>
<script src="/libs/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/libs/css/bootstrap.css" media="screen"/>
<link rel="stylesheet" href="/libs/bootswatch-yeti/css/bootstrap.css" media="screen"/>
<title layout:title-pattern="$DECORATOR_TITLE :: $CONTENT_TITLE">Wisdom Monitor</title>
</head>
<body>
<div th:replace="tiles :: topbar"></div>
<div class="container-fluid">
<div class="row">
<div th:replace="tiles :: sidebar"></div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" layout:fragment="content">
This is the content.
</div>
</div>
</div>
</body>
</html>
This template is a regular Thymeleaf template (that can use fragment such as titles::topbar), but notice the
layout:fragment="content" indicating that the template using this layout will provide the actual "content" of this
element.
Then, create the templates inheriting from the layout:
<html layout:decorator="master">
<head lang="en">
<title>iPOJO</title>
<link rel="stylesheet" href="/assets/table.css"/>
<link href="/assets/dashboard.css" rel="stylesheet"/>
</head>
<body>
<div layout:fragment="content">
<!-- the actual content goes there -->
<h1 class="page-header">iPOJO</h1>
<div class="container">
<!-- .... -->
</div>
In the html element, notice the layout:decorator indicating the layout template (here it’s master). Then, the
content fragment is indicated using layout:fragment="content" in the div element that will contain the content.
More details about the thymeleaf layout system are available here: https://github.com/ultraq/thymeleaf-layout-dialect.
8.2.5. Using the router from the template
A template can use a special object named #routes to retrieve the url of action methods. In the following snippet,
the action attribute is assigned to the url of the upload method of the current controller (the controller having
requested the template rendering).
th:attr="action=${#routes.route('upload')}"
You can also ask for the url of an action method receiving parameters:
th:attr="action=${#routes.route('upload', 'param', 'value', 'param2', 'value2')}"
Finally, you can target methods from other controllers with:
th:attr="action=${#routes
.route(`org.acme.controller.MyOtherController`, 'upload', 'param', 'value')}"
If the route cannot be found, the template rendering fails.
The routes object is also able to compute the URL of assets:
<link th:href="${#routes.asset('css/bootstrap.min.css')}" rel="stylesheet"/>
<link th:href="${#routes.asset('/css/bootstrap-theme.min.css')}" rel="stylesheet"/>
<script th:src="${#routes.asset('/jquery.js')}"></script>
<script th:src="${#routes.asset('js/bootstrap.min.js')}"></script>
It locates the assets in the assets directories and in webjars. With such a functionality, you don’t need to write the complete urls. In addition, if an asset cannot be located, the template rendering fails.
8.2.6. Using the HTTP Context, session, flash and request
Like you can access the Wisdom router from the template, you can also inject the following objects:
-
the current HTTP context (using the
httpvariable) -
the session
-
the flash scope
-
the request object
<div>
<ul>
<li><span>From Session</span> = <span th:text="${#session.get('value')}">VALUE</span></li>
<li><span>From Flash</span> = <span th:text="${#flash.get('value')}">VALUE</span></li>
<li><span>From Request</span> = <span th:text="${#request.data().get('value')}">VALUE</span></li>
</ul>
</div>
8.2.7. Extending Thymeleaf
The Thymeleaf Template language can be extended using dialects. Wisdom tracks these dialects from the service registry and enhanced the template facilities dynamically. So to extend the template language, just implement your own dialect such as:
/**
* An example of custom dialect.
*/
@Service
public class MyDialect extends AbstractDialect {
@Override
public String getPrefix() {
return "my";
}
@Override
public Set<IProcessor> getProcessors() {
return ImmutableSet.<IProcessor>of(new SayToAttrProcessor());
}
private class SayToAttrProcessor
extends AbstractTextChildModifierAttrProcessor {
public SayToAttrProcessor() {
super("sayto");
}
public int getPrecedence() {
return 10000;
}
@Override
protected String getText(final Arguments arguments, final Element element,
final String attributeName) {
return "Hello, " + element.getAttributeValue(attributeName) + "!";
}
}
}
To go further, check the Thymeleaf extension page.
9. Session and Flash scopes
9.1. How it is different in Wisdom
As in the Play Framework, Wisdom makes a distinction between the session and flash scope. If you have to keep data across multiple HTTP requests, you can save them in the Session or the Flash scope. Data stored in the Session is available during the whole user session, and data stored in the flash scope is only available to the next request.
It’s important to understand that Session and Flash data are not stored in the server but are added to each subsequent HTTP Request, using Cookies. This means that the data size is very limited (up to 4 KB) and that you can only store string values.
Cookies are signed with a secret key so the client can’t modify the cookie data (or it will be invalidated). The Wisdom session is not intended to be used as a cache. If you need to cache some data related to a specific session, you can use the Wisdom Cache Service and use the session to store a unique ID to associate the cached data with a specific user.
There is no technical timeout for the session, which expires when the user closes the web browser. If you need a functional timeout for a specific application, just store a timestamp into the user Session and use it however your application needs (e.g. for a maximum session duration, maximum inactivity duration, etc.).
9.2. Reading a Session value
You can retrieve the incoming Session from the HTTP request:
@Route(method= HttpMethod.GET, uri = "/session")
public Result readSession() {
String user = session("connected");
if(user != null) {
return ok("Hello " + user);
} else {
return unauthorized("Oops, you are not connected");
}
}
9.3. Storing data into the Session
As the Session is just a Cookie, it is also just an HTTP header, but Wisdom provides a helper method to store a session value:
@Route(method= HttpMethod.GET, uri = "/session/login")
public Result login() {
String user = session("connected");
if(user != null) {
return ok("Already connected");
} else {
session("connected", "wisdom");
return readSession();
}
}
The same way, you can remove any value from the incoming session:
@Route(method= HttpMethod.GET, uri = "/session/logout")
public Result logout() {
session().remove("connected");
return ok("You have been logged out");
}
9.4. Discarding the whole session
If you want to discard the whole session, there is special operation:
@Route(method= HttpMethod.GET, uri = "/session/clear")
public Result clear() {
session().clear();
return ok("You have been logged out");
}
9.5. Flash scope
The Flash scope works exactly like the Session, but with two differences:
-
data are kept for only one request
-
the Flash cookie is not signed, making it possible for the user to modify it.
| The flash scope should only be used to transport success/error messages on simple non-Ajax applications. As the data is just kept for the next request and because there are no guarantees to ensure the request order in a complex Web application, the Flash scope is subjected to race conditions. |
Here are a few examples using the Flash scope:
@Route(method= HttpMethod.GET, uri = "/session/flash")
public Result welcome() {
String message = flash("success");
if(message == null) {
message = "Welcome!";
flash("success", message);
}
return ok(message);
}
10. Handling and serving JSON requests
10.1. Handling a JSON request
A JSON request is an HTTP request using a valid JSON payload as request body. Its Content-Type header must specify
the text/json or application/json MIME type.
An action can retrieve the raw body content, or can ask Wisdom to convert it to an object, such as a JSON Node object in the following example:
@Route(method = HttpMethod.POST, uri = "/json/hello")
public Result hello() {
JsonNode json = context().body(JsonNode.class);
if (json == null) {
return badRequest("Expecting Json data");
} else {
String name = json.findPath("name").textValue();
if (name == null) {
return badRequest("Missing parameter [name]");
} else {
return ok("Hello " + name);
}
}
}
Of course, it’s much better to use the @Body annotation:
@Route(method = HttpMethod.POST, uri = "/json/hello2")
public Result helloWithBody(@NotNull @Body JsonNode json) {
String name = json.findPath("name").textValue();
if (name == null) {
return badRequest("Missing parameter [name]");
} else {
return ok("Hello " + name);
}
}
In this example, the body is automatically mapped to a JsonNode. The @NotNull annotation manages the case where
the body is empty. A bad request result will be sent back.
A third, even better, way to receive Json content is to use a validated bean/structure instead of a JsonNode:
@Route(method = HttpMethod.POST, uri = "/json/hello3")
public Result helloWithBodyUsingBean(@Valid @Body Person person) {
return ok("Hello " + person.name);
}
The structure contains validation annotation, constraining the received data. For any non compliant content, a bad request will be returned. In addition, this way also accepts requests with other types of content (such as form input).
10.2. Serving a JSON response
In our previous examples we handled requests with a JSON body, but replied with a text/plain response. Let’s change
that to send back a valid JSON HTTP response:
@Requires Json json; (1)
@Route(method = HttpMethod.POST, uri = "/json/hello4")
public Result helloReturningJsonNode(@Valid @Body Person person) {
ObjectNode result = json.newObject(); (2)
result.put("name", person.name);
result.put("message", "hello " + person.name);
return ok(result); (3)
}
| 1 | First, access the Wisdom Json service using the @Requires annotation. |
| 2 | Create a new (Jackson) ObjectNode from the retrieves Json service |
| 3 | Just pass the created object in the ok method |
Invoked with the {"name":"wisdom"} payload, the action returns:
{
"name" : "wisdom",
"message" : "hello wisdom"
}
However, building your own Json object can be very annoying and cumbersome. Fortunately, Wisdom builds a Json representation from an object:
private class Response { (1)
public final String name;
public final String message;
private Response(String name) {
this.name = name;
this.message = "hello " + name;
}
}
@Route(method = HttpMethod.POST, uri = "/json/hello5")
public Result helloReturningJsonObject(@Valid @Body Person person) {
Response response = new Response(person.name);
return ok(response).json(); (2)
}
| 1 | Define the structure you want to return |
| 2 | Pass the created object to the ok method and invoke the json method |
10.3. Extending Json support with your own serializer and deserializer
Wisdom relies on Jackson to handle the JSON requests and responses. Despite the provided default serialization and de-serialization processes, you may need to extend it with your own mapping. Wisdom lets you extend Jackson by registering Jackson’s modules containing serializers and deserializers.
To achieve this, you have two ways:
-
expose a
com.fasterxml.jackson.databind.module.SimpleModuleservice -
use the
JacksonModuleRepositoryservice (the pre 0.7 way)
10.3.1. Expose a Module service
The first way is straightforward. You just need to implement a Service exposing the com.fasterxml.jackson.databind.module.SimpleModule class:
package snippets.controllers.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.wisdom.api.annotations.Service;
import java.io.IOException;
/**
* An example of Module service to customize the JSON serialization and de-serialization process.
*/
@Service(Module.class) (1)
public class MyJsonModuleService extends SimpleModule {
public MyJsonModuleService() {
super("My Json Module"); (2)
addSerializer(Car.class, new JsonSerializer<Car>() { (3)
@Override
public void serialize(Car car, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("custom", "customized value");
jsonGenerator.writeStringField("color", car.getColor());
jsonGenerator.writeStringField("brand", car.getBrand());
jsonGenerator.writeStringField("model", car.getModel());
jsonGenerator.writeNumberField("power", car.getPower());
jsonGenerator.writeEndObject();
}
});
addDeserializer(Car.class, new JsonDeserializer<Car>() { (4)
@Override
public Car deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
String color = node.get("color").asText();
String brand = node.get("brand").asText();
String model = node.get("model").asText();
int power = node.get("power").asInt();
return new Car(brand, model, power, color);
}
});
}
}
| 1 | Use the @Service annotation and specify that you want to expose the Module class as a service |
| 2 | If you extends SimpleModule, call the super constructor with your module name |
| 3 | Register the serializer |
| 4 | Register your deserializer |
Unfortunately, this approach lets you register only one module at a time, that’s why the second way give you more flexibility.
10.3.2. Using the Jackson Module Repository service
This second way let you register several modules from a single component. Your custom serializers and deserializers must be registered within a module and then registered to the JacksonModuleRepository service (provided by Wisdom itself):
package snippets.controllers.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.felix.ipojo.annotations.*;
import org.wisdom.api.content.JacksonModuleRepository;
import java.io.IOException;
/**
* An example of Jackson module provider.
*/
@Component
@Instantiate
public class MyJsonModuleProvider {
@Requires
JacksonModuleRepository repository; (1)
private final SimpleModule module;
public MyJsonModuleProvider() { (2)
module = new SimpleModule("My Json Module");
module.addSerializer(Car.class, new JsonSerializer<Car>() {
@Override
public void serialize(Car car, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("custom", "customized value");
jsonGenerator.writeStringField("color", car.getColor());
jsonGenerator.writeStringField("brand", car.getBrand());
jsonGenerator.writeStringField("model", car.getModel());
jsonGenerator.writeNumberField("power", car.getPower());
jsonGenerator.writeEndObject();
}
});
module.addDeserializer(Car.class, new JsonDeserializer<Car>() {
@Override
public Car deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
String color = node.get("color").asText();
String brand = node.get("brand").asText();
String model = node.get("model").asText();
int power = node.get("power").asInt();
return new Car(brand, model, power, color);
}
});
}
@Validate
public void start() {
repository.register(module); (3)
}
@Invalidate
public void stop() {
repository.unregister(module); (4)
}
}
| 1 | Retrieve the MyJsonModuleProvider service |
| 2 | In your constructor, build your module with a set of serializers and deserializers. |
| 3 | Registers your module into the repository |
| 4 | Don’t forget to unregister the module from the repository |
10.4. JSONP
JSON with Padding (JSONP) is a communication technique to request data from a server in a different domain, something prohibited by typical web browsers because of the same-origin policy. To use JSONP, the server must be able to generate a JSONP response, and you know what ? Wisdom knows how to do that.
Basically a JSONP response is a JavaScript code fragment structure as follows: padding({the data});. The padding is
the name of the method (callback) that will be called in the JavaScript code having requested this response.
There are several ways to generate a JSONP response:
-
using the
ok(padding, json node)method -
using the Json Service
@Requires
Json json;
@Route(method = HttpMethod.GET, uri = "/render")
public Result usingRender(@Parameter("callback") String callback) {
JsonNode node = json.parse("{ \"foo\": \"bar\" }");
return ok(callback, node);
}
The previous snippet uses the first way to get a JSONP response. It builds a JSON Node and
creates a JSONP result object. The mime type of the response is set to text/javascript. The
previous code generates callback({"foo":"bar"});.
JSONP responses can be generated from the JSON service too:
@Route(method = HttpMethod.GET, uri = "/json")
public Result usingJsonService(@Parameter("callback") String callback) {
return ok(json.toJsonP(callback, json.newObject().put("foo", "bar"))).as(MimeTypes.JAVASCRIPT);
}
@Route(method = HttpMethod.GET, uri = "/user")
public Result user(@Parameter("callback") String callback) {
return ok(json.toJsonP(callback, new User(1, "wisdom", ImmutableList.of("coffee",
"whisky")))).as(MimeTypes.JAVASCRIPT);
}
The json.toJSONP method generates the JSONP response from the given callback name (padding) and the given content.
This approach is convenient as you can map business objects into JSON directly (as shown in the second example).
However it requires setting the JavaScript mime type explicitly.
10.5. Configuring the JSON support
Wisdom JSON feature relies on Jackson. You can enable or disable Jackson features from the application.conf file.
Jackson features are listed on https://github.com/FasterXML/jackson-databind/wiki/JacksonFeatures. By default, the
FAIL_ON_UNKNOWN_PROPERTIES is disabled.
Here is an example of configuration:
jackson {
INDENT_OUTPUT: true
EAGER_DESERIALIZER_FETCH: true
FAIL_ON_UNKNOWN_PROPERTIES: false
}
11. Handling file uploads
11.1. Uploading files in a form using multipart/form-data
The standard way to upload files in a web application is to use a form with a special multipart/form-data encoding,
which allows to mix standard form data with file attachments.
Start by writing an HTML form:
<!DOCTYPE html>
<html>
<head>
<title>Wisdom - File Upload</title>
</head>
<body>
<form action="#" enctype="multipart/form-data" method="POST" th:attr="action=${#routes.route('upload')}">
<fieldset>
<input type="file" name="upload" />
<input type="submit" value="Upload"/>
</fieldset>
</form>
</body>
</html>
Now let’s define the upload action:
@Route(method = HttpMethod.POST, uri = "/")
public Result upload(@FormParameter("upload") FileItem uploaded) {
return ok("File " + uploaded.name() + " of type " + uploaded.mimetype() +
" uploaded (" + uploaded.size() + " bytes)");
}
The uploaded file is accessed using the @Attribute("upload") FileItem uploaded. upload is the name of the field used
in the upload form. From the file item, you can:
-
get the file name
-
get the file size and mime type
-
get the content of the file as a byte array (will load the content in memory), or as a stream
| Wisdom may decide to store the uploaded file to a temporary directory, but this depends on the file size and current load of the server. If you want to store the file on disk, you must copy the file by yourself. |
11.2. Direct file upload
Another way to send files to the server is to use Ajax to upload files asynchronously from a form. In this case,
the request body will not be encoded as multipart/form-data, but will just contain the plain file contents.
@Route(method = HttpMethod.POST, uri = "/ajax")
public Result ajax() throws IOException {
byte[] content = IOUtils.toByteArray(context().reader());
return ok(content.length + " bytes uploaded");
}
The context().getReader() method lets you access the raw content of the request. content().body() returns the raw
content as a String.
12. Handling asynchronous results
12.1. Why asynchronous results?
Until now, we have been able to compute the result to send to the web client directly. This is not always the case: the result may depend on an expensive computation or on a long web service call.
Because of the way Wisdom works, action code must be as fast as possible (i.e. non blocking). So what should we return from our action if we are not yet able to compute the result? We should return an asynchronous result, i.e. a result that will be computed in the background and sent to the client when it’s done!
An asynchronous result will eventually be redeemed with a value of type Result. By using a AsyncResult instead of
a normal Result, we are able to return from our action quickly without blocking anything. Wisdom will then serve
the result as soon as the promise is redeemed.
The web client will be blocked while waiting for the response but nothing will be blocked on the server, and server resources can be used to serve other clients.
NOTE:*Why is it important to not block the server?* Wisdom is based on a NIO model and uses very few threads. Blocking one of them drastically reduces the velocity of the server.
12.2. How to create an Asynchronous Result ?
To create an AsyncResult, you just need to write the heavy computation in a Callable<Result> object:
@Route(method = HttpMethod.GET, uri = "/async")
public Result async() {
return new AsyncResult(new Callable<Result>() {
@Override
public Result call() throws Exception {
// heavy computation here...
return ok("Computation done");
}
});
}
The method signature must still return a Result
|
You can also use the @Async annotation to make a regular action method as an asynchronous method:
@Route(method = HttpMethod.GET, uri = "/async")
@Async
public Result regular() {
return ok("Computation done");
}
The @Async annotation let you configure a timeout. If the timeout is reached, and the result is still not computed, an error result is returned to the client. The default unit is TimeUnit.SECONDS.
|
13. Streaming HTTP responses
13.1. Standard responses and Content-Length header
Since HTTP 1.1, to keep a single connection open to serve several HTTP requests and responses,
the server must send the appropriate Content-Length HTTP header along with the response.
By default, when you send a simple result, such as:
public Result hello() {
return ok("Hello!");
}
You are not specifying a Content-Length header. Of course, because the content you are sending is well known,
Wisdom is able to compute the content size for you and to generate the appropriate header.
Note that for text-based content this is not as simple as it looks, since the Content-Length header must be computed
according the encoding used to translate characters to bytes.
To be able to compute the Content-Length header properly, Wisdom must consume the whole response data and load its
content into memory. Obviously it has some drastic limitations.
13.2. Serving files
If it’s not a problem to load the whole content into memory for simple content what about a large data set? Let’s say we want to send back a large file to the web client.
Wisdom provides methods to ease the manipulation of file:
@Route(method = HttpMethod.GET, uri = "/movie")
public Result file() {
return ok(new File("/tmp/myMovie.mkv"));
}
| Files are sent chunk by chunk avoiding copying the whole file in memory. This means you can serve very large files. See Chunked Transfer Encoding for more details. |
Additionally this helper will also compute the Content-Type header from the file name.
If you want to force the browser to download the file (and not just display it), use the following version to set the
Content-Disposition header.
@Route(method = HttpMethod.GET, uri = "/pdf")
public Result fileAsAttachment() {
return ok(new File("/tmp/wisdom.pdf"), true);
}
When you passe true as second argument, Wisdom sets the Content-Disposition header to Content-Disposition:
attachment; filename=wisdom.pdf.
13.3. Serving URLs
Wisdom also supports serving urls directly. Handling URLs is a bit more complicated as the size in unknown, and so chunk transfer is required. Don’t worry Wisdom handles everything for you:
@Route(method = HttpMethod.GET, uri = "/perdu")
public Result url() throws MalformedURLException {
return ok(new URL("http://perdu.com")).html();
}
Serving URLs is useful when loading resources packaged with the application:
@Route(method = HttpMethod.GET, uri = "/manifest")
public Result manifest() throws MalformedURLException {
URL manifest = this.getClass()
.getClassLoader().getResource("/META-INF/MANIFEST.MF");
return ok(manifest).as(MimeTypes.TEXT);
}
As for files, Wisdom tries to deduce the Content-Type from the URL. However, as shown, it can be overridden.
| As the content size in unknown, the web browser is not able to display a proper download progress bar. |
13.4. Serving Input Streams
You can also return input streams directly. As for URLs, the content length is unknown and so it will be transferred
chunk by chunk. Unlike URLs and Files, Wisdom does not try to determine the Content-Type so,
you must set it explicitly:
@Route(method = HttpMethod.GET, uri = "/manifest-as-stream")
public Result manifestAsStream() throws MalformedURLException {
InputStream is = this.getClass()
.getClassLoader().getResourceAsStream("/META-INF/MANIFEST.MF");
return ok(is).as(MimeTypes.TEXT);
}
13.5. Avoiding chunks
If you don’t want to transfer the data using chunks, you can use the following variants:
ok(new RenderableFile(file, false));
ok(new RenderableURL(url, false));
ok(new RenderableStream(stream, false));
Avoiding chunks means copying the whole data into memory, this has a major impact on the memory used by your
server and may lead to OutOfMemory issues.
|
14. Web Sockets and SockJS
14.1. What are web sockets
WebSocket is a protocol providing full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011, and the WebSocket API in Web IDL is being standardized by the W3C.
When a client (Browser) wants to establish a connection with a server using a web socket, it starts by a handshake. Once done, a tunnel connects the client and the server. So, the server can push data to the client and vice-versa. Such a mechanism paves the way to more reactive web applications, where notifications and up to date data are pushed to the client without having to rely on ajax or long polling.
WebSocket are identified using urls. These urls starts either by ws:// or wss://.
14.2. Receiving data
A controller willing to listen for data sent by clients on a specific web socket has to use the @onMessage annotation:
@OnMessage("/socket")
public void onMessage(@Body String message) {
logger().info("Message received : {}", message);
}
Every time a client sends data on ws:///localhost:9000/socket, the callback is called. Notice
the @Body annotation
parsing the message to the parameter’s type (here as String). The @Body annotation works the same way as in action
methods:
@OnMessage("/jsonSocket")
public void onJsonMessage(@Body Data data) {
logger().info("Data received : name: {}, age: {}", data.name, data.age);
}
The web socket URI provided in the @OnMessage annotation’s parameter can contain a dynamic part
as for action methods:
@OnMessage("/socket/{name}")
public void messageOnSeveralSockets(@Parameter("name") String name, @Body String message) {
logger().info("Message received on {} : {}", name, message);
}
The @Parameter annotation let you retrieve the dynamic parts.
Finally, you can identify the client sending the data using a special parameter named client:
@OnMessage("/socket")
public void identifyClient(@Parameter("client") String client, @Body String message) {
logger().info("Message received from {} : {}", client, message);
}
Be aware that the client identifier changes if the user disconnects and reconnects.
|
14.3. Send data to a specific client
Now that we can receive data from the client, it would be nice to push data to it.
@Requires
Publisher publisher;
@OnMessage("/echo")
public void echo(@Parameter("client") String client, @Body String message) {
logger().info("Message received : {}", message);
publisher.send("/echo", client, message.toUpperCase());
}
Two important things here:
-
The
publisheris a service (provided by Wisdom) responsible for sending data to web socket clients. -
We use the
sendmethod pushing data to a specific client
So, the previous snippet would produce such a kind of conversation:
client >>> hello >>> server client <<< HELLO <<< server
14.4. Send data to all clients
The previous example sends data specifically to one client. However, data can be broadcast to all clients connected to a specific web socket:
@OnMessage("/echo-all")
public void echoAll(@Body String message) {
logger().info("Message received : {}", message);
publisher.publish("/echo-all", message.toUpperCase());
}
The main difference is the usage of the publish method instead of send.
14.4.1. Sending Json or binary data
So far, we have only sent String messages. However, you can send or publish binary data too:
@Every("1h")
public void binary() {
byte[] bytes = new byte[5];
random.nextBytes(bytes);
logger().info("Message dispatching binary : {}", bytes);
publisher.publish("/binary", bytes);
}
By the way, notice that this method is not an OnMessage callback, but a method executed every hour.
You can also send JSON messages directly too:
@Requires
Json json;
@Every("1h")
public void sendJson() {
publisher.publish("/ws/json", json.newObject().put("name", "hello").put("date", System.currentTimeMillis()));
}
14.5. Being notified of client connections and disconnections
In addition to OnMessage, there are two other annotations useful to know when clients connect and disconnect from
the listened socket:
@Opened("/ws/json")
public void opened(@Parameter("client") String client) {
logger().info("Client connection on web socket {}", client);
}
@Closed("/ws/json")
public void closed(@Parameter("client") String client) {
logger().info("Client disconnection on web socket {}", client);
}
@Opened and @Closed callbacks can also use URI with dynamic parts too. To retrieve the identifier of the client,
just use @Parameter("client").
15. Action Interception
When Wisdom processes a request, it searches for the route to invoke and, if found, invokes it. However, the invocation can be intercepted, letting filters and interceptors do stuff before and/or after the actual invocation.
15.1. Filters vs. Interceptors
Wisdom proposes two types of interceptors:
-
Filters select the request to intercept using urls
-
Interceptors are configured by the controllers
Both filters and interceptors are services. Filters are services independent from the running application. They intercept all requests matching a regex (provided by the filter). On the other hand, interceptors are configured by the controllers. The set of interceptors called for an action is specified by the action itself or by the controller using the annotations bound to interceptors. When several interceptors are declared, a chain is computed and is called sequentially:
Both filters and interceptors are enqueued in the interception chain. Filters are executed first, and then interceptors. When several filters match the incoming requests, the order is determined using their priority (highest first).
Filters can intercept unbound requests, i.e. requests that do not have a matching action method, or all actions having thrown an exception. Such interceptions are not feasible using interceptors.
15.2. Creating a filters
A filter is a component implementing org.wisdom.api.interception.Filter and exposing it as a service. Here is an
example of a filter measuring the time spent to compute the response to a request:
package snippets.interceptors;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Instantiate;
import org.apache.felix.ipojo.annotations.Provides;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wisdom.api.http.Result;
import org.wisdom.api.interception.Filter;
import org.wisdom.api.interception.RequestContext;
import org.wisdom.api.router.Route;
import java.util.regex.Pattern;
/**
* A filter measuring the time spent to compute the time spent to handle an action
*/
@Component
@Provides
@Instantiate
public class TimeFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(TimeFilter.class.getName());
private static final Pattern REGEX = Pattern.compile("/documentation.*");
@Override
public Result call(Route route, RequestContext context) throws Exception {
final long begin = System.currentTimeMillis();
try {
return context.proceed();
} finally {
final long end = System.currentTimeMillis();
LOGGER.info("Time spent to reply to {} {} : {} ms",
route.getHttpMethod(), context.context().path(),
(end - begin));
}
}
/**
* Gets the Regex Pattern used to determine whether the route
* is handled by the filter or not.
* Notice that the router are caching these patterns and so
* cannot changed.
*/
@Override
public Pattern uri() {
return REGEX;
}
/**
* Gets the filter priority, determining the position of the
* filter in the filter chain. Filter with a high
* priority are called first. Notice that the router are caching
* these priorities and so cannot changed.
* <p/>
* It is heavily recommended to allow configuring the priority
* from the Application Configuration.
*
* @return the priority
*/
@Override
public int priority() {
return 100;
}
}
The call method is the method intercepting the request. It should, in the very high majority of the cases, call the
proceed method to continue the chain. If it does not, the chain is cut, and no other filters or
interceptors
will be called.
The uri method returns the regex selecting the intercepted urls. In the example, all requests
pointing to
"/documentation" are intercepted.
The priority method allows configuring the position of the filters in the chain if there are several matching
filters. Filters with the highest priorities are first. Notice that the default error page filter
from Wisdom (displaying
the error pages and route not found pages) has a priority of 1000.
Filters can come and go at anytime, promoting a very dynamic model. So, you can intercept requests targeting controllers not developed by you (such as the asset controller), or deploy a temporary filter to understand a specific issue.
15.3. Creating an interceptor’s annotation
Developing an interceptor requires an annotation and the interceptor itself. Interceptors are configured using an
annotation bound to the interceptor. This annotation uses the @Interception meta-annotation specifying that an
interceptor is consuming the annotation:
package snippets.interceptors;
import org.wisdom.api.annotations.Interception;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Just logged requests.
*/
@Interception
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logged {
boolean duration() default true;
}
The previous snippet shows an interceptor annotation that will be handled by an interceptor. The annotation must be visible at runtime, and can target both types and methods (this means they can be used either on classes or on methods).
| when used on classes, the interceptor is applied to all contained actions. |
| The package containing the annotation must be exported to be usable by Wisdom components not included in the current project. |
15.4. Implementing the interceptor
Interceptors are Wisdom components registered as OSGi services. Don’t worry, Wisdom makes that easy:
@Component
@Provides(specifications = Interceptor.class)
@Instantiate
public class LoggerInterceptor extends Interceptor<Logged> {
Logger logger = LoggerFactory.getLogger(LoggerInterceptor.class);
@Override
public Result call(Logged configuration, RequestContext context)
throws Exception {
logger.info("Invoking " + context.context().request().method() +
" " + context.context().request().uri());
long begin = System.currentTimeMillis();
Result result = context.proceed();
long end = System.currentTimeMillis();
if (configuration.duration()) {
logger.info("Result computed in " + (end - begin) + " ms");
}
return result;
}
@Override
public Class<Logged> annotation() {
return Logged.class;
}
}
First, the class is annotated with three annotations:
@Component
@Provides
@Instantiate
This instructs the framework to expose the OSGi service.
Then, the class contains two main methods:
-
callintercepting the action invocation -
annotationreturning the class of the handled annotation
The annotation method is straightforward. It just returns the class of the handled annotation.
The call method is intercepting the action invocation. It invokes the context.proceed() method to call the next
interceptor. It can also return a Result object immediately, shortcutting the chain. The call method receives the
RequestContext object and the actual interceptor configuration. The RequestContext let the interceptor to:
-
get the current HTTP Context with the
context()method -
get the current HTTP Request with the
request()method -
get the current route with the
route()method -
set data consumed by other interceptor and filters using the
data()method
15.5. Annotating controllers
As said, interceptor annotation can target either classes or methods:
@Controller
@Logged(duration = true)
public class MyController extends DefaultController {
@Route(method= HttpMethod.GET, uri = "/intercepted")
@Logged(duration = true)
public Result action() {
//...
return ok();
}
15.6. Sharing data between filters and interceptor of a chain
Filters and interceptors can share data when they are on the same chain (i.e. intercepting the same request). The
RequestContext object hold the shared data:
context.data().put("my-data", "hello world");
This data can be consumed by the filters and interceptors invoked after the entity having set the data.
16. Working with assets
Assets are files delivered by your application or by Wisdom. It can be CSS, JavaScript, HTML files or resources such as images.
Wisdom supports three types of assets:
-
Assets packaged with the application, they are placed in the
src/main/resources/assetsdirectory -
Assets served by wisdom but not included in the application, they are placed in the
src/main/assetsdirectory, and copied to thewisdom/assetsdirectory -
Assets packages as webjars.
The list of all available assets is available on /assets (http://localhost:9000/assets). This page is only served
in development mode.
16.1. Structuring your assets
We strongly recommend that you use the following conventions to organize your assets:
assets
└ javascripts
└ stylesheets
└ images
16.2. How your assets are packaged and served
The assets from src/main/resources/assets are packaged in the application’s jar file (don’t forget it’s an OSGi
bundle).The assets from src/main/assets are placed in the wisdom/assets directory and packaged in the distribution zip
file. Despite this difference, both types of assets are accessible from the /assets/ path.
Let’s see some examples:
src/main/assets/javascripts/script.js ==> /assets/javascripts/script.js src/main/assets/stylesheets/style.css ==> /assets/stylesheets/style.css src/main/resources/assets/javascripts/script.js ==> /assets/javascripts/script.js src/main/resources/assets/stylesheets/style.css ==> /assets/stylesheets/style.css
When an asset file is present from both sources, the asset from src/main/assets is served. This policy
lets you override internal files with external assets.
|
Wisdom contains a built-in controller to serve the assets. By default, this controller provides caching, ETag, and gzip compression.
16.3. Etag support
The Assets controller automatically manages ETag HTTP Headers. The ETag value is generated from the file’s last modification date. (If the resource file is embedded into a file, the JAR file’s last modification date is used.)
When a web browser makes a request specifying this Etag, the server can respond with 304 NotModified,
without body. By this method, bandwidth is saved.
You can disable the Etag support by setting http.useETag to false in the application configuration:
http.useETag = false
By default, Etag is enabled.
16.4. Cache Control
In addition to the Etag support, Wisdom lets you configure the asset cache time. More precisely the value set by
Wisdom in the Cache-Control HTTP header. By setting the http.cache_control_max_age configuration property in the
application configuration, you can set the amount of time (in seconds) the browser can keep the resource in its own
cache:
http.cache_control_max_age = 3600
To disable the cache, set this value to 0:
http.cache_control_max_age = 0
This configuration instructs Wisdom to use the no-cache value, forbidding the browser and other proxies to use
caching facilities.
By default, the cache age is set to 3600 seconds.
16.5. Asset processing
Before being packaged, assets are processed. For example, CoffeeScript files are compiled to JavaScript,
while Less files are compiled to CSS. This processing is done during the build process (and not at runtime).
16.6. CoffeeScript processing
CoffeeScript is a little language that compiles into JavaScript. Underneath that awkward
Java-esque patina, JavaScript has always had a gorgeous heart. CoffeeScript is an attempt to expose the good parts of
JavaScript in a simple way. Any .coffee files from your assets are compiled to Javascript automatically. The
source map is also generated. For example, the file src/main/assets/javascripts/some-coffee.coffee:
song = ["do", "re", "mi", "fa", "so"]
singers = {Jagger: "Rock", Elvis: "Roll"}
bitlist = [
1, 0, 1
0, 0, 1
1, 1, 0
]
kids =
brother:
name: "Max"
age: 11
sister:
name: "Ida"
age: 9
is accessible from /assets/javascripts/some-coffee.js:
// Generated by CoffeeScript 1.10.0
(function() {
var bitlist, kids, singers, song;
song = ["do", "re", "mi", "fa", "so"];
singers = {
Jagger: "Rock",
Elvis: "Roll"
};
bitlist = [1, 0, 1, 0, 0, 1, 1, 1, 0];
kids = {
brother: {
name: "Max",
age: 11
},
sister: {
name: "Ida",
age: 9
}
};
}).call(this);
//# sourceMappingURL=some-coffee.js.map
In watch mode, the .coffee files are automatically recompiled.
|
The CoffeeScript compilation relies on the original compiler executed on top of the node.js runtime.
16.7. Typescript processing
TypeScript lets you write JavaScript the way you really want to. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
Any .ts files from your assets are compiled to Javascript automatically. The source maps are also generated. For
example, the file src/main/assets/typescript/Animal.ts:
class Animal {
constructor(public name: string) { }
move(meters: number) {
alert(this.name + " moved " + meters + "m.");
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move() {
alert("Slithering...");
super.move(5);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move() {
alert("Galloping...");
super.move(45);
}
}
var sam = new Snake("Sammy the Python");
var tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
is accessible from /assets/javascripts/Animal.js:
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Animal = (function () {
function Animal(name) {
this.name = name;
}
Animal.prototype.move = function (meters) {
alert(this.name + " moved " + meters + "m.");
};
return Animal;
})();
var Snake = (function (_super) {
__extends(Snake, _super);
function Snake(name) {
_super.call(this, name);
}
Snake.prototype.move = function () {
alert("Slithering...");
_super.prototype.move.call(this, 5);
};
return Snake;
})(Animal);
var Horse = (function (_super) {
__extends(Horse, _super);
function Horse(name) {
_super.call(this, name);
}
Horse.prototype.move = function () {
alert("Galloping...");
_super.prototype.move.call(this, 45);
};
return Horse;
})(Animal);
var sam = new Snake("Sammy the Python");
var tom = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
//# sourceMappingURL=Animal.js.map
In watch mode, the .ts files are automatically recompiled.
|
The TypeScript compilation relies on the original compiler executed on top of the node.js runtime. The compilation
process is configured from the typescript element in your pom.xml file such as:
<typescript>
<removeComments>true</removeComments>
<module>AMD</module>
<target>ES5</target>
</typescript>
Are supported:
-
enabled(boolean) : enables / disables the typescript processing (enabled by default) -
version: the Typescript compiler version -
generateDeclaration(boolean) : enables/ disables corresponding '.d.ts' file (enabled by default) -
generateMap(boolean) : enables / disables corresponding '.map' file (enabled by default) -
removeComments(boolean) : strip out comment from the generated javascript files (disabled by default) -
mapRoot: Specifies the location where debugger should locate map files instead of generated locations. -
module: Specify module code generation: 'commonjs' or 'amd' -
target: Specify ECMAScript target version: 'ES3' (default), 'ES5', or 'ES6' (experimental) -
noImplicitAny(boolean) : Raise error on expressions and declarations with an implied 'any' type (disabled by default) -
otherArguments(list) : Other arguments you want to pass to thetsccompiler.
16.8. Less processing
LESS extends CSS with dynamic behavior such as variables, mixins,
operations and functions. Wisdom automatically compiles any .less files from your assets to CSS. For example,
the file src/main/assets/stylesheets/site.less:
@base: #f938ab;
.box-shadow(@style, @c) when (iscolor(@c)) {
-webkit-box-shadow: @style @c;
-moz-box-shadow: @style @c;
box-shadow: @style @c;
}
.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) {
.box-shadow(@style, rgba(0, 0, 0, @alpha));
}
.box {
color: saturate(@base, 5%);
border-color: lighten(@base, 30%);
div { .box-shadow(0 0 5px, 30%) }
}
is accessible from /assets/stylesheets/site.css:
.box {
color: #fe33ac;
border-color: #fdcdea;
}
.box div {
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
In watch mode, the .less files are automatically recompiled.
|
The Less version compilation relies on the original less compiler executed on top of the node.js runtime.
16.9. Stylesheets Aggregation and Minification
Wisdom integrates Clean-CSS to aggregate and minify stylesheets. By
default, it minifies all .css file into a -min.css file. However this behavior can be configured to aggregate the
files and minify the result.
In your pom.xml, in the wisdom-maven-plugin configuration section, add:
<stylesheets>
<aggregations>
<aggregation>
<minification>true</minification> <!-- optional, default to true -->
<output>my-css.css</output> <!-- optional -->
<files>
<file>style.css</file>
<file>socket/socket.css</file>
</files>
</aggregation>
</aggregations>
</stylesheets>
The stylesheets element let you configure the aggregation and minification. You can have several aggregation
element under the aggregations element. Each aggregation must list the set of files it aggregates. Only internal
assets (i.e. from src/main/resources/assets) can be aggregated. Each files is looked up from the
target/classes/assets directory (containing the compiles assets). The extension can be omitted (for example,
style instead of style.css). The aggregation can optionally defined the output file, also related to
target/classes/assets. By default it builds the name as follows: artifactid-min.css (or just artifactid.css if
minification is disabled).
Aggregation also supports a removeIncludedFiles attribute (disabled by default) to remove the individual aggregated
files after processing.
If you don’t want to list all the file one by one, you can use FileSets instead of <files>:
<fileSets>
<fileSet>
<includes>
<include>stylesheets/*/*.css</include>
<include>less/*.css</include>
</includes>
</fileSet>
</fileSets>
Be aware that wildcards can introduce ordering issue as the order of the selected files may depend on the file system you are using.
Using a file set let you exclude files too, as well as set the base directory. By default, the directory is the
location of internal assets (target/classes/assets):
<fileSets>
<fileSet>
<directory>target/external</directory>
<excludes>
<exclude>**/*.map</exclude>
</excludes>
<includes>
<include>*.css</include>
</includes>
</fileSet>
</fileSets>
if files is set, the fileSets are ignored.
|
16.10. JavaScript Aggregation and Minification
Wisdom integrates Google Closure to check, aggregate and minify JavaScript
files. For any JavaScript file (even the one generated by the CoffeeScript compiler), a minified version is generated.
The minified file name ends with -min.js. For example, my-script.js will be minified into my-script-min.js.
To use the minified version, add the -min suffix to your script tags in your templates or HTML files.
| Files already minified are not re-minified. |
You can configure the optimization level of the Closure Compiler and the pretty print option from your
pom.xml file:
<configuration>
<googleClosureCompilationLevel>ADVANCED_OPTIMIZATIONS</googleClosureCompilationLevel>
<googleClosurePrettyPrint>true</googleClosurePrettyPrint>
</configuration>
The value of the compilation level can be WHITESPACE_ONLY (default), SIMPLE_OPTIMIZATIONS or
ADVANCED_OPTIMIZATIONS.
The pretty print option lets you configure how the minified file is formatted . Enabling pretty print impacts the final file size but is much more readable.
Aggregation is configured inside the <javascript> element:
<javascript>
<aggregations>
<aggregation>
<files>
<file>coffee/math</file>
<file>js/log.js</file>
</files>
<output>math-min.js</output>
</aggregation>
</aggregations>
</javascript>
You can have several aggregation element under the aggregations element. Each aggregation must list the set of
files it aggregates. Only internal assets (i.e. from src/main/resources/assets) can be aggregated. Each files is
looked up from the target/classes/assets directory (containing the compiles assets). The extension can be omitted
(for example, coffee/math instead of coffee/math.js). The aggregation can optionally defined the output file, also
related to target/classes/assets. By default it builds the name as follows: artifactid-min.js.
Aggregation also supports a removeIncludedFiles attribute (disabled by default) to remove the individual aggregated
files after processing.
If you don’t want to list all the file one by one, you can use FileSets instead of <files>:
<fileSets>
<fileSet>
<includes>
<include>coffee/*.js</include>
<include>js/*.js</include>
</includes>
</fileSet>
</fileSets>
Be aware that wildcards can introduce ordering issue as the order of the selected files may depend on the file system you are using.
Using a file set let you exclude files too, as well as set the base directory. By default, the directory is the
location of internal assets (target/classes/assets):
<fileSets>
<fileSet>
<directory>target/external</directory>
<excludes>
<exclude>**/*.map</exclude>
</excludes>
<includes>
<include>*.js</include>
</includes>
</fileSet>
</filSsets>
if files is set, the fileSets are ignored.
|
You can also disable the Google Closure support with:
<configuration>
<skipGoogleClosure>true</skipGoogleClosure>
</configuration>
In watch mode, you can disable Google Closure support with: mvn wisdom:run -DskipGoogleClosure=true
16.11. Image optimizations
Images from your assets are automatically optimize to reduce their sizes. The optimization uses
ImageMin and process jpg, png, gif and svg.
You can skip the image optimization using <skipImageOptimization>true</skipImageOptimization> or in the command line
using -DskipImageOptimization=true.
You can configure the optimization using the <imageMinification> element in your pom.xml file:
<configuration>
<imageMinification>
<enabled>true</enabled> <!-- equivalent to skipImageOptimization -->
<version>2.0.0</version> <!-- the imagemin-cli version -->
<interlaced>true</interlaced> <!-- whether or not Gif should be interlaced -->
<progressive>true</progressive> <!-- whether or not JPEG should be progressive -->
<optimizationLevel>3</optimizationLevel> <!-- the PNG optimization level in [0,7] -->
</imageMinification>
</configuration>
16.12. WebJar packaging
The internal assets from your project can be packaged into a webjar. This WebJar will contained the process versions of your assets and has a structure conform to the webjar specification. WebJars are really useful to share assets.
WebJar packaging is disabled by default, but can be easily enabled using:
<configuration>
<packageWebJar>true</packageWebJar>
</configuration>
By default, all the internal resources of your project are packaged into a webjar. This webjar use the project’s
artifact id as name, and project’s version as version. The created artifact use the webjar classifier.
The webjar can be customized using the webjar element in the pom.xml file:
<webjar>
<!-- change the webjar name -->
<name>sample-resources</name>
<!-- change the webjar version -->
<version>1.0</version>
<!-- change the webjar classifier -->
<classifier>resource</classifier>
<!-- the selected set of file -->
<fileset>
<directory>${project.build.directory}/classes/assets</directory>
<excludes>
<exclude>**/js/*</exclude>
</excludes>
</fileset>
</webjar>
| The fileset directory must be a full path. By default the asset ouput directory is used. |
16.13. Customizing asset location
You extend the location where Wisdom is looking for assets.
First create the src/main/instances directory and add the following snippet to your pom.xml file:
<execution>
<id>copy-instances</id>
<!-- here the phase you need -->
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>/Users/clement/Projects/wisdom/wisdom-framework/documentation/documentation/target/wisdom/application</outputDirectory>
<resources>
<resource>
<directory>src/main/instances</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
Then create a org.wisdom.resources.AssetController-A_NAME.cfg in the instances directory you just created. Change
A_NAME by an identifier that you can understand. In this file you can set:
# the file system path relative to the Wisdom directory in which external
# assets are located.
path=public
# a boolean indicating whether or not it should also look into bundles
manageAssetsFromBundles=true
# the URL root to load these assets
url=/public
# is `manageAssetsFromBundles` is set to true the path inside bundles
# where assets are located
pathInBundles=/interns/
Let’s see two examples. In this first example, it extends assets locations with an in bundle lookup looking for
assets in the /interns directory (inside the jar file). These assets are served from the /internal url. As the
path property is not set, it does not support assets located on the file system.
manageAssetsFromBundles=true
url=/internal
pathInBundles=/interns/
This second example extends the asset lookup with an external location (public) served from the /public url:
path=public
manageAssetsFromBundles=false
url=/public
the path property must always starts with a /. The pathInBundles value must start and end with a /.
|
17. WebJars
WebJars are client-side web libraries (e.g. jQuery & Bootstrap) packaged into JAR (Java Archive) files. It allows to :
-
Explicitly and easily manage the client-side dependencies in JVM-based web applications
-
Know which client-side dependencies you are using
Wisdom natively supports WebJars: WebJars from your Maven dependencies are detected and copied to the Wisdom Runtime. In addition, Wisdom makes requiring the embedded assets very easy.
Assets included in webjars are listed on the /assets page (http://localhost:9000/assets).
17.1. Maven dependencies
Web Jars are plain Jars embedding web assets (such as javascript and css files). The list of WebJars is listed on http://www.webjars.org/.
Search for the library you need and copy the dependency to your project pom file. For example, to use jquery, add:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.0-2</version>
</dependency>
When building your Wisdom project, it unpacks your dependencies to wisdom/assets/libs. So you can quickly check what files are available, the different libraries and their versions.
17.2. Importing the embedded assets
Assets embedded in Web Jars are available under "/libs". Let’s see some examples:
<link rel="stylesheet" href="/libs/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/libs/css/bootstrap-theme.min.css"/>
<script src="/libs/jquery.js"></script>
<script src="/libs/js/bootstrap.min.js"></script>
The path to the file depends on how the library is structured.
You can also specify the library name to be sure you get the file from the right library (useful when file names conflict):
<link rel="stylesheet" href="/libs/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/libs/bootstrap/css/bootstrap-theme.min.css"/>
<script src="/libs/jquery/jquery.js"></script>
<script src="/libs/bootstrap/js/bootstrap.min.js"></script>
Finally, you can also use the library name and version, to be even more specific:
<script src="/libs/jquery/2.1.0-2/jquery.js"></script>
18. Using the Cache
Caching data is a typical optimization in modern applications, and so Wisdom provides a global cache. An important point about the cache is that it behaves just like a cache should: the data you just stored may just go missing.
For any data stored in the cache, a regeneration strategy needs to be put into place in case the data goes missing. This philosophy is one of the fundamentals behind Wisdom, and is different from Java EE, where the session is expected to retain values throughout its lifetime.
The default cache service uses EHCache and it’s enabled by default. You can also provide your own
implementation via a service.
18.1. Accessing the Cache Service
The cache is available as a service, so accessible using:
// Inject the cache service.
@Requires
private Cache cache;
@Route(method = HttpMethod.GET, uri = "/")
public Result action() {
// Retrieve an object from the cache
Object cached = cache.get("my.cache.key");
if (cached == null) {
cached = new Object();
// Store an object in the cache for a specific duration (in second)
cache.set("my.cache.key", cached, 60);
}
return ok();
}
| The API is intentionally minimal to allow various implementations to be implemented. |
Using this simple API you can store data in the cache:
cache.set("item.key", news, 60 * 15);
You can retrieve the data later:
News cached = cache.get("item.key");
To remove an item from the cache use the remove method:
cache.remove("item.key");
18.2. Caching HTTP responses
You can easily create an augmented cached action using standard action interception. Wisdom provides a default built-in interceptor for the standard case:
@Route(method= HttpMethod.GET, uri = "/cached")
@Cached(key="cached", duration = 60)
public Result cached() {
String s = DateFormat.getDateTimeInstance().format(new Date());
return ok(s);
}
@Route(method= HttpMethod.GET, uri = "/cachedForAYear")
@Cached // Use the uri as cache key, and the duration set to 365 days.
public Result cachedWithoutKey() {
String s = DateFormat.getDateTimeInstance().format(new Date());
return ok(s);
}
| if the duration is not specified, the result is cached for one year. |
| if the key is not specified it uses the request’s uri (path and query) |
19. Scheduling tasks
Wisdom lets you define periodic jobs. To define a job, you need to:
-
implement the
scheduledinterface, and expose it as a service -
add the
@Everyon a method that will be executed periodically
About the first step, if you are in a controller, you just have to implement the Scheduled
interface:
@Controller
public class ScheduledController extends DefaultController implements Scheduled {
@Every("30m")
public void task() {
System.out.println("Task fired");
}
}
If you are not in a controller, you need to use the @Provides annotation to expose the service
(or just use the @Service annotation).
@Component
@Provides
@Instantiate
public class Printer implements Scheduled {
@Every("1h")
public void print() {
System.out.println(new SimpleDateFormat().format(new Date()) + " - Hello ...");
}
@Every(period = 1, unit = TimeUnit.DAYS)
public void cleanup() {
System.out.println("Cleanup !");
}
}
For each method you want to run periodically, add the @Every annotation specifying the period. The period format is
very simple:
"1s" : every second "1m" : every minute "1h" : every hour "1d" : every day "2h30m" : every 2 hours and 30 minutes
The task is not run immediately but only after the specified period.
As you can see above, you can also use the period and unit parameters to configure the period.
|
20. Authentication
Wisdom proposes a very simple security layer letting you secure your endpoints. This model is inspired from the Play 2 Framework security support.
21. Overview
Before going further let’s explain how authentication works in Wisdom. First of all don’t forget that thanks to Wisdom extensibility you can customize this process, or implement something from your own.
Authentication support is mainly based on 2 entities:
-
a filter intercepting actions annotated with the
@Authenticatedannotation. -
an
Authenticatorservice responsible for identifying the user executing the request
Notice that this mechanism does not includes login / logoff support that are application specific. For instance these features would be very different if you use OAuth, a remove authentication server (LDAP, Crowd), or if you add authentication to a REST API.
The following sequence diagram depicts how the entities listed above interact:
First a user emit a request targeting a route that requires authentication. The request is intercepted and the authenticator that need to be used among the set of available authenticator. If the authenticator cannot be found, the request is rejected. Once the authenticator is selected, the filter ask it if the incoming request is authenticated or not. The authenticator can implement various mechanism for this, such as checking the session cookie, request parameters… It returns the username (then go to the green sequence) or null, this latter case meaning that the request is not authenticated (red sequence).
The authenticator is also responsible for given the result to return if the user is not authenticated. For instance it can build a redirect response to the login page.
So, as an application developer, you just need to provide an Authenticator, the rest of the process being managed
by Wisdom.
Let’s have a look to the overall process if we introduce a login / logoff controller:
In this example, the login action adds the user id into the session. This let the authenticator retrieve the user id
from the session and deduce the user name from this id. So once logged in, the user can access to /private, as the
authenticator is granting the access. The logoff action remove the id from the session. So, once logged off, the user
cannot access to /private. The request is redirected to the login page.
To be less abstract, you can check how the Wisdom Monitor is secured:
21.1. Implementing a authentication service
The first step is to implement a service checking if the current session is 'authenticated'. Such a class must implement the Authenticator interface.
The following snippet is a basic implementation checking if the property username is contained
in the session ( and
if it’s 'admin'):
@Component
@Provides
@Instantiate
public class MyAuthenticator implements Authenticator {
@Override
public String getName() {
return "my-authenticator";
}
@Override
public String getUserName(Context context) {
if (context.session().get("username") != null) {
return context.session().get("username");
}
String username = context.parameter("username");
if (username != null && username.equals("admin")) {
context.session().put("username", "admin");
return "admin";
} else {
return null;
}
}
@Override
public Result onUnauthorized(Context context) {
return Results.unauthorized("Your are not authenticated !");
}
}
The implementation provides three methods:
-
getUserNamedetermines the username of the currently authenticated user -
onUnauthorizeddefines the actions to do when the user is not authenticated -
getName()returns the identifier of this authenticator service. This identifier is used for selection (see below).
21.2. Annotating your action method or controller
Once you have your authenticator service, you can annotate your action method or controller (this is equivalent to
annotating all methods) with the @Authenticated annotation:
@Controller
public class SecretKeeperController extends DefaultController {
/**
* Secured action.
*/
@Route(method= HttpMethod.GET, uri="/security/secret")
@Authenticated("Monitor-Authenticator")
public Result secret() {
return ok("This is a secret... " + context().request().username());
}
/**
* Not secured action.
*/
@Route(method= HttpMethod.GET, uri="/security")
public Result notSecret() {
String name = context().session().get("username");
if (name == null) {
name = "anonymous";
}
return ok("Hello " + name);
}
}
Once annotated, access to your method invokes the authenticator service to check if the current context / session
/ request is 'authenticated'. If not the onUnauthorized method is called and its result is used
as the response.
21.3. Selecting the right authentication service
You can have several authenticator services at the same time. To select the authenticator to use:
@Authenticated("id")
The 'id' is the String returned by the getName() method. If the specified authenticator is not available, an
unauthorized response is returned.
22. Internationalize your application
22.1. Externalizing messages
Wisdom lets you externalize your messages by creating properties files (containing your
messages as explained on this page) in
the src/main/resources/i18n folder. The name of the file lets you configure the locale/languages of the messages
contained in the file. Let’s see some examples:
-
src/main/resources/i18n/app.propertiesprovides messages in the default locale -
src/main/resources/i18n/app_fr.propertiesprovides messages in French -
src/main/resources/i18n/app_en_US.propertiesprovides messages in American English
The default locale (empty locale) is used when there is no locale matching the request. Generally
we use this locale for English messages (but you can configure it using the
application.locale property in the application.conf file).
| Unlike what’s said on the Javadoc page, the files containing the messages are read using UTF-8. |
You can retrieve messages using the org.wisdom.api.i18n.InternationalizationService service:
@Requires
private InternationalizationService i18n;
@Route(method= HttpMethod.GET, uri = "internationalization/messages")
public Result retrieveInternationalizedMessages() {
return ok(
"english: " + i18n.get(Locale.ENGLISH, "welcome") + "\n" +
"french: " + i18n.get(Locale.FRENCH, "welcome") + "\n" +
"default: " + i18n.get(InternationalizationService.DEFAULT_LOCALE, "welcome") + "\n"
).as(MimeTypes.TEXT);
}
Notice that you need to specify the language explicitly.
| The messages do not have to be packaged within the application’s bundle, but can be provided in separate bundles, even deployed dynamically. |
22.2. Retrieving messages using the request languages
HTTP Requests can specify the expected languages using the Accept-Language header. Wisdom parses this header and
lets you provide the messages in the most suitable language.
To retrieve the messages using the request accepted languages, just use:
@Route(method= HttpMethod.GET, uri = "internationalization/request")
public Result retrieveInternationalizedMessagesFromRequest() {
return ok(
i18n.get(request().languages(), "welcome")
).as(MimeTypes.TEXT);
}
Wisdom determines what’s the best language to use for the request. For example,
for a request specifying an Accept-Language with en-US,en;q=0.8,de;q=0.6,fr;q=0.4,
Wisdom tries to provide the message in American English, then English, then German,
then French. If none of them are provided, the default locale is used.
22.3. Use in templates
Using internationalized messages in a template is pretty simple. It relies on the Thymeleaf syntax:
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
A utext instructs Wisdom to use an internationalized message specified using #{key}. For example,
the following template uses two internationalized messages:
<!DOCTYPE html>
<html>
<head>
<title th:utext="#{app.title}">Title</title>
</head>
<body>
<h1 th:utext="#{app.welcome(${username})}">Welcome Message</h1>
</body>
</html>
Notice that the second message is parameterized.
22.4. Formatting messages
Messages can be formatted using the java.text.MessageFormat format. For example, if you have defined a message like
this:
files.summary=The directory {1} contains {0} file(s).
You can then specify parameters as:
i18n.get(request().languages(), "files.summary", dir.list().length, dir.getName())
22.5. Retrieve messages in JavaScript
The internationalization service can be requested using simple HTTP Requests to retrieve the messages. This lets you retrieve the messages from a JavaScript client or anything able to emit a HTTP GET request.
GET http://localhost:9000/i18n/welcome ===> The message with the welcome key (returned as text) GET http://localhost:9000/i18n ===> All messages (returned as JSON) GET http://localhost:9000/i18n/welcome?locale=en ===> The message with the welcome key in the specified language GET http://localhost:9000/i18n?locales=en ===> All messages in the specified locales (returned as JSON) GET http://localhost:9000/bundles/Messages_fr.properties ===> Gets a Resource Bundle (Java Properties) containing all messages in the specified locale (here fr)
Except if specified otherwise, Wisdom computes the language to use according to the request (so according to the
ACCEPT-LANGUAGE header). When retrieving all messages, the JSON structure is composed as follows:
{
"files.summary":"The directory {1} contains {0} file(s)",
"welcome":"Welcome"
}
22.6. Using the JQuery i18n plugin
To use the JQuery i18n Properties Plugin, just use
the last URL form: http://localhost:9000/bundles/Messages_fr.properties:
jQuery.i18n.properties({ name:'Messages', path:'/i18n/bundles/', mode:'both', language:'fr', callback: function() { // We specified mode: 'both' so translated values will be // available as JS vars/functions and as a map
// Accessing a simple value through the map
jQuery.i18n.prop('msg_hello');
// Accessing a value with placeholders through the map
jQuery.i18n.prop('msg_complex', 'John');
// Accessing a simple value through a JS variable
alert(msg_hello +' '+ msg_world);
// Accessing a value with placeholders through a JS function
alert(msg_complex('John'));
}
});
---
22.7. Using i18next
Wisdom also provide the support of i18next.
var option = { resGetPath: '/i18n/bundles/messages.json?locales=lng', dynamicLoad: true }; i18n.init(option, function(t) { // translate nav $(".translation").i18n();
var appName = t("app.welcome");
console.log(appName);
}); ---
22.8. i18n and cache
The internationalization support computes ETAG based on the latest modifications of the message for a specific
locale. That means that requests to retrieve resource bundles (JQuery i18n plugin), messages as JSON format (i18next)
and the list of all messages (/i18n) support client side caching.
== Application configuration
Wisdom configuration system uses the HOCON syntax. This
section presents a brief introduction to HOCON and how you application can read its configuration from the
application.conf file.
the application.conf file contains all the configuration data. In your project it’s located in the
src/main/configuration directory. At runtime, this file is copied to the conf directory.
22.9. HOCON
HOCON stands for "Human-Optimized Config Object Notation". It’s a easy to read, structured syntax that avoid some of the ambiguity of the Java Properties files.
Instead of explaining in details what HOCON is, let’s look at some examples:
monitor {
auth {
enabled = true
username = admin
password = admin
}
http {
enabled = true
}
jmx {
enabled = true
}
# the update period in second, 10 seconds by default
# period = 10
}
This first example introduces the "structure" syntax using "{}". As you can see, HOCON is very close to JSON. To retrieve a property, you can use`path` such as:
-
monitor.auth.enabled⇒true -
monitor.auth.username⇒admin
HOCON also support the "Flat" format:
documentation.standalone = false
However, be aware that it’s not compatible with the Java Properties format, even if it’s very close. Differences are mostly about quoted / unquoted values:
foo {
value = "this is a quoted String"
}
HOCON supports substitution:
foo = bar
baz = ${foo} # == bar
It also supports includes
# Include in the root
include "my-include.conf"
sub {
# Include there
include "my-sub-conf.conf"
}
That’s mostly all you need to know about HOCON.
22.10. Retrieving configuration from your application
Your application can retrieve the configuration using the org.wisdom.api.configuration.ApplicationConfiguration
service. Let’s look at a sample.
Let’s imagine we have the following configuration in the application.conf file:
my-application-configuration {
my-key = "my value"
}
To retrieve the "my value" data, just require the ApplicationConfiguration file and retrieve the data:
@Requires
ApplicationConfiguration configuration;
public void readConfiguration() {
System.out.println(configuration.get("my-application-configuration.my-key"));
// or
Configuration conf =
configuration.getConfiguration("my-application-configuration");
System.out.println(conf.get("my value"));
}
There are other useful methods you can use:
String v = configuration.get("key");
v = configuration.getWithDefault("key", "default");
v = configuration.getOrDie("key");
boolean b = configuration.getBoolean("key");
b = configuration.getBooleanWithDefault("key", true);
b = configuration.getBooleanOrDie("key");
int i = configuration.getInteger("key");
i = configuration.getIntegerWithDefault("key", 5);
i = configuration.getIntegerOrDie("key");
long l = configuration.getLong("key");
l = configuration.getLongWithDefault("key", 5l);
l = configuration.getLongOrDie("key");
// The application base directory
File baseDir = configuration.getBaseDir();
// Convert the value to a MyData object, using 'converters'
MyData data = configuration.get("key", MyData.class);
data = configuration.get("key", MyData.class, "data1,data2,data3");
data = configuration.get("key", MyData.class, DEFAULT_DATA);
// Durations
// Durations are converted to the given unit, for instance for:
// key = 1 minute
// key = 2 hours
long duration = configuration.getDuration("key", TimeUnit.SECONDS);
duration = configuration.getDuration("key", TimeUnit.SECONDS, 2);
// Sizes in bytes to avoid the confusion between powers of 1000 and powers of 1024
// For instance for
// key = 1 kB => 1000 bytes
// key = 1 K => 1024 bytes
long size = configuration.getBytes("key");
size = configuration.getBytes("key", 2048);
22.10.1. From Properties to HOCON
Historically, Wisdom was using Apache Commons Configuration to handle its configuration. However, we realized it has ambiguities, and we needed a format that handle modularisation. To ease the migration from the former format to hocon, we provide a converter that you run only once. It transforms entry by entry the properties file to the HOCON format:
mvn org.wisdom-framework:wisdom-maven-plugin:0.10.0:properties2hocon -Dbackup=false
It reads the src/main/configuration/application.conf file and transforms it. You can enabled or disable the backup
support. This automatic transformation keeps the format close to the initial format to ease the learning. In
addition, it may not be perfect, so a review of the output is generally required.
23. Exception Handling
When an action method throws an exception, one of the three following possibilities happens:
-
a default Internal Server Error is returned to the client
-
the exception is an
org.wisdom.api.exceptions.HttpExceptionor extendsorg.wisdom.api.exceptions.HttpExceptionand so a specific result is sent to the client -
the exception can be handled by a
org.wisdom.api.exceptions.ExceptionMapperservice
23.1. Default Error Handling
By default, when an exception is thrown by a controller, an internal server error is returned to the client (status 500). If the request accepts HTML and if the error template is available, the exception is presented as a web page.
23.2. HTTP Exception
org.wisdom.api.exceptions.HttpException lets us customize the error returned when such
an exception is thrown. When a
controller throws an exception of type org.wisdom.api.exceptions.HttpException (or a subclass of it), a specific
result is built and returned to the client.
@Route(method = HttpMethod.GET, uri = "/http_error")
public Result http() {
throw new HttpException(418, "bad");
}
23.3. Exception Mapper
In other cases it may not be appropriate to throw instances of HttpException, or classes that extend
HttpException, and instead it may be preferable to map an existing exception to a result. For
such cases it is possible to use a custom exception mapping provider. The provider must implement
the ExceptionMapper<E extends Exception> interface and exposes it as a service.
@Service
public class NoSuchElementExceptionMapper implements ExceptionMapper<NoSuchElementException> {
/**
* Gets the class of the exception instances that are handled by this mapper.
*
* @return the class
*/
@Override
public Class<NoSuchElementException> getExceptionClass() {
return NoSuchElementException.class;
}
/**
* Maps the instance of exception to a {@link org.wisdom.api.http.Result}.
*
* @param exception the exception
* @return the HTTP result.
*/
@Override
public Result toResult(NoSuchElementException exception) {
return new Result().status(404).render("nobody there");
}
}
The above class is annotated with @Service, so it will be exposed as an OSGi service. When an application throws an
NoSuchElementException the toResult method of the NoSuchElementExceptionMapper instance will be invoked.
24. Testing applications
Wisdom provides everything you need to test your application. More specifically, it supports:
-
Unit tests - test your classes and action methods
-
In-container integration tests - test your controllers and services on a running server
-
Blackbox integration tests - emit HTTP request and check the results
-
UI tests - check the web page exposed by your application
-
JavaScript Testing - check you JavaScript code
To ease the development ot tests, Wisdom provides AssertJ, OSGi Testing Helpers for in-container tests, a HTTP fluent API for blackbox tests, and FluentLenium for UI tests.
JavaScript code relies on Karma and can be executed during the (unit) test phase or integration-test phase.
By default, tests are looking for a free port. So don’t worry about configuring the port used by your
tests. You can configure the used port by setting it explicitly with the http.port system
property.
|
24.1. Unit Tests
The unit test support lets you write Junit tests to check the behavior of your classes. These
tests cases are written
in Java classes in the src/test/java folder. Notice that test case classes must start or end with Test (this is
a Surefire convention).
Unit tests should extend the WisdomUnitTest class to access utility methods.
import snippets.controllers.Name;
import snippets.controllers.Simple;
import org.junit.Test;
import org.wisdom.api.http.Result;
import org.wisdom.test.parents.Action;
import org.wisdom.test.parents.Invocation;
import org.wisdom.test.parents.WisdomUnitTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.wisdom.test.parents.Action.action;
public class UnitTest extends WisdomUnitTest {
@Test
public void test() {
assertThat(1 + 1).isEqualTo(2);
}
@Test
public void testActionWithoutParameter() {
// This is an instance of my controller
final Simple simple = new Simple();
// Call the action method as follows:
Action.ActionResult result = action(new Invocation() {
@Override
public Result invoke() throws Throwable {
return simple.index();
}
}).parameter("name", "clement").header("Accept", "text/html").invoke();
assertThat(status(result)).isEqualTo(OK);
assertThat(toString(result)).isEqualTo("Follow the path to Wisdom");
}
@Test
public void testActionWithParameter() {
// This is an instance of my controller
final Name name = new Name();
// Call the action method as follows:
Action.ActionResult result = action(new Invocation() {
@Override
public Result invoke() throws Throwable {
return name.index("clement");
}
}).invoke();
assertThat(status(result)).isEqualTo(OK);
assertThat(toString(result)).isEqualTo("Hi " + "clement" + ", follow us on the Wisdom path");
}
}
The first test is a really simple test just checking that 1+1=2. Notice the AssertJ syntax.
The second test checks the behavior of one of your action methods. As your action method can
access the HTTP method,
the call is wrapped into an Invocation call:
Action.ActionResult result = action(new Invocation() {
@Override
public Result invoke() throws Throwable {
return simple.index();
}
}).invoke();
Such a pattern lets you call your action directly, and retrieve the HTTP result. You can also
configure the HTTP
headers, parameters, form attributes, cookies, session…. The chain must end with invoke()
Action.ActionResult result = action(new Invocation() {
@Override
public Result invoke() throws Throwable {
return simple.index();
}
}).parameter("name", "clement").header("Accept", "text/html").invoke();
The ActionResult contains the HTTP Context (that may have been modified by your action method, and the HTTP result.
24.2. In-Container Tests
In-container tests let you check the behavior of your controller and services running in the server. The server is
automatically launched. These tests cases are written in Java classes in the src/test/java
folder. Notice that test
case classes must start or end with "IT" (this is a Surefire/Failsafe convention).
In-container tests must extend WisdomTest, and follow the same rules as unit tests. However, @Inject annotated
fields are injected.
import controllers.Documentation;
import org.junit.Test;
import org.wisdom.api.http.Result;
import org.wisdom.test.parents.Action;
import org.wisdom.test.parents.Invocation;
import org.wisdom.test.parents.WisdomTest;
import javax.inject.Inject;
import static org.assertj.core.api.Assertions.assertThat;
import static org.wisdom.test.parents.Action.action;
public class InContainerIT extends WisdomTest {
// Inject the controllers, services or template you are testing
@Inject
Documentation documentation;
@Test
public void testDocumentation() {
// Call the action method as follows:
Action.ActionResult result = action(new Invocation() {
@Override
public Result invoke() throws Throwable {
return documentation.doc();
}
}).header("Accept", "text/html").invoke();
// It returns a redirection to the index.html page.
assertThat(status(result)).isEqualTo(SEE_OTHER);
assertThat(result.getResult().getHeaders().get(LOCATION)).contains("/index.html");
}
@Test
public void test() {
// Not recommended, but this is also executed
assertThat(1 + 1).isEqualTo(2);
}
}
The @Inject annotation lets you access the real instance of your controller, service or template:
// Controller injection: @Inject HelloController controller; // Service injection: @Inject private Validator validator; // The OSGi bundle context @Inject private BundleContext context;
Once injected your test can invoke them either directly or in an invocation block to configure the HTTP context.
| the same server instance is used for all your tests. |
24.3. BlackBox Tests
Black box tests are emitting HTTP requests and retrieving the result. Such tests are not executed in the server. Your application is deployed within the server (it reuses the launched one by in-container tests if it’s already running).
As for in-container tests, a black box test class must start or end with "IT". These classes must
extend
org.wisdom.test.parents.WisdomBlackBoxTest.
import org.jsoup.nodes.Document;
import org.junit.Test;
import org.wisdom.test.http.HttpResponse;
import org.wisdom.test.parents.WisdomBlackBoxTest;
import static org.assertj.core.api.Assertions.assertThat;
public class BlackBoxIT extends WisdomBlackBoxTest {
@Test
public void testDoc() throws Exception {
HttpResponse<Document> response = get("/documentation").asHtml();
assertThat(response.code()).isEqualTo(OK);
assertThat(response.body()
// JSOUP let us find HTML element very easily
// and retrieve its content:
.getElementById("_the_wisdom_framework").text())
.isEqualTo("1. The Wisdom Framework");
}
}
The test API lets you emit any type of HTTP requests and configure headers, payload, cookies… In addition, the response can be smoothly wrapped to Json Node, HTML Document (JSoup), String, or input stream.
If your blackbox test requires controllers or services from your test sources (i.e. located in the src/test/java
directory), you need to use @BeforeClass and @AfterClass as follows:
@BeforeClass
public static void init() throws BundleException {
installTestBundle();
}
@AfterClass
public static void cleanup() throws BundleException {
removeTestBundle();
}
Notice that the methods have to be static. The invoked bundles are located in org.wisdom.test.parents
.WisdomBlackBoxTest.
24.4. FluentLenium Tests
UI Tests are loading pages using a browser and check the elements present on the page. Wisdom uses FluentLenium to ease the implementation of UI tests.
As for in-container tests, UI test classes must start or end with "IT". These classes must extend
org.wisdom.test.parents.WisdomFluentLeniumTest.
import org.junit.Test;
import org.wisdom.test.parents.WisdomFluentLeniumTest;
import static org.fluentlenium.assertj.FluentLeniumAssertions.assertThat;
public class UIIT extends WisdomFluentLeniumTest {
@Test
public void doc() throws Exception {
goTo("/documentation");
assertThat(find("#_the_wisdom_framework")).hasText("1. The Wisdom Framework");
}
}
You can select the browser to use by setting the -Dfluentlenium.browser property in the command line. Accepted
values are firefox, chrome, ie (Internet Explorer) and safari. If not set it uses the HTML Unit Browser
(Fluentlenium’s default) relying on Firefox.
Notice that depending on the browser you choose you may have to install additional software or specify addition values. Check the following links:
24.5. Stopping the server instance
When building a Maven multi-module project, the same instance of the server is used for all your tests. This can be annoying. Fortunately, Wisdom provides a test listener to stop the instance once the module build is completed:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.18</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<properties>
<property>
<name>listener</name>
<value>org.wisdom.test.WisdomRunListener</value>
</property>
</properties>
</configuration>
</execution>
</executions>
</plugin>
The previous configuration is automatically generated when creating a new project.
24.6. JavaScript tests
To test your JavaScript code, Wisdom integrates Karma, a test runner executing the code in a real or emulated browser
. Karma test can be executed either during the test phase, or during the integration-test phase. In both case you
need a Karma configuration file located in the src/test/javascript directory, in which your JavaScript tests will
also be.
Karma relies on a set of plugins that you must configure from your pom.xml file:
<karmaPlugins>
<plugin>karma-jasmine,0.1.5</plugin>
<plugin>karma-phantomjs-launcher,0.1.1</plugin>
<plugin>karma-junit-reporter</plugin>
</karmaPlugins>
This list indicates the name of the plugin and its version (optional).
24.6.1. Unit Tests
Unit tests are configured in the src/test/javascript/karma.conf.js file. Here is an example:
module.exports = function(config) {
config.set({
basePath: "${basedir}",
frameworks: ['jasmine'],
files: [
'${project.build.outputDirectory}/assets/square.js',
'src/test/javascript/*Spec.js'
],
exclude: ['src/test/javascript/karma.conf*.js'],
port: 9876,
logLevel: config.LOG_INFO,
browsers: ['PhantomJS'],
singleRun: true,
plugins: [
'karma-jasmine',
'karma-phantomjs-launcher',
'karma-junit-reporter'
],
reporters:['progress', 'junit'],
junitReporter: {
outputFile: 'target/surefire-reports/karma-test-results.xml',
suite: ''
}
});
};
You can find more details about the configuration file here.
The configuration file is filtered, meaning you can use Maven properties such as /Users/clement/Projects/wisdom/wisdom-framework/documentation/documentation.
|
The set of plugins listed in the configuration must have been listed in the karmaPlugins parameter in the
pom.xml file.
|
singleRun must be set to true.
|
using the junitReporter allow retrieving the Karma tests in your continuous integration server such as Jenkins.
|
Once you have this configuration file, you can write JavaScript unit test. Here is an example using Jasmine:
// squareSpec.js
describe('The square function', function(){
it('should square a number', function(){
expect(square(3)).toBe(9);
});
});
describe('true is true', function() {
it('should be true', function() {
});
it('should be true 2', function() {
expect(true).toBe(true);
});
});
24.6.2. Integration Tests
Unlike unit tests, integrationt ests need to be enabled in your Maven pom.xml file:
<executions>
<execution>
<id>javascript-it</id>
<goals>
<goal>test-javascript-it</goal>
</goals>
<phase>integration-test</phase>
</execution>
</executions>
The Karma integration tests are configured in the src/test/javascript/karma.conf-it.js file. This file is also
filtered, and in addition to the Maven properties can rely on:
-
hostname: the local server -
httpPort: the HTTP port -
httpsPorts: the HTTPS port
So, the proxies configuration can be like:
module.exports = function(config) {
config.set({
basePath: "${basedir}",
frameworks: ['jasmine'],
files: [
'${project.build.outputDirectory}/assets/square.js',
'src/test/javascript/*Spec.js'
],
exclude: ['src/test/javascript/karma.conf*.js'],
port: 9876,
logLevel: config.LOG_INFO,
browsers: ['PhantomJS'],
singleRun: true,
proxies: {
'/': 'http://${hostname}:${httpPort}'
},
plugins: [
'karma-jasmine',
'karma-phantomjs-launcher',
'karma-junit-reporter'
],
reporters:['progress', 'junit'],
junitReporter: {
outputFile: 'target/surefire-reports/karma-IT-results.xml',
suite: ''
}
});
};
To run integration tests, Wisdom starts your application and then launch Karma tests. So, you can load pages and executes Ajax calls.
25. Deployment
This section describes how to deploy a Wisdom application on a server. Wisdom is particularly easy to deploy as it’s almost self-contained. The only requirements are:
-
a Java 7` virtual machine
-
a writable disk space
25.1. Packaging a distribution
To build a distribution of your application just use: mvn clean package. A distribution containing the Wisdom
runtime, the application dependencies and external assets are created in the target folder as a zip file. Wisdom
application deployment is based on this distribution file.
25.2. Launching & Stopping
First, unzip the distribution file anywhere. Be aware that the zip file does not include a base directory. To launch
a Wisdom application in background, just use the chameleon.sh script provided at the root of the unzipped location:
./chameleon.sh start
Stopping is quite similar:
./chameleon.sh stop
Why chameleon.sh? Actually, Wisdom is built on top of the OW2 Chameleon project,
packaging a set of OSGi bundles and providing low-level features, such as logging, dynamic deployment…
|
To avoid the output on the console, use:
./chameleon.sh start > /dev/null
The server’s process id is written to the RUNNING_PID file. To kill a running Wisdom server,
it is enough to send a SIGTERM to the process to properly shutdown the application.
|
the HTTP port can be set by configuring the JVM_ARGS system variable before launching the application:
export JVM_ARGS="-Dhttp.port=9005" && ./chameleon.sh start
|
25.4. Production deployment
25.4.1. Specifying the HTTP server address and port
You can provide both the HTTP port and address from the JVM_ARGS system variable. The default is to listen on port
9000 at the 0.0.0.0 address (all addresses).
export JVM_ARGS="-Dhttp.port=8080 -Dhttp.address=127.0.0.1"
./chameleon.sh start
| Note that these configurations are only provided for the default embedded Vertx server. |
You can configure the HTTPS port using the https.port property.
You can disable HTTP or HTTPS using the -1 value. The 0 value enables a free port lookup.
| The free port lookup does not rely on the 'port 0 hack', but actually attempts to bind on random ports. |
25.5. Specifying additional JVM arguments
You can specify any JVM arguments to the JVM_ARGS system variable. Otherwise the default JVM settings will be used:
export JVM_ARGS="-Xms128M -Xmx512m -server"
./chameleon.sh start
25.6. Specifying alternative configuration files
The default is to load the conf/application.conf file. You can specify an alternative configuration file if needed:
export JVM_ARGS="-Dapplication.configuration=conf/application-prod.conf"
./chameleon.sh start
| The application configuration file uses the HOCON syntax. Unlike properties file, it supports includes. |
25.7. Wisdom application as a system service
This section explains how to create a system service starting and stopping your application when the machine boots and shutsdown.
| This section explains the configuration for CentOS, however the same idea can be applied for other systems such as Ubuntu |
The service script used to manage a Wisdom application is very basic and relies on the chameleon.sh script. In
addition, most of the configuration is similar to most Wisdom applications. This script can be downloaded from
here.
In this script, you must configure some variables:
WISDOM_HOME=/home/wisdom/wisdom => The path of the wisdom application
APPLICATION_MODE="PROD" => The execution mode
export JVM_ARGS="-Dapplication.mode=${APPLICATION_MODE}" => Add the other variables at the end of this line
JAVA_HOME=/usr/java/latest/ => The Java location
USER=wisdom => The user executing the application
| The set user must be able to write in the application directory. |
Once edited, copy the script file to /etc/init.d. Then add the execution flag with chmod +x /etc/init
.d/filename. Add the script to the boot and shutdown sequence using: chkconfig --add /etc/init.d/filename. Finally,
enable the configuration with chkconfig filename on. So, if your file is named wisdom,
the sequence of instructions is the following:
cp wisdom.sh /etc/init.d/wisdom chmod +x /etc/init.d/wisdom chkconfig --add /etc/init.d/wisdom chkconfig wisdom on
Once done, start the service using /etc/init.d/wisdom start. The service can be stopped using /etc/init.d/wisdom stop.
26. Setting up a front end HTTP server
You can easily deploy your application as a stand-alone server by setting the application HTTP port to 80:
export JVM_ARGS="-Dhttp.port=80"
./chameleon.sh start
Note that you probably need root permissions to bind a process to this port.
However, if you plan to host several applications in the same server or load balance several instances of your application for scalability or fault tolerance, you can use a front end HTTP server. Note that using a front end HTTP server will rarely give you better performance than using the Wisdom server directly. However, HTTP servers are very good at handling HTTPS, conditional GET requests and static assets, and many services assume a front end HTTP server is part of your architecture.
26.1. Set up with lighttpd
This example shows you how to configure lighttpd as a front end web server. Note that you can do the same with Apache, but if you only need virtual hosting or load balancing, lighttpd is a very good choice and much easier to configure!
The /etc/lighttpd/lighttpd.conf file should define things like this:
server.modules = (
"mod_access",
"mod_proxy",
"mod_accesslog"
)
…
$HTTP["host"] =~ "www.myapp.com" {
proxy.balance = "round-robin" proxy.server = ( "/" =>
( ( "host" => "127.0.0.1", "port" => 9000 ) ) )
}
$HTTP["host"] =~ "www.loadbalancedapp.com" {
proxy.balance = "round-robin" proxy.server = ( "/" => (
( "host" => "127.0.0.1", "port" => 9001 ),
( "host" => "127.0.0.1", "port" => 9002 ) )
)
}
26.2. Set up with nginx
This example shows you how to configure nginx as a front end web server. Note that you can do the same with Apache, but if you only need virtual hosting or load balancing, nginx is a very good choice and much easier to configure!
The /etc/nginx/nginx.conf file should define things like this:
http {
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
# proxy_http_version appeared in nginx 1.1.4
proxy_http_version 1.1;
upstream my-backend {
server 127.0.0.1:9000;
}
server {
server_name www.mysite.com mysite.com;
rewrite ^(.*) https://www.mysite.com$1 permanent;
}
server {
listen 443;
ssl on;
ssl_certificate /etc/ssl/certs/my_ssl.crt;
ssl_certificate_key /etc/ssl/private/my_ssl.key;
keepalive_timeout 70;
server_name www.mysite.com;
location / {
proxy_pass http://my-backend;
}
}
}
Note Make sure you are using version 1.2 or greater of Nginx otherwise chunked responses won’t work properly.
26.3. Set up with Apache
The example below shows a simple set up with Apache httpd server running in front of a standard Wisdom configuration.
LoadModule proxy_module modules/mod_proxy.so … <VirtualHost *:80> ProxyPreserveHost On ServerName www.loadbalancedapp.com ProxyPass /excluded ! ProxyPass / http://127.0.0.1:9000/ ProxyPassReverse / http://127.0.0.1:9000/ </VirtualHost>
| Apache does not support Websockets, so you may wish to use another front end proxy (such as haproxy or nginx) that does implement this functionality. |
26.4. Advanced proxy settings
When using an HTTP frontal server, request addresses are seen as coming from the HTTP server. In a usual set-up,
where you both have the Wisdom app and the proxy running on the same machine, the Wisdom app will see the requests
coming from 127.0.0.1.
Proxy servers can add a specific header to the request to tell the proxied application where the request came from.
Most web servers will add an X-Forwarded-For header with the remote client IP address as first argument. If the
proxy server is running on localhost and connecting from 127.0.0.1, Wisdom trusts its X-Forwarded-For header. If
you are running a reverse proxy on a different machine, you can set the trustxforwarded configuration item to true
in the application configuration file, like so:
trustxforwarded=true
However, the host header is untouched, it’ll remain issued by the proxy. If you use Apache 2.x, you can add a directive like:
ProxyPreserveHost on
The host: header will be the original host request header issued by the client. By combining theses two techniques,
your app will appear to be directly exposed.
If you don’t want this Wisdom app to occupy the whole root, add an exclusion directive to the proxy config (for Apache):
ProxyPass /excluded !
27. HTTP Configuration
HTTP and HTTPS ports can be configured using:
http.port = 9000 https.port = 9001
Passing -1 to one of these value disables the support.
You can also configure:
http {
upload {
disk.threshold = 16384 # The threshold switching from memory to file storage for file upload
max = -1 # The max size in bytes. If an uploaded file exceeds this size, a bad request is immediately returned
}
}
request.body.max.size = 102400 # the max body size, the rest is not read.
28. Configuring HTTPS
Wisdom can be configured to serve HTTPS. To enable this, simply tell Wisdom which port to listen to using the https.port
system property. For example:
export JVM_ARGS="-Dhttps.port=9005"
./chameleon.sh start
The https.port property can also be written in the application configuration file.
28.1. SSL Certificates
By default, Wisdom generates itself a self signed certificate, however typically this will not be suitable for serving a website in production. Wisdom uses Java key stores to configure SSL certificates and keys.
Signing authorities often provide instructions on how to create a Java keystore (typically with reference to Tomcat configuration). The official Oracle documentation on how to generate keystores using the JDK keytool utility can be found here.
Having created your keystore, the following system properties can be used to configure Wisdom to use it:
-
https.keyStore: The path to the keystore containing the private key and certificate, if not provided generates a keystore for you -
https.keyStoreType: The key store type, defaults to JKS -
https.keyStorePassword: The password, defaults to a blank password -
https.keyStoreAlgorithm: The key store algorithm, defaults to the platforms default algorithm -
https.trustStore: The path to the keystore containing the trusted key -
https.trustStoreType: The trust key store type, defaults to JKS -
https.trustStorePassword: The trust key store password, defaults to a blank password -
https.trustStoreAlgorithm: The trust key store algorithm, defaults to the platforms default algorithm
Paths are either relative to the server root or absolute.
If https.trustStore if set to noCA, all host are trusted. Be aware to not use this configuration in production.
28.2. Turning HTTP off
To disable binding on the HTTP port, set the http.port system property to be -1, eg:
export JVM_ARGS="-Dhttp.port=-1 -Dhttps.port=9005 -Dhttps.keyStore=/path/to/keystore -Dhttps.keyStorePassword=pwd"
./chameleon.sh start
The http.port property can also be written in the application configuration file, and as a consequence,
HTTP can be disabled from the application configuration file.
29. Logging configuration
Wisdom Framework uses SLF4Jas log API, and LogBack as backend. This section explains how to configure logback, but also how to use other API (JUL, commons-logging, log4j…).
29.1. Configuring Logback
Usually you want to use different logging settings when running in test, dev or on production. The best way to configure Logback is to follow the excellent guide at: http://logback.qos.ch/manual/configuration.html.
There are two main ways how you can configure Logback.
29.2. Using logback.xml
By default Logback will look for a file called logger.xml in the conf directory of your application. Wisdom
provides a default logging configuration generated into src/main/configuration.
29.3. Using Java system property to specify link to logback.xml
Logback evaluates a Java system property named logback.configurationFile. This approach is handy when you launch
your application in a service file, or a script:
./chameleon.sh -Dlogback.configurationFile=your/conf/logger.xml
This allows you to use one logging configuration for all your instances. More about that approach here: http://logback.qos.ch/manual/configuration.html.
29.4. Using JUL (Java Utils Logger)
JUL is the standard logging API provided in Java. You can use JUL logger without requiring any other configuration or additional bundles. JUL loggers are redirected to logback, so messages are written in the logback logs:
@Route(method = HttpMethod.GET, uri = "/log/jul")
public Result jul(@Parameter("message") String message) {
final Logger logger = Logger.getLogger(LogController.class.getName());
logger.severe(message);
return ok();
}
29.5. Using Commons-Logging
Apache Commons Logging is a Log API provided by Apache (http://commons.apache.org/proper/commons-logging/). To
transfer the logged entry from commons-logging to logback, you need to deploy the jcl-over-slf4j bundle. Adding the
following dependency adds the bundle:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.13</version>
</dependency>
Then, calls to commons-logging is redirected to slf4j and backed using logback:
@Route(method = HttpMethod.GET, uri = "/log/jcl")
public Result jcl(@Parameter("message") String message) {
final Log log = org.apache.commons.logging.LogFactory.getLog(LogController.class.getName());
log.error(message);
return ok();
}
29.6. Using log4j
Apache Log4J is another Log API from Apache (http://logging.apache.org/log4j). As for Apache Commons Logging, Log4J requires an additionnal bundle:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.13</version>
</dependency>
Then, calls to commons-logging is redirected to slf4j and backed using logback:
@Route(method = HttpMethod.GET, uri = "/log/log4j")
public Result log4j(@Parameter("message") String message) {
org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogController.class.getName());
log.error(message);
return ok();
}
30. Cryptographic Functions
Wisdom Framework provided a org.wisdom.api.crypto.Crypto service offering functions related to cryptography,
security and encoding.
The service can be used to:
-
encode/decode using Base64
-
compute hashes (MD5, SHA-1, SHA-256, SHA-512)
-
encrypt / decrypt message using AES
-
sign message using HMAC SHA1
-
generate random and signed token
-
encode Strings to Hexadecimal and decode them to array of bytes
When a function need a key, by default it uses the application.secret key stored in the application.conf file.
Be sure to never leak this key.
These functions are also used to sign cookies.
30.1. Using the Crypto service
package snippets.controllers.crypto;
import org.apache.felix.ipojo.annotations.Requires;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.crypto.Crypto;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
@Controller
public class CryptoController extends DefaultController {
@Requires
Crypto crypto;
@Route(method = HttpMethod.GET, uri = "/crypto")
public Result aes() {
String message = crypto.encryptAES("this is a secret");
return ok(message);
}
}
The complete API of the crypto service is available here
30.2. Configuring the Crypto service
The crypto service can be configured from the application.conf file:
# Crypto service configuration.
crypto {
default-hash: MD5 # Can be SHA-1, SHA-256 and SHA-512
aes {
key-size: 128 # The size of the AES key
transformation: "AES/CBC/PKCS5Padding" # The AES transformation to apply
iterations: 20 # Number of iteration
}
}
31. Validation Service
The validation service lets you validate user input, such as forms or Ajax request payloads. The validation service reuses the Bean Validation 1.1 API, meaning it offers the Validator as a service.
31.1. Retrieving the validation service and API
As said above, the validator service is based on Bean Validation 1.1. So it’s API is the
Bean Validation API. The main entry point is the
Validator service that lets you validate beans or parameters. The
Hibernate Validation documentation provides
everything you need to know about the usage of the validator.
You can retrieve the Validator service as follows:
package snippets.controllers.validation;
import org.apache.felix.ipojo.annotations.Requires;
import org.wisdom.api.DefaultController;
import org.wisdom.api.annotations.Body;
import org.wisdom.api.annotations.Controller;
import org.wisdom.api.annotations.Route;
import org.wisdom.api.http.HttpMethod;
import org.wisdom.api.http.Result;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Set;
@Controller
public class ManualValidationController extends DefaultController {
@Requires
Validator validator;
@Route(method = HttpMethod.POST, uri = "/validation/bean/manual")
public Result actionWithValidatedBody(
@Body User user
) {
final Set<ConstraintViolation<User>> violations = validator.validate(user);
if (violations.isEmpty()) {
return ok();
} else {
return badRequest("Oh no, this is not right: "
+ violations.iterator().next().getMessage());
}
}
public static class User {
@NotNull
@Size(min = 4, max = 8)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
31.2. Available set of constraints
The Bean Validation specification defines a set of constraints defined in the javax .validation.constraints package:
| Annotation | Supported data types | Use |
|---|---|---|
@AssertFalse |
Boolean, boolean |
Checks that the annotated element is false |
@AssertTrue |
Boolean, boolean |
Checks that the annotated element is true |
@DecimalMax(value=,inclusive=) |
BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value is less than the specified maximum, when inclusive=false. Otherwise whether the value is less than or equal to the specified maximum. The parameter value is the string representation of the max value according to the BigDecimal string representation. |
@DecimalMin(value=,inclusive=) |
BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value is larger than the specified minimum, when inclusive=false. Otherwise whether the value is larger than or equal to the specified minimum. The parameter value is the string representation of the min value according to the BigDecimal string representation. |
@Digits(integer=,fraction=) |
BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value is a number having up to integer digits and fraction fractional digits |
@Future |
java.util.Date, java.util.Calendar, java.time.chrono.ChronoLocalDate, java.time.chrono.ChronoLocalDateTime, java.time.chrono.ChronoZonedDateTime, java.time.Instant, java.time.OffsetDateTime, java.time.Year, java.time .YearMonth |
Checks whether the annotated date is in the future |
@Max(value=) |
BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value is less than or equal to the specified maximum |
@Min(value=) |
BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value is higher than or equal to the specified minimum |
@NotNull |
Any type |
Checks that the annotated value is not null. |
@Null |
Any type |
Checks that the annotated value is null |
@Past |
java.util.Date, java.util.Calendar, java.time.chrono.ChronoLocalDate, java.time.chrono.ChronoLocalDateTime, java.time.chrono.ChronoZonedDateTime, java.time.Instant, java.time.OffsetDateTime, java.time.Year, java.time .YearMonth |
Checks whether the annotated date is in the past |
@Pattern(regex=,flag=) |
CharSequence |
Checks if the annotated string matches the regular expression regex considering the given flag match |
@Size(min=, max=) |
CharSequence, Collection, Map and arrays |
Checks if the annotated element’s size is between min and max (inclusive) |
@Valid |
Any non-primitive type |
Performs validation recursively on the associated object. If the object is a collection or an array, the elements are validated recursively. If the object is a map, the value elements are validated recursively. |
The default implementation of the validation service uses Hibernate Bean Validation and proposes additional useful constraints. To have accessed to them you need to add the following dependency to your pom.xml file:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.2.Final</version>
<scope>provided</scope>
</dependency>
These additional constraints are:
| Annotation | Supported data types | Use | @CreditCardNumber(ignoreNonDigitCharacters=) |
|---|---|---|---|
CharSequence |
Checks that the annotated character sequence passes the Luhn checksum test. Note, this validation aims to check for user mistakes, not credit card validity! See also Anatomy of Credit Card Numbers. ignoreNonDigitCharacters allows to ignore non digit characters. The default is false. |
@EAN |
CharSequence |
Checks that the annotated character sequence is a valid EAN barcode. type determines the type of barcode. The default is EAN-13. |
CharSequence |
Checks whether the specified character sequence is a valid email address. The optional parameters regexp and flags allow to specify an additional regular expression (including regular expression flags) which the email must match. |
|
@Length(min=, |
CharSequence |
Validates that the annotated character sequence is between min and max included |
@LuhnCheck(startIndex= , |
CharSequence |
Checks that the digits within the annotated character sequence pass the Luhn checksum algorithm (see also Luhn algorithm). startIndex and endIndex allow to only run the algorithm on the specified sub-string. checkDigitIndex allows to use an arbitrary digit within the character sequence as the check digit. If not specified it is assumed that the check digit is part of the specified range. Last but not least, ignoreNonDigitCharacters allows to ignore non digit characters. |
@Mod10Check(multiplier=, |
CharSequence |
Checks that the digits within the annotated character sequence pass the generic mod 10 checksum algorithm. multiplier determines the multiplier for odd numbers (defaults to 3), weight the weight for even numbers (defaults to 1). startIndex and endIndex allow to only run the algorithm on the specified sub-string. checkDigitIndex allows to use an arbitrary digit within the character sequence as the check digit. If not specified it is assumed that the check digit is part of the specified range. Last but not least, ignoreNonDigitCharacters allows to ignore non digit characters. |
@Mod11Check(threshold=, |
CharSequence |
Checks that the digits within the annotated character sequence pass the mod 11 checksum algorithm. threshold specifies the threshold for the mod11 multiplier growth; if no value is specified the multiplier will grow indefinitely. treatCheck10As and treatCheck11As specify the check digits to be used when the mod 11 checksum equals 10 or 11, respectively. Default to X and 0, respectively. startIndex, endIndex acheckDigitIndex and ignoreNonDigitCharacters carry the same semantics as in @Mod10Check. |
@NotBlank |
CharSequence |
Checks that the annotated character sequence is not null and the trimmed length is greater than 0. The difference to @NotEmpty is that this constraint can only be applied on strings and that trailing white-spaces are ignored. |
@NotEmpty |
CharSequence, Collection, Map and arrays |
Checks whether the annotated element is not null nor empty |
@Range(min=, |
BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types |
Checks whether the annotated value lies between (inclusive) the specified minimum and maximum |
@SafeHtml(whitelistType= , |
CharSequence |
Checks whether the annotated value contains potentially malicious fragments such as <script/>. In order to use this constraint, the jsoup library must be part of the class path. With the whitelistType attribute a predefined whitelist type can be chosen which can be refined via additionalTags or additionalTagsWithAttributes. The former allows to add tags without any attributes, whereas the latter allows to specify tags and optionally allowed attributes using the annotation @SafeHtml.Tag. |
@ScriptAssert(lang=, |
Any type |
Checks whether the given script can successfully be evaluated against the annotated element. In order to use this constraint, an implementation of the Java Scripting API as defined by JSR 223 ("Scripting for the JavaTM Platform") must part of the class path. The expressions to be evaluated can be written in any scripting or expression language, for which a JSR 223 compatible engine can be found in the class path. |
@URL(protocol=, |
31.3. Extending the set of constraints
If the set of provided constraints is not enough, you can create your own constraints and validators. To implement such custom constraints, follow the Bean Validation guide. However you should:
-
put the annotation and its validator in the same package in the same bundle.
-
export the package containing the annotation (and so the validator)
31.4. Internationalization
Constraints define a message. If this message is wrapped into {}, it is interpreted as a message key and the value
is looked into the internationalization service (Check Internationalize your application for more details) as in:
@Route(method = HttpMethod.GET, uri = "/validation/i18n")
public Result actionWithValidatedParams(
@NotNull(message = "{id_not_set}") @Parameter("id") String id,
@Size(min = 4, max = 8, message = "{name_not_fit}") @Parameter("name") String name) {
return ok();
}
Then the message is looked into the resource bundles:
id_not_set: The id must be set
name_not_fit: The name '${validatedValue}' must contain between {min} and {max} characters
id_not_set: L'identifiant n'est pas spécifié
name_not_fit: Le nom '${validatedValue}' doit contenir entre {min} et {max} caractères
So, Wisdom is picking the right message according to the request languages (passed into the ACCEPT_LANGUAGE header).
As you can see, messages can use variables to make the error more informative. For instance, in English the message
is The name 'ts' must contain between 4 and 8 characters, while on a browser using the French language, the message is
Le nom 'ts' doit contenir entre 4 et 8 caractères. If the asked language is not available, default
(english) is used.
32. Using Wisdom Executors
Wisdom provides a set of services to handle backend and scheduled tasks:
-
org.wisdom.api.concurrent.ManagedExecutorServiceacting as a regular thread pool. It’s a enhanced version of the JavaExecutorServices. -
org.wisdom.api.concurrent.ManagedScheduledExecutorServicesupporting scheduling. It’s a enhanced version of the JavaScheduledExecutorServices.
Main difference with the pur Java versions are:
-
A management API to know the current state of the thread pool (size, max, core, number of completed tasks…)
-
Hung task detection
-
They return enhanced futures supporting
onSuccessandonFailurecallbacks -
Support execution context to migrate data from the caller thread to the background thread
32.1. Using the system executor
By default, Wisdom has a system pool you can use to run backend / asynchronous tasks. Be aware that Wisdom is relying on it, so, it you plan to submit lots of jobs, you should create your own executor (see below).
Once injected, using the service is very straightforward:
@Component
@Instantiate
public class ExecutorSample {
@Requires(filter = "(name=" + ManagedExecutorService.SYSTEM + ")", proxy = false)
ManagedExecutorService service;
// Equivalent to this if you don't need the enhanced API
@Requires(filter = "(name=" + ManagedExecutorService.SYSTEM + ")", proxy = false)
ExecutorService executor;
public void doSomething() {
service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "heavy computation has been done";
}
});
// With handler
ManagedFutureTask<?> task = service.submit(new Runnable() {
@Override
public void run() {
System.out.println("Happening in background");
}
});
// Cancel it.
task.cancel(true);
}
}
32.2. Using the system scheduler
As the system executor, the system scheduler is available as a service. Using it is straightforward:
@Component
@Instantiate
public class SchedulerSample {
@Requires(filter = "(name=" + ManagedScheduledExecutorService.SYSTEM + ")")
ManagedScheduledExecutorService service;
// Equivalent to this if you don't need the enhanced API
@Requires(filter = "(name=" + ManagedScheduledExecutorService.SYSTEM + ")")
ScheduledExecutorService executor;
public void doSomething() {
service.schedule(new Callable<String>() {
@Override
public String call() throws Exception {
return "will be done tomorrow";
}
}, 1, TimeUnit.DAYS);
// With handler
ManagedFutureTask<?> task = service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("Happening in background");
}
}, 1, 5, TimeUnit.MINUTES);
// Cancel it.
task.cancel(true);
}
}
32.3. Configuring the system executor and scheduler
The configuration of the system executor and scheduler is made from the application.conf file:
pools {
executors {
wisdom-system-executor { # This is a name of the system executor
threadType: POOLED # Thread type between POOLED and DAEMON
hungTime : 60s # Hung threshold
coreSize : 5 # Number of core threads created on start
maxSize : 25 # Maximum numbers of threads
keepAlive : 5s # The idle time before disposing an unused thread
workQueueCapacity : 2147483647 # Size of the work queue
# (2147483647 = Integer.MAX = unbound)
priority: 5 # the thread priority
}
}
schedulers {
wisdom-system-scheduler {
threadType: POOLED # Thread type between POOLED and DAEMON
hungTime : 60s # Hung threshold
coreSize : 5 # Number of threads
priority: 5 # the thread priority
}
}
}
The values written above are the default values.
32.4. Creating your own executor or scheduler
You can create another executor or scheduler by adding its configuration in the application.conf file:
pools {
executors {
my-executor { # This is a name of the executor, used in the @Requires filter
threadType: DAEMON # Thread type between POOLED and DAEMON
hungTime : 1h # Hung threshold
coreSize : 3 # Number of core threads created on start
maxSize : 5 # Maximum numbers of threads
keepAlive : 1h # The idle time before disposing an unused thread
workQueueCapacity : 5 # Size of the work queue
# (2147483647 = Integer.MAX = unbound)
priority: 2 # the thread priority
}
}
schedulers {
my-scheduler {
threadType: POOLED # Thread type between POOLED and DAEMON
hungTime : 10s # Hung threshold
coreSize : 10 # Number of threads
priority: 8 # the thread priority
}
}
}
Once configured, you can retrieve them using:
@Requires(filter = "(name=my-executor)", proxy = false)
ManagedExecutorService executor;
@Requires(filter = "(name=my-scheduler)", proxy = false)
ManagedScheduledExecutorService scheduler;
32.5. Using Managed Task
As said above, once of the main difference with the 'regular' Java execution service is the type of future
returned by the different methods. Wisdom enhanced the regular feature with administration methods as well as callbacks.
32.5.1. Cancelling a task
First, as usually a task can be cancelled using the cancel(boolean mayInterrupt) method.
32.5.2. Task callbacks
You can register callbacks on a submitted tasks:
@Component
@Instantiate
public class ExecutorCallbackSample {
@Requires(filter = "(name=" + ManagedExecutorService.SYSTEM + ")", proxy = false)
ManagedExecutorService service;
public void doSomething() {
service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "heavy computation has been done";
}
}).onSuccess(new ManagedFutureTask.SuccessCallback<String>() {
@Override
public void onSuccess(ManagedFutureTask<String> future, String result) {
System.out.println("Task has returned " + result);
}
}).onFailure(new ManagedFutureTask.FailureCallback() {
@Override
public void onFailure(ManagedFutureTask future, Throwable throwable) {
System.out.println("Task has thrown an exception");
}
}, MoreExecutors.directExecutor());
}
}
When registering these callbacks, you can pass an 'executor' that will invoke the callback method. By default,
callbacks are invoked in the same executor as the listened task. To use the thread having submitted the task, use
MoreExecutors.directExecutor() as for the failure callback on the previous example.
32.6. Detecting hung tasks
Each executor and schedulers has a 'hung threshold' allowing to detect hung task. The executor or scheduler do not do anything when a hung task is detected. It’s a only for monitoring and administration support.
To retrieve the hung tasks use: executor.getHungTasks(). You can also check on the future returned on submission.
32.7. Execution Context
When submitting a task, Wisdom retrieve all org.wisdom.api.concurrent.ExecutionContextService services to build an
org.wisdom.api.concurrent.ExecutionContext. This context is captured during the submission (in the caller threads),
and applied before the execution of the task. After its completion, the context is un-applied. This mechanism let
you migrate data that are thread-sensitive to the thread that is going to execute a task.
You can implement your own org.wisdom.api.concurrent.ExecutionContextService to extend this support.
| Execution context are not supported for periodic and scheduled tasks |
33. Vertx
Wisdom is based on Vertx. this section explains how you can benefit from the underlying Vertx infrastructure.
33.1. Accessing the Event Bus
The Vertx Event Bus is exposed as an OSGi service. To access it, you first need to add the following dependency to
your pom.xml file:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<scope>provided</scope>
</dependency>
The version is inherited from the imported bom.
|
Once done, you can access the Event Bus as following:
@Requires
EventBus bus; // org.vertx.java.core.eventbus.EventBus
33.2. Sending data on the bus
Once you access the event bus you can send and publish data:
@Service
public class MyDataSender implements Scheduled {
private final static Logger LOGGER = LoggerFactory.getLogger(MyDataSender.class);
@Requires
EventBus bus;
Random random = new Random();
@Every("1s")
public void generateAndSendData() {
int value = random.nextInt(40);
bus.publish("/data", value);
LOGGER.info("Has published {} on {}", value, "/data");
}
}
33.3. Receiving data from the bus
You can also register a handler to receive data transmitted through the bus:
@Component
@Instantiate
public class MyDataReceiver {
private final static Logger LOGGER = LoggerFactory.getLogger(MyDataReceiver.class);
@Requires
EventBus bus;
private Handler<Message> handler;
@Validate
public void start() {
handler = new Handler<Message>() {
@Override
public void handle(Message message) {
LOGGER.info("Has received " + message.body());
}
};
bus.registerHandler("/data", handler);
}
@Invalidate
public void stop() {
bus.unregisterHandler("/data", handler);
}
}
33.4. Clustering
Vertx support distributed Event Bus, and so the creation of clustered configurations. The cluster is managed using Hazelcast, an In-Memory Data Grid is data management software. So, events sent to the event bus can be received by receivers running on a remote machine.
To enable the clustering you first need to configure Vertx. Edit the src/main/configuration/application.conf file
and add:
vertx {
cluster-host = "localhost" # Address or IP used for the clustering
cluster-port = 25501 # The port used for the clustering
}
| The couple host-port must be unique. If not, Vertx cannot bind to the address, and the event bus creation fails. |
Once done, create the src/main/configuration/cluster.xml containing the Hazelcast configuration. Copy and edit
this file is a good start.
| Do not forget to edit the interface on which Hazelcast is bound. |
When clustering is enabled, you will see a stack trace on startup. Just ignore it:
java.lang.IllegalArgumentException: version string 'unknown' is not valid at com.hazelcast.management.ManagementCenterIdentifier.getVersionAsInt(ManagementCenterIdentifier.java:44) at com.hazelcast.management.ManagementCenterIdentifier.<init>(ManagementCenterIdentifier.java:68) at com.hazelcast.management.ManagementCenterService.newManagementCenterIdentifier(ManagementCenterService.java:201) ...
33.5. HTTP Configuration
Vertx is also used as network stack by Wisdom, so you can configure various aspects of HTTP handling and also declare several servers.
The following example depicts how to configure the different aspect of HTTP:
vertx {
acceptBacklog: 10000 # The accept backlog
receiveBufferSize: 4096 # The receive buffer size
sendBufferSize: 4096 # The send buffer size
maxWebSocketFrameSize: 65536 # The maximum websocket frame size
websocket-subprotocols: [] # The list of websocket subprotocols that are allowed
# sockJS configuration
sockjs {
prefixes: [] # The list of path that need to be handled by SockJS
timeout: 5s # The server sends a close event if a client has not been seen for a while.
heartbeat: 5s # Heartbeat time to keep long running HTTP connections opened.
max: 4kb # Max frame size
}
compression: true # does the server should handle compression or not, enabled by default.
}
You can also disable the defaults HTTP servers and provides your own:
vertx {
servers {
server1 { # Name of the server
host: "0.0.0.0" # host IP can be omitted (listening on all interfaces)
ssl: true | false # enables or disables HTTPS
authentication: true|false # enables or disables the mutual authentication
port: -1, 0, integer # the port number, 0 for random, -1 to disable
allow: [] # array of path that should be allowed such as /foo*
deny: [] # array of path that are not allowed such as /private*
onDenied: "/foo" # when a denied request is received, where the request is redirected
}
server2 {
# ...
}
}
}
With such mechanism you can configure as many HTTP server you want with different port, security level and allow/deny
policies. These policies are checked for HTTP request and websockets. In the case of HTTP requests if the request is
denied, it can either return a FORBIDDEN result or redirect the request to the onDenied url. For websockets the
denied messages are just ignored. By default, everything is allowed.
The host value specify on which host / network interface the server is bound. By default all interfaces are bound
("0.0.0.0").
The ssl flag enables or disables HTTPS. The authentication lets you specify whether or not the server require client mutual authentication (see http://docs.oracle.com/cd/E19226-01/820-7627/bncbs/index.html).
The vert.x response encoding (compression) is made for all response having a size between the encoding.max and encoding.min
configuration keys (size in bytes such as 1Kb).
34. Extending the Wisdom Build Process
Wisdom lets you extend the build process and react to file updates to improve the development experience. The Wisdom build system is based on Apache Maven, and so, to extend it you just implement a regular Maven Plugin, aka Mojo.
This section does not explain how to develop a regular Maven plugin. Refer to The Maven Mojo Developer Documentation for further details. However, it explains how your Mojo can participate in the Wisdom Watch Mode.
34.1. Rationale
We are often asked why we want a Watch Mode based on Maven, and not something separated, as most of frameworks do. This is because duplicating the build process creates a strange experience. The result looks the same, but it may not really be the same. Debugging such kind of differences is almost impossible. So in Wisdom, Maven is the reference and the Watch Mode is built on top of it. During the Watch Mode, Wisdom invokes the Maven Mojos directly.
This also means that using your Wisdom Build extension is just like using any other Maven plugin: * add the plugin in the pom.xml file * configure it * you’re done!
34.2. From Mojo To Watcher
The Wisdom Watch Mode is based on the concept of Watcher. A Watcher is an object who
is notified when a file, accepted by the watcher, is created, updated or deleted.
So, first, in your Mojo’s project, add the dependency on the Wisdom-Maven-Plugin:
<dependency>
<groupId>org.wisdom-framework</groupId>
<artifactId>wisdom-maven-plugin</artifactId>
<version>0.10.0</version>
</dependency>
To make your Mojo participate in the Wisdom Watch Mode, it needs to become a Watcher and
registers itself in the
watch pipeline. In order to do that, the easiest way is to make your Mojo extend AbstractWisdomWatcherMojo
instead of AbstractMojo.
The Pipeline is the ordered sequence of Watchers invoked in the Watch Mode. This list contains Mojo
instances (that are also Watcher). This list is collected during the regular execution of your build. So the
order is the same as the Maven plugin’s order.
|
By extending AbstractWisdomWatcherMojo, it automatically registers your Mojo to the Pipeline.
As a Watcher your mojo has to implement the following methods:
/**
* Checks whether the given file is managed by the current watcher. Notice that implementation must not check
* for the existence of the file as this method is also called for deleted files.
*
* @param file is the file.
* @return {@literal true} if the watcher is interested in being notified on an event
attached to the given file,
* {@literal false} otherwise.
*/
public boolean accept(File file);
/**
* Notifies the watcher that a new file is created.
*
* @param file is the file.
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* return {@literal true} to let other watchers be notified.
* @throws WatchingException if the watcher failed to process the given file.
*/
public boolean fileCreated(File file) throws WatchingException;
/**
* Notifies the watcher that a file has been modified.
*
* @param file is the file.
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* returns {@literal true} to let other watchers to be notified.
* @throws WatchingException if the watcher failed to process the given file.
*/
public boolean fileUpdated(File file) throws WatchingException;
/**
* Notifies the watcher that a file was deleted.
*
* @param file the file
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* return {@literal true} to let other watchers be notified.
* @throws WatchingException if the watcher failed to process the given file.
*/
public boolean fileDeleted(File file) throws WatchingException;
The accept method let you select the file the watcher handles. For instance, if you want to
handle .css files, your accept method will be similar to:
@Override
public boolean accept(File file) {
return
(WatcherUtils.isInDirectory(file, WatcherUtils.getInternalAssetsSource(basedir))
|| (WatcherUtils.isInDirectory(file, WatcherUtils.getExternalAssetsSource(basedir)))
)
&& WatcherUtils.hasExtension(file, "css");
}
It first checks the location of the file. In this example, it checks that the file is either in the internal assets, i
.e. src/main/resources/assets or in the global assets (src/main/assets). Then, it checks for the
file’s extension. Notice that you must not check for the existence, as the accept method is
also called for deleted file.
Once the accept method is implemented, you need to implement the:
-
fileCreated(File file): called when an accepted file is created -
fileUpdated(File file): called when an accepted file is updated -
fileDeleted(File file): called when an accepted file is deleted
In general, the fileCreated and fileUpdated methods call the Mojo’s main method (execute), while the
fileDeleted method cleans up generated files when the input file is deleted.
These methods return a boolean controlling the pipeline execution. If one of the method returns
false, the pipeline is interrupted, and none of the following watchers are called. In most cases,
you need to return true.
34.3. Handling errors : the Watching Exceptions
As you may have noticed, the Watcher's methods can throw WatchingException. Watching Exceptions let you
indicate an issue in the processing. A default error page is generated from this exception with
the message, of the guilty file, and if specified, the line and position of the error.
34.4. Initialization and Injection
As your Watcher is a Mojo and the pipeline is reusing the Mojo’s instances, all of the injected
parameters from your Mojo are still available. Notice that the execute method is called before
the Watcher’s methods, meaning that you can do the required initialization there if required.
| The pipeline is executed by one thread only (no parallel execution). |
34.5. Disabling the Watch Mode
Maybe your Mojo is skipped. In that case you need to explicitly unregister it from the pipeline by calling the
removeFromWatching method in your execute method.
34.6. Generating a Watcher using the Maven Archetype
We provide a Maven Archetype generating the project to develop your Watcher. Executes the following command to generate it:
mvn org.apache.maven.plugins:maven-archetype-plugin:generate \ -DarchetypeArtifactId=wisdom-simple-watcher-archetype \ -DarchetypeGroupId=org.wisdom-framework \ -DarchetypeVersion=0.10.0 \ -DgroupId=YOUR_GROUPID \ -DartifactId=YOUR_ARTIFACTID \ -Dversion=1.0-SNAPSHOT
34.7. Using Node and NPMs
Most of the Web Tools developed today are available as NPM (i.e. Node’s module). Fortunately, Wisdom provides a set of utility classes to install and execute NPM.
Wisdom has it’s own node version installed in ~/.wisdom/node/Node_Version. So, the
NPMs you are installing and executing do not conflict with the user’s ones. These NPM are installed in
~/.wisdom/node/Node_Version/lib/node_modules.
|
| The Wisdom’s NPM support only work for executable NPM’s. |
To use a NPM tool, declare a NPM object as follows:
NPM myNPM = npm(this, npm_name, version);
In most cases, declare your NPM object as a field, as both the execute method and watcher’s method will invoke it. If the specified NPM is not install yet, it will install it (from NPM).
It is a good practice to declare the NPM’s version as a parameter, so the user can specify the version:
@Parameter(defaultValue = "0.3.4")
String version;
To execute the NPM, use:
myNPM.execute("exec", input.getAbsolutePath(), destination.getAbsolutePath() ...);
The "exec" argument is the name of the command to launch. Available commands are available from the package.json
files of the NPM, in the bin part:
"bin": {
"myth": "bin/myth"
},
The others arguments are the command’s arguments. In the previous example the input and output paths.
So, for instance, here are the lines invoking the myth processing:
try {
int exit = myth.execute("myth", input.getAbsolutePath(), destination.getAbsolutePath());
getLog().debug("Myth execution exiting with status: " + exit);
} catch (MojoExecutionException e) {
throw new WatchingException("An error occurred during Myth processing of " + input.getAbsolutePath(), e);
}
The CoffeeScript invocation is made by:
try {
int exit = coffee.execute(COFFEE_SCRIPT_COMMAND, "--compile", "--map", "--output", out.getAbsolutePath(),
input.getAbsolutePath());
getLog().debug("CoffeeScript compilation exits with " + exit + " status");
} catch (MojoExecutionException e) {
if (!Strings.isNullOrEmpty(coffee.getLastErrorStream())) {
throw buildCompilationError(coffee.getLastErrorStream(), input);
} else {
throw new WatchingException(ERROR_TITLE, "Error while compiling " + input
.getAbsolutePath(), input, e);
}
}
In this last example, we retrieve the error stream (if any) and parse the error message to build a
WatchingException containing the compilation error, the guilty line and position.
34.8. Publishing your application in the Wisdom Extension Registry.
So you have made your very own extension for Wisdom and you want the world to see it? Great! You can add it to the Wisdom Extension registry located here.
In order to add your extension to the registry you need to create a Json file. This file will contain the details about your extension such as the name, the project’s homepage and so on.
It may be helpful to have information on your homepage on how to use your extension. Extensions are generally used by either adding the extension as a dependency in a project’s pom file, or as a maven plugin.
Here is a quick overview of the structure for the json file.
{
"name": "wisdom-monitor",
"version":"0.6.2",
"description": "The Wisdom monitor provides an administration interface letting you know the current status of the Wisdom server and deployed applications. This interface is modular and can be easily extended with your own views.",
"repository": {
"type": "git",
"url": "https://github.com/wisdom-framework/wisdom/tree/master/extensions/wisdom-monitor"
},
"author": "The Wisdom Team",
"license": {
"type": "Apache",
"url": "https://github.com/wisdom-framework/wisdom/blob/master/LICENSE.txt"
},
"homepage": "https://github.com/wisdom-framework/wisdom/tree/master/extensions/wisdom-monitor",
"keywords": [
"cpu usage",
"system",
"monitor" ,
"memory",
"threads",
"bundles"
]
}
Required fields are: name - the name of your extension, this name must be unique; version,
description, and homepage. The other fields are optional.
After you have have saved your Json file to a location accessible on the web, you can then enter its url into the text box at the bottom of the registry page. (If you don’t see the input box make sure you’re on the developers view version).
| Currently to update your extension you just resubmit the url, in the same process you did for adding it. |
35. The Wisdom Project
The Wisdom Framework is an open source project licensed under the Apache License 2.0.
All contributions are welcome !
35.1. Source Code
The source code is hosted on GitHub: https://github.com/wisdom-framework/wisdom/.
35.2. Bug Tracker
The Wisdom Project is using the GitHub Issue Tracker: https://github.com/wisdom-framework/wisdom/issues.
35.3. Continuous Integration
The main continuous integration server is provided by Cloudbees (Wisdom adheres to the FOSS program): https://wisdom-framework.ci.cloudbees.com/
35.4. Building Wisdom
To build Wisdom you need Apache Maven 3.2.1+. The Wisdom build is divided in several profiles:
mvn clean install -Pcore,\!framework,\!extensions,\!documentation mvn clean install -P\!core,framework,extensions,documentation
To skip the tests, append the -DskipTests option to the command line.
As tests are being executed in a Wisdom server, you may experience some timing issues. If it’s
the case, try setting the TIME_FACTOR to extend the wait durations:
mvn clean install -Pfull-runtime -DTIME_FACTOR=2
| Time Factor: The time factor extends the time waited to determine whether a service is published or not. It is also used to detect the Stability of the platform, i.e. when all services and bundles are started. |
35.5. Releasing Wisdom
As Wisdom contains a Maven Plugin used by other projects and extending the lifecycle (so called extension), the maven-release-plugin cannot be used. This Maven limitation makes the release process a bit cumbersome.
First, check that your ~/.m2/settings.xml contains you GPG password when the release profile is activated:
<profile>
<id>release</id>
<properties>
<gpg.passphrase>YOUR_PASSPHRASE</gpg.passphrase>
</properties>
</profile>
Then check that your credential to Sonatype OSS are written in your ~/.m2/settings.xml:
<server> <id>sonatype-nexus-staging</id> <username>YOUR_USERNAME</username> <password>SECRET</password> </server>
You also need to be sure you have the permission to push to the Wisdom Git repository.
-
Check that you have the latest version, and declare the code freeze on wisdom-discuss.
-
Use the maven-versions-plugin to update to the release version:
# We will use this version everywhere: export VERSION=enter the released version here mvn clean mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false
-
Be sure that you don’t have any
-SNAPSHOT<anymore.
egrep --exclude="**/target*" -ir "(-SNAPSHOT)<" .
Notice that you will have matches, and that’s normal. You need to verify by yourself.
-
Let’s now check that everything is fine:
mvn install -Prelease,core,\!framework,\!extensions,\!documentation -Dmaven.repo.local=$HOME/tmp/repo-for-$VERSION mvn install -Prelease,\!core,framework,extensions,documentation -Dmaven.repo.local=$HOME/tmp/repo-for-$VERSION mvn clean -Prelease -Dmaven.repo.local=$HOME/tmp/repo-for-$VERSION
If everything is fine, continue, else rollback. It’s generally recommended to check that the artifact are correctly signed with GPG.
-
Commit the updated pom files, create a tag, and push.
git add -A git commit -m "[RELEASE] update to release version: $VERSION" git push origin master git tag -a wisdom-framework-$VERSION -m "[RELEASE] Create tag for $VERSION" git push origin wisdom-framework-$VERSION
-
The tag is created, we are going to bump to the version to the next development version:
The new version must ends with -SNAPSHOT
|
export NEW_VERSION=enter the new version here mvn versions:set -DnewVersion=$NEW_VERSION -Pcore,\!framework,\!extensions,\!documentation -DgenerateBackupPoms=false mvn clean install -DskipTests -Pcore,\!framework,\!extensions,\!documentation mvn versions:set -DnewVersion=$NEW_VERSION -P\!core,framework,extensions,documentation -DgenerateBackupPoms=false mvn clean install -P\!core,framework,extensions,documentation -DskipTests git add -A git commit -m "[RELEASE] update to development version: $NEW_VERSION" git push origin master
What you did so far is the equivalent to the mvn release:prepare. It’s now time to perform the release, so building the released artifacts.
-
Checkout the tag we created
git fetch git tag -l git checkout tags/wisdom-framework-$VERSION
-
Let’s build and deploy the release, as everything has been checked before we can safely skip the tests:
mvn clean deploy -Prelease,core,\!extensions,\!framework,\!documentation -DskipTests -DskipITs mvn clean deploy -Prelease,framework,extensions,documentation,\!core -DskipTests -DskipITs
-
When done, open a browser to https://oss.sonatype.org/, and log in.
-
On the left sidebar, go to 'Staging Repositories', and find the created repository.
-
Select it, and click on the
closebutton. A couple of checks are performed, such as checking that all the expected artifacts are available, and that all artifacts are correctly signed. -
Once done, re-check the repository and click on the 'release' button.
-
Wait until the artifacts lands in Maven Central.
-
Announce the release on the
wisdom-discussmailing list. Don’t forget to integrate the release notes. These releases notes can be computed from the Github Issue Tracker. -
On the Github Issue Tracker, don’t forget to create a new milestone for the next release, and decide which issues are going to be fixed in this new release.
-
Almost done, now we have to generate the 'documentation artifacts'
mkdir ~/Desktop/Wisdom-$VERSION # Copy the source code archive cp target/*-source-release.zip ~/Desktop/Wisdom-$VERSION # Copy the documentation jar cp documentation/documentation/target/documentation-$VERSION.jar ~/Desktop/Wisdom-$VERSION # Generate and copy the javadoc mvn javadoc:aggregate-jar -Pcore,framework,extensions,documentation cp target/*$VERSION-javadoc.jar ~/Desktop/Wisdom-$VERSION # Generate the wisdom-maven-plugin site cd core/wisdom-maven-plugin mvn site site:jar cp target/*$VERSION-site.jar ~/Desktop/Wisdom-$VERSION
-
On Github, go to the release section (https://github.com/wisdom-framework/wisdom/releases). A new release should have been created. Edit its name to be 'Wisdom Framework $VERSION' (replace $VERSION) and add a link to the release notes. In addition upload the
-source-release.zipartifacts as binaries. -
Send the 3 others artifacts to wisdom-commit so they can be uploaded on the web site. The upload need to wait until the artifact reach Maven Central.
-
Send a mail to wisdom-discuss to announce the release
-
Update the docker images, they will be rebuilt automatically by DockerHub. The image are built from https://github.com/cescoffier/wisdom-docker. You need to update to the release version, then launch
mvn clean install, and push the result as to a tag and to the master branch. Don’t forget to force the addition of all jars:---- # Update to version mvn clean install # Remove the cache used for test rm -Rf target/wisdom/chameleon-test-cache git add -f target/wisdom/**/* git commit -m "Update docker image to ${VERSION}" git push origin master ----
If you reach this point, you made it ! Congratulations !
36. F.A.Q.
36.1. IDE Integration
36.1.1. Eclipse
Maven integration in Eclipse requires a m2e connector to fully use plugins in the native Eclipse’s build process. An overview of this issue can be found at here. For now there is no plan to add such a connector for Wisdom. To remove warnings and errors, we recommend to set an lifecycle mapping file in your Eclipse.
Here is the minimal lifecycle mapping file to remove wisdom’s errors in Eclipse.
Eclipse compiler may generate classes in target\classes. If you see issue in the watch mode, change this folder to be target/classes-eclipse, and let Wisdom compile your classes.
36.2. Using Eclipse Equinox
By default Wisdom uses Apache Felix as OSGi framework. You can also use
Eclipse Equinox. To switch to Equinox, edit your project’s pom.xml file and add the
<wisdomRuntime> parameter of the wisdom-maven-plugin:
<plugin>
<groupId>org.wisdom-framework</groupId>
<artifactId>wisdom-maven-plugin</artifactId>
<version>0.10.0</version>
<extensions>true</extensions>
<configuration>
<wisdomRuntime>equinox</wisdomRuntime>
</configuration>
</plugin>