This is the simplest fastest full fledged http server we could come up with.
Prerequisites
- Java 1.8
- Apache Maven 3
Build the project
mvn verify
mvn license:format
One of our goals was to make it as easy as possible to start with.
Release versions are deployed on Maven Central:
<dependency>
<groupId>net.code-story</groupId>
<artifactId>http</artifactId>
<version>1.27</version>
</dependency>
Starting a web server that responds Hello World
on /
uri is as simple as that:
import net.codestory.http.*;
public class HelloWorld {
public static void main(String[] args) {
new WebServer(routes -> routes.get("/", "Hello World")).start(8080);
}
}
Adding more routes is not hard either:
new WebServer(routes -> routes.
get("/", "Hello World").
get("/Test", "Test").
get("/OtherTest", "Other Test")
).start(8080);
Routes can have path parameters:
routes.get("/hello/:who", (context, name) -> "Hello " + name);
routes.get("/add/:first/to/:second", (context, first, second) -> Integer.parseInt(first) + Integer.parseInt(second));
Notice that path parameters have to be of type String
.
The notation with lambdas is very compact but cannot support path parameters of type other than String
. So we've added
the notion of resource, in a way similar to jaxb.
routes.add(new CalculationResource());
public class CalculationResource {
@Get("/add/:first/to/:second")
public int add(int first, int second) {
return first + second;
}
}
Each method annotated with @Get
is a route. The method can have any name. The number of 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.
routes.add(CalculationResource.class);
When a web server is started. It automatically treats files found in app
folder as static pages. The app
folder
is searched first on the classpath and then in the working directory.
So the simplest way to start a web server is in fact:
import net.codestory.http.*;
public class HelloWorld {
public static void main(String[] args) {
new WebServer().start(8080);
}
}
Instead of providing a fixed port, 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. Not 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();
The web server recognizes html files but not only. It is also able to transform more user-friendly file formats on the fly:
- Html (
.html
) - Markdown (
.md
or.markdown
) -> Html - Asciidoc (
.asciidoc
) -> Html - Xml (
.xml
) - Css (
.css
) - Less (
.less
) -> Css - Javascript (
.js
) - Coffeescript (
.coffee
orlitcoffee
) -> Javascript - Zip (
.zip
) - Gz (
.gz
) - Pdf (
.pdf
) - Gif (
.gif
) - Jpeg (
.jpeg
orjpg
) - Png (
.png
) - All other files are treated as plain text.
Static pages can use Yaml Front Matter as in Jekyll. For example this index.md
file:
---
greeting: Hello
to: World
---
[[greeting]] [[to]]
Will be rendered as:
<p>Hello World</p>
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:
<ul>
<li><p>Doc</p>
</li>
<li><p>Grumpy</p>
</li>
<li><p>Happy</p>
</li>
</ul>
Like in Jekyll, pages can be decorated with a layout. The name of the layout should be configured in the Yaml Front Matter section.
For example, given this app/_layouts/default.html
file:
<!DOCTYPE html>
<html lang="en">
<body>
[[body]]
</body>
</html>
and this app/index.md
file:
Hello World
A request to /
will give this result:
<!DOCTYPE html>
<html lang="en">
<body>
<p>Hello World</p>
</body>
</html>
A layout file can be a .html
, .md
, .markdown
, .txt
or .asciidoc
file. It should be put in app/_layouts
folder.
The layout name used in the Yaml Front Matter section can omit the layout file extension.
Layouts are recursive, ie a layout file can have a layout.
A layout can use variables defined in the rendered file. Here's an example with an html title:
<!DOCTYPE html>
<html lang="en">
<head>
<title>[[title]]</title>
</head>
<body>
[[body]]
</body>
</html>
and this app/index.md
file:
---
title: Greeting
---
Hello World
A request to /
will give this result:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Greeting</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
In addition to the variables defined in the Yaml Front Matter section, some site-wide variables are available.
- All variables defined in the
app/_config.yml
file site.data
is a map of every file inapp/_data/
parsed and indexed by its file name (without extension)site.pages
is a list of all static pages inapp/
. Each entry is a map containing all variables in the file's YFM section, pluscontent
,path
andname
variables.site.tags
is 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.categories
is 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.
We also support WebJars to server static assets.
Just add a maven dependency to a WebJar and reference the static resource in your pages with the /webjars/
suffix.
Here's an example with Bootstrap:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.0.3</version>
</dependency>
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/webjars/bootstrap/3.0.3/css/bootstrap.min.css">
</head>
<body>
<p>Hello World</p>
</body>
</html>
Ok, so its easy to mimic the behavior of a static website generated with Jekyll. But what about dynamic pages. Turns out it's heady too.
Let's create a hello.md
page 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 /hello
as is:
routes.get("/hello", Model.of("name", "Bob");
Now, when the pages 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.markdown
, uri.txt
then uri.asciidoc
. 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 try to guess what to do with it:
java.lang.String
is interpreted as inline html with content typetext/html;charset=UTF-8
.byte[]
is interpreted asapplication/octet-stream
.java.io.InputStream
is interpreted asapplication/octet-stream
.java.io.File
is interpreted as a static file. The content type is guessed from file's extension.java.nio.file.Path
is interpreted as a static file. The content type is guessed from file's extension.Model
is interpreted as a template which name is guessed, rendered with given variables. The content type is guessed from file's extension.ModelAndView
is interpreted as a template with given name, rendered with given variables. The content type is guessed from file's extension.void
is empty content.- any other type is serialized to json with content type
application/json;charset=UTF-8
.
Now that the website is dynamic, we might also want to post data. We support GET
, POST
, PUT
and DELETE
methods.
Here's how one would post data.
routes.post("/person", (context) -> {
String name = context.get("name");
int age = context.getInteger("age");
Person person = new Person(name, age);
// do something
return Payload.created();
});
It's even easier to let Jackson do the mapping between form parameters and Java Beans.
routes.post("/person", (context) -> {
Person person = context.payload(Person.class);
// do something
return 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 be used for 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.payload(Person.class);
repository.update(person);
return Payload.created();
});
}
Or to avoid duplication:
routes
.with("/person/:id").
get((context, id) -> Model.of("person", repository.find(id))).
put((context, id) -> {
Person person = context.payload(Person.class);
repository.update(person);
return Payload.created();
});
}
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"));
TODO
TODO
TODO
TODO
Cross-cutting behaviors 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 IOException {
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<String> hashes;
public BasicAuthFilter(String uriPrefix, String realm, Map<String, String> users) {
this.uriPrefix = uriPrefix;
this.realm = realm;
this.hashes = new ArrayList<>();
users.entrySet().forEach((entry) -> {
String user = entry.getKey();
String password = entry.getValue();
String hash = Base64.getEncoder().encodeToString((user + ":" + password).getBytes());
hashes.add("Basic " + hash);
});
}
@Override
public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws IOException {
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 BasicAuthFilter
and LogRequestFilter
are pre-packaged filters that you can use in your applications.
TODO
TODO
TODO (Formulas, Tables, ...)
TODO
TODO
Build the release:
mvn release:clean
mvn release:prepare
mvn release:perform
Go to https://oss.sonatype.org/, log in, go to Staging Repositories, close the netcode-story-XXXX repository then release it. Synchro to Maven Central is done hourly.
- Javadoc
- PROD_MODE is not really convenient. It forces to have more complex prod script
- Add some sort of http standard log
- Cleanup Payload class. Make Payload immutable?
- Cors support
- monitoring
- Optional Payload?
- If-Modified-since activé par défaut
- Laisser le Compiler Cache donner le last modified
- Gere les last modified dans Payload
- Auto reload meme sans les lambda avec capture de variables locales
- Streaming
- File upload
- OPTIONS method
- Singletons qui utilise les annotations standards
- Remplacer Simple par un Servlet Filter qui fonctionne par defaut sur un Jetty Http
- Help use local storage
- Add your own/reuse Servlet filters
- Supporter les coffee et less pré-générés
- Supporter les templates pré-générés
- Fonctionnement en mode war
- TRACE method
- CONNECT method
- nio