This is the simplest fastest full fledged web server we could come up with.
This is the simplest fastest full fledged web server we could come up with.
Fluent-http is a very capable web stack based on SimpleFramework HTTP server.
Its goal is to provide everything a java web developer needs to build modern web sites with REST back-ends and HTML5 front-ends.
Simple rules are used to develop fluent-http and we believe it's what makes it a pleasure to use:
java-1.8
A single dependency is what it takes. Release versions are deployed on Maven Central:
net.code-story http 2.105
Starting a web server that responds
Hello Worldon
/uri is as simple as:
import net.codestory.http.WebServer;public class HelloWorld { public static void main(String[] args) { new WebServer().configure(routes -> routes.get("/", "Hello World")).start(); } }
What this code does:
8080
GETrequests on
/, it will respond
Hello Worldas
text/html
404error
$CURRENT_DIR/appfolder as static resources
Not too bad for a one-liner, right?
Adding more routes is not hard either. It's based on Java 8 Lambdas:
new WebServer().configure(routes -> routes .get("/", "Hello World") .get("/Test", (context) -> "Other Test").url("/person") .get((context) -> new Person()) .post((context) -> { Person person = context.extract(Person.class); // Do something return Payload.created(); }) .url("/company") .get((context) -> new Company()) .post((context) -> { Company company = context.extract(Company.class); // Do something return Payload.created(); })
).start();
Routes can have path parameters:
routes.get("/hello/:who", (context, who) -> "Hello " + who)); routes.get("/add/:first/to/:second", (context, first, second) -> Integer.parseInt(first) + Integer.parseInt(second));
Routes can also have query parameters:
routes.get("/hello?who=:who", (who) -> "Hello " + who)); routes.get("/hello?to=:to&from=:from", (to, from) -> "Hello " + to + " from " + from));
Notice that path and query parameters have to be of type
String. To overcome this limitation, fluent-http can be configured with
Resource classesinstead of simple lambdas.
To sum up:
lambdas) is very easy to read but comes with limitations.
Resource classes) has no such limitation and is very natural to people used to
Spring MVCor
Jersey.
... routes.add(new PersonResource()); routes.add("calculation", new CalculationResource()); ...@Prefix("/person") public class PersonResource { @Post("/") public void create(Person person) { // Do something }
@Put("/:id") public Payload update(String id, Person person) { return new Payload(201); }
@Get("/:id") public Person find(String id, Context context, Headers headers, Request request, Response response, Cookies cookies, Query query, User user) { Person person = ... return NotFoundException.notFoundIfNull(person); } }
public class CalculationResource { @Get("/add/:first/to/:second") public int add(int first, int second) { return first + second; } }
Each method annotated with
@Get,
@Head,
@Post,
@Put,
@Optionsor
@Deleteis a route. The method can have any name. The parameters must match the uri pattern. Parameters names are not important but it's a good practice to match the uri placeholders. The conversion between path parameters and method parameters is done with Jackson.
We can also let the web server take care of the resource instantiation. It will create a singleton for each resource, and recursively inject dependencies as singletons. It's a kind of poor's man DI framework, and be assured that your favourite DI framework can be plugged in also.
routes.add(CalculationResource.class);
Resources can read values from header parameters. To do this, use
context.header("headerName"):
@Get("tokenFromHeader") public User user(Context context) { return new Payload(context.header("token")).withCode(HttpStatus.OK); }
Before we take an in-depth look at fluent-http, you can go take a look at samples here if it's how you prefer to learn.
By default, fluent-http runs in
developmentmode.
appfolder
.mapand
.sourcefor coffee and less files
In production mode:
appfolder
.mapand
.sourcefiles
We encourage you to use production mode whenever you deploy you website in real life and not activate it in dev mode. To activate production mode, start the JVM with
-DPROD_MODE=true.
When a web server is started, it automatically treats files found in
appfolder as static pages. The
appfolder is searched first on the classpath (think
src/main/resources/app) and then in the working directory.
So the simplest way to start a web server is in fact:
import net.codestory.http.WebServer;public class HelloWorld { public static void main(String[] args) { new WebServer().start(); } }
Instead of relying on the default port, you can specify a port yourself. (Not sure anyone does this anymore thanks to Docker containers.)
new WebServer().start(4242);
... or you can also let the web server find a tcp port available.
int port = new WebServer().startOnRandomPort().port();
This is specially helpful for integration tests running in parallel. Note that the way it finds a port available is bulletproof on every OS. It chooses a random port, tries to start the web server and retries with a different port in case of error. This is much more reliable than the usual technique that relies on:
ServerSocket serverSocket = new ServerSocket(0); int port = serverSocket.getLocalPort(); serverSocket.close();
Fluent-http recognizes html files but not only. It is also able to transform more user-friendly file formats on the fly:
.html)
.mdor
.markdown) -> Compiled transparently to .html
.xml)
.json)
.css)
.less) -> Compiled transparently to .css
.js)
.coffeeor
litcoffee) -> Compiled transparently to .js
.zip)
.gz)
.gif)
.jpegor
jpg)
.png)
All those file formats are served without additional configuration. Files are served with automatic content-type, etag and last-modified headers. And you can override them of course.
Fluent-http supports WebJars to serve static assets. Just add a maven dependency to a
WebJarand reference the static resource in your pages with the
/webjars/prefix.
Here's an example with Bootstrap:
org.webjars bootstrap 3.3.2
Hello World
Or better yet, use the
[[webjar]]handlebars helper to set the version dynamically:
[[webjar bootstrap.min.js]]
The
[[webjar]]helper also supports a list as parameter:
--- bootstrapAssets: [ bootstrap.min.css, bootstrap.min.js ] ---[[webjar bootstrapAssets]]
Static pages can use Yaml Front Matter as in Jekyll. For example this
index.mdfile:
--- greeting: Hello to: World --- [[greeting]] [[to]]
Will be rendered as:
Hello World
Take a look at
Jekyllto understand the full power of
Yaml Front Matter. It makes it very easy to build static pages without duplication.
To make Yaml Front Matter even more useful, static pages can use HandleBar template engine.
--- names: [Doc, Grumpy, Happy] --- [[#each names]] - [[.]] [[/each]]
Will be rendered as:
Doc
Grumpy
Happy
Handlebarssyntax can be used in
.htmlor
.mdfiles. You can use the built-in helpers or add your own helpers.
Note that because our stack is meant to be used with js frameworks like AngularJs, we couldn't stick with standard
{{}}handlebars notation. We use the
[[]]syntax instead, It makes it possible to mix server-side templates with client-side templates on the same page.
Like in Jekyll, pages can be decorated with a layout. The name of the layout should be configured in the
Yaml Front Mattersection.
For example, given this
app/_layouts/default.htmlfile:
[[body]]
and this
app/index.mdfile:
--- layout: default --- Hello World
A request to
/will give this result:
Hello World
A layout file can be a
.html,
.md,
.markdownor
.txtfile. It should be put in
app/_layoutsfolder. The layout name used in the
Yaml Front Mattersection can omit the layout file extension. Layouts are recursive, ie a layout file can have a layout.
A decorating layout can use variables defined in the rendered file. Here's an example with an html title:
[[title]][[body]]
and this
app/index.mdfile:
--- title: Greeting layout: default --- Hello World
A request to
/will give this result:
GreetingHello World
In addition to the variables defined in the Yaml Front Matter section, some site-wide variables are available.
app/_config.ymlfile,
site.datavariable is a map of every file in
app/_data/parsed and indexed by its file name (without extension),
site.pagesis a list of all static pages in
app/. Each entry is a map containing all variables in the file's YFM section, plus
content,
pathand
namevariables.
site.tagsis a map of every tag defined in static pages YMF sections. For each tag, there's the list of the pages with this tag. Pages can have multiple tags.
site.categoriesis a map of every category defined in static pages YMF sections. For each category, there's the list of the pages with this category. Pages can have one or zero category.
Ok, so its easy to mimic the behaviour of a static website generated with Jekyll. But what about dynamic pages? Turns it's easy too.
Let's create a
hello.mdpage with an unbound variable.
Hello [[name]]
If we query
/hello, the name will be replaced with an empty string since nowhere does it say what its value is. The solution is to override the default route to
/helloas is:
routes.get("/hello", Model.of("name", "Bob"));
Now, when the page is rendered,
[[name]]will be replaced server-side with
Bob.
If not specified, the name of the page (ie. the view) to render for a given uri is guessed after the uri. Files are looked up in this order:
uri,
uri.html,
uri.md,
uri.markdownthen
uri.txt. Most of the time it will just work, but the view can of course be overridden:
routes.get("/hello/:whom", (context, whom) -> ModelAndView.of("greeting", "name", whom));
A route can return any
Object, the server will guess the response's type:
java.lang.Stringis interpreted as inline html with content type
text/html;charset=UTF-8.
byte[]is interpreted as
application/octet-stream.
java.io.InputStreamis interpreted as
application/octet-stream.
java.io.Fileand
java.nio.file.Pathare interpreted as a static file. The content type is guessed from the file's extension.
java.net.URLis interpreted as a classpath resource. The content type is guessed from the resource's extension.
Modelis interpreted as a template which name is guessed, rendered with given variables. The content type is guessed from the file's extension.
ModelAndViewis interpreted as a template with given name, rendered with given variables. The content type is guessed from the file's extension.
voidis empty content.
Jackson, with content type
application/json;charset=UTF-8.
For a finer control over the response of a route, one can return a
Payloadobject rather than an
Object. Using a payload, one can set headers, cookies, content type and actual response.
routes.get("/hello", (context) -> Payload.ok()); routes.get("/hello", (context) -> Payload.seeOther("/anotherUri")); routes.get("/hello", (context) -> new Payload("Hello")); routes.get("/hello", (context) -> new Payload("text/html", "Hello", 200)); routes.get("/hello", (context) -> new Payload("text/html", "Hello", 200).withCookie("key", "value")); ...
Cookies can be read on the request and sent on the response.
To read the cookies, you either ask through the
Context(mainly for lambda routes).
routes.get("/hello", (context) -> Payload.cookies().value("name")); routes.get("/hello", (context) -> Payload.cookies().value("name", "default")); routes.get("/hello", (context) -> Payload.cookies().value("name", 42)); routes.get("/hello", (context) -> Payload.cookies().value("user", User.class)); routes.get("/hello", (context) -> Payload.cookies().keyValues());
Or, for annotated resources, directly get an instance of
Cookiesinjected.
public class MyResource { @Post("/uri") public void action(Cookies cookies) { String value = cookies.value("name", "Default value"); ... } }
To add a cookie to a response, use a
withCookie(...)method on the
Payload:
return new Payload(...).withCookie("key", "value"); return new Payload(...).withCookie("key", 42); return new Payload(...).withCookie("key", new Person(...)); ...
Now that the website is dynamic, we might also want to post data. We support
GET,
POST,
PUTand
DELETEmethods. Here's how one would support posting data:
routes.post("/person", context -> { String name = context.query().get("name"); int age = context.query().getInteger("age");Person person = new Person(name, age); // do something
return Payload.created(); });
It's much easier to let Jackson do the mapping between form parameters and Java Beans.
routes.post("/person", context -> { Person person = context.extract(Person.class); // do somethingreturn Payload.created(); });
Using the annotated resource syntax, it's even simpler:
public class PersonResource { @Post("/person") public void create(Person person) { repository.add(person); } }
Multiple methods can match the same uri:
public class PersonResource { @Get("/person/:id") public Model show(String id) { return Model.of("person", repository.find(id)); }@Put("/person/:id") public void update(String id, Person person) { repository.update(person); } }
Same goes for the lambda syntax:
routes .get("/person/:id", (context, id) -> Model.of("person", repository.find(id))) .put("/person/:id", (context, id) -> { Person person = context.extract(Person.class); repository.update(person); return Payload.created(); }); }
Or to avoid duplication:
routes .url("/person/:id") .get((context, id) -> Model.of("person", repository.find(id))) .put((context, id) -> { Person person = context.extract(Person.class); repository.update(person); return Payload.created(); }); }
Etag headers computation is automatic on every query. Each time a non streaming response is made by the server, etag headers are added automatically. It's performed by the default
PayloadWriterimplementation.
Each time a query is made with etag headers, the server can decide whether to return a
302 Not Modifiedcode or not.
If you want to implement a different etag strategy, you have to provide your own implementation of
PayloadWriter.
routes.setExtensions(new Extensions() { public PayloadWriter createPayloadWriter(Request request, Response response, Env env, Site site, Resources resources, CompilerFacade compilers) { return new PayloadWriter(request, response, env, site, resources, compilers) { protected String etag(byte[] data) { return Md5.of(data); }... };
}));
Starting the web server in SSL mode is very easy. You need a certificate file (
.crt) and a private key file (
.der), That's it. No need to import anything in a stupid keystore. It cannot be easier!
new WebServer().startSSL(9443, Paths.get("server.crt"), Paths.get("server.der"));
It is also possible to use a TLS certificate chain with intermediate CA certificates:
new WebServer().startSSL(9443, Arrays.asList(Paths.get("server.crt"), Paths.get("subCA.crt")), Paths.get("server.der") );
When an authentication with a client certificate is required, it is possible to specify a list of accepted trust anchor certificates:
new WebServer().startSSL(9443, Arrays.asList( Paths.get("server.crt"), Paths.get("subCA.crt")), Paths.get("server.der"), Arrays.asList(Paths.get("trustAnchor.crt")) );
Note that, as of now, we only accept
rsakey file in
PKCS#8format in a
.derbinary file.
You have probably noticed, fluent-http comes with pre-packaged, kitten-ready" 404 & 500 error pages.
If you want to customize these pages or are member of the CCC "Comité Contre les Chats", just put a
404.htmlor
500.htmlat the root of your
appfolder and they will be served instead of the defaults.
Json is supported as a first class citizen. Producing json is as easy as this:
routes.get("/product", () -> new Product(...)); routes.get("/products", () -> Arrays.asList(new Product(...), new Product(...)));
These routes serve the Products serialized as json using Jackson. The content type will be
application/json;charset=UTF-8.
When fluent-http talks json, the jackson json processor is used. Sometimes (meaning: always in any decent sized project), you want to provide your own home-cooked
ObjectMapper. You can do this by configuring or replacing the ObjectMapper through the
Extensionsinterface.
routes.setExtensions(new Extensions() { @Override public ObjectMapper configureOrReplaceObjectMapper(ObjectMapper defaultObjectMapper, Env env) { defaultObjectMapper.registerModule(new JSR310Module()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);return defaultObjectMapper;
} });
Cross-cutting behaviours can be implemented with filters. For example, one can log every request to the server with this filter:
routes.filter((uri, context, next) -> { System.out.println(uri); return next.get(); })
A filter can be defined in its own class:
routes.filter(LogRequestFilter.class);public class LogRequestFilter implements Filter { @Override public Payload apply(String uri, Context context, PayloadSupplier next) throws Exception { System.out.println(uri); return next.get(); } }
A filter can either pass to the next filter/route by returning
next.get()or it can totally bypass the chain of filters/routes by returning its own Payload. For example, a Basic Authentication filter would look like:
public class BasicAuthFilter implements Filter { private final String uriPrefix; private final String realm; private final List hashes;public BasicAuthFilter(String uriPrefix, String realm, Map users) { this.uriPrefix = uriPrefix; this.realm = realm; this.hashes = new ArrayList<>();
users.forEach((String user, String password) -> { String hash = Base64.getEncoder().encodeToString((user + ":" + password).getBytes()); hashes.add("Basic " + hash); });
}
@Override public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws Exception { if (!uri.startsWith(uriPrefix)) { return nextFilter.get(); // Ignore }
String authorizationHeader = context.getHeader("Authorization"); if ((authorizationHeader == null) || !hashes.contains(authorizationHeader.trim())) { return Payload.unauthorized(realm); } return nextFilter.get();
} }
Both
BasicAuthFilterand
LogRequestFilterare pre-packaged filters that you can use in your applications.
Out of the box support for Guice: just throw in your Guice dependency in your pom, and you're ready to roll.
Let's say you got some Guice Module like this.
public class ServiceModule extends AbstractModule { @Override protected void configure() { bind(MongoDB.class);bind(AllProducts.class); bind(AllOrders.class); bind(AllEmails.class);
} }
Wiring them in can be done in your WebConfiguration like this
public void configure(Routes routes) { routes.setIocAdapter(new GuiceAdapter(new ServiceModule())); routes.get("/foobar", "FOO BAR FTW
"); }
Now you can inject your beans like you would expect
public class AllProducts { private final MongoCollection products;@Inject public AllProducts(MongoDB mongodb) { products = mongodb.getJongo().getCollection("product"); }
We support Spring injected bean in exactly the same manner as with guice. Check the SpringAdapter class, which works the same way as its Guice counterpart.
Look at the SpringAdapterTest we wrote for a working example.
You'll probably sooner than later want to add to some custom HandleBars helpers for your server-side templates.
You first start to write you own helper in Java. ```Java import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; .../...
public enum HandleBarHelper implements Helper { appStoreClassSuffix { @Override public CharSequence apply(Object context, Options options) throws IOException { if (((String) context).contains("google")) return "play"; else return "ios"; } }, .../... } ```
You wire it in by adding your helper class to the
CompilersConfigurationby the way of the
Extensionsinterface.
routes .setExtensions(new Extensions() { @Override public void configureCompilers(CompilersConfiguration compilers, Env env) { compilers.addHandlebarsHelpers(HandleBarHelper.class); compilers.addHandlebarsHelpers(AnotherHelper.class); } })
You are able to use your own helper in any of your template like this example for the code above.
fluent-httpcomes with some build-in handlebars helpers, to make your life easier:
livereloadwhich provides the livereload client-side js script.
[[livereload]]is already injected in the
defaultlayout.
GoogelAnalyticswhich provides client-side injection of the google analytics script. Use it like that:
[[google_analytics 'UA-XXXXXX-X']]
StringHelperwhich provides
[[capitalizeFirst]],
[[lower]],
[[stringFormat ]]check the documentation
css,
script
fluent-http uses a disk cache to store
.cssand
.jsfiles produced from
.lessor .
coffeefiles. This directory is by default stored in your "user home" in a
.code-storydirectory.
If you don't want it to be here you can
-Duser.home=/foo/baras you see fit. If you're paranoid and run fluent-http under
nobodyuser make sure
nobodycan read/write this directory.
SimpleHttpServer: ThreadConfigurationSample.java
mvn license:format
mvn clean verify
Build the release:
mvn release:clean release:prepare release:perform