Skip to content

Commit

Permalink
Add a server adapter for undertow.io
Browse files Browse the repository at this point in the history
Simple remains the default adapter for the web server for the time being, but we might make undertow the default in the future since my own benchmarks show that undertow gets 3 times the throughput.

See #53
  • Loading branch information
testinfected committed Nov 2, 2016
1 parent 595b3d6 commit a4b7605
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 2 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ targetCompatibility = 1.8

def libs = [
simple : 'org.simpleframework:simple-http:6.0.1',
undertow : 'io.undertow:undertow-core:1.4.4.Final',
jmustache : 'com.samskivert:jmustache:1.9',
hamcrest : ['org.hamcrest:java-hamcrest:2.0.0.0',
'org.hamcrest:hamcrest-junit:2.0.0.0'],
Expand All @@ -42,6 +43,7 @@ repositories {

dependencies {
compile libs.simple, optional
compile libs.undertow, optional
compile libs.jmustache, optional

testCompile libs.hamcrest
Expand Down
4 changes: 4 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ artifacts:
simple_common: org.simpleframework:simple-common:jar:6.0.1
simple_transport: org.simpleframework:simple-transport:jar:6.0.1
simple_http: org.simpleframework:simple-http:jar:6.0.1
undertow: io.undertow:undertow-core:jar:1.4.4.Final
xnio_api: org.jboss.xnio:xnio-api:jar:3.3.6.Final
xnio: org.jboss.xnio:xnio-nio:jar:3.3.6.Final
jboss_logging: org.jboss.logging:jboss-logging:jar:3.2.1.Final
mustache: com.samskivert:jmustache:jar:1.9

# -- test dependencies
Expand Down
5 changes: 3 additions & 2 deletions buildfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ Release.commit_message = lambda { |version| "Bump version number to #{version}"
Release.tag_name = lambda { |version| "v#{version}" }

SIMPLE = [:simple_common, :simple_transport, :simple_http]
UNDERTOW = [:jboss_logging, :xnio_api, :xnio, :undertow]

define 'molecule', :group => 'com.vtence.molecule', :version => VERSION_NUMBER do
compile.options.source = '1.8'
compile.options.target = '1.8'

compile.with SIMPLE, :mustache
compile.with SIMPLE, UNDERTOW, :mustache
test.with :hamcrest, :hamcrest_junit, :jmock, :juniversalchardet

package :jar
Expand All @@ -27,5 +28,5 @@ define 'molecule', :group => 'com.vtence.molecule', :version => VERSION_NUMBER d
pom.add_github_project('testinfected/molecule')
pom.scm_developer_connection = 'scm:hg:git+ssh://git@github.com:testinfected/molecule.git'
pom.add_developer('testinfected', 'Vincent Tence', 'vtence@gmail.com', ['Developer'])
pom.optional_dependencies.concat [SIMPLE, :mustache].flatten
pom.optional_dependencies.concat [SIMPLE, UNDERTOW, :mustache].flatten
end
274 changes: 274 additions & 0 deletions src/main/java/com/vtence/molecule/servers/UndertowServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package com.vtence.molecule.servers;

import com.vtence.molecule.Application;
import com.vtence.molecule.Body;
import com.vtence.molecule.BodyPart;
import com.vtence.molecule.FailureReporter;
import com.vtence.molecule.Request;
import com.vtence.molecule.Response;
import com.vtence.molecule.Server;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormDataParser;
import io.undertow.server.handlers.form.FormEncodedDataDefinition;
import io.undertow.server.handlers.form.MultiPartParserDefinition;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.HttpString;
import org.xnio.IoUtils;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;

import static io.undertow.util.QueryParameterUtils.getQueryParamEncoding;
import static io.undertow.util.QueryParameterUtils.parseQueryString;

public class UndertowServer implements Server {

private final String host;
private final int port;

private Undertow server;
private FailureReporter failureReporter = FailureReporter.IGNORE;

public UndertowServer(String host, int port) {
this.host = host;
this.port = port;
}

public void reportErrorsTo(FailureReporter reporter) {
this.failureReporter = reporter;
}

public int port() {
return port;
}

public String host() {
return host;
}

public void run(final Application app) throws IOException {
start(Undertow.builder().addHttpListener(port, host), app);
}

public void run(final Application app, SSLContext context) throws IOException {
start(Undertow.builder().addHttpsListener(port, host, context), app);
}

private void start(Undertow.Builder builder, Application app) {
server = builder.setHandler(new DispatchHandler(new ApplicationHandler(app)))
.build();
server.start();
}

public void shutdown() throws IOException {
if (server != null) server.stop();
}

private static class DispatchHandler implements HttpHandler {
private final ApplicationHandler handler;

public DispatchHandler(ApplicationHandler handler) {
this.handler = handler;
}

public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.startBlocking();
exchange.dispatch(() -> handler.handleRequest(exchange));
}
}

private class ApplicationHandler implements HttpHandler {
private final Application app;

public ApplicationHandler(Application app) {
this.app = app;
}

public void handleRequest(HttpServerExchange exchange) {
final List<Closeable> resources = new ArrayList<>();
final Request request = new Request();
final Response response = new Response();
try {
read(request, exchange, resources);
app.handle(request, response);
response.whenSuccessful(commitTo(exchange))
.whenFailed((result, error) -> failureReporter.errorOccurred(error))
.whenComplete((result, error) -> closeAll(resources, exchange));
} catch (Throwable failure) {
failureReporter.errorOccurred(failure);
closeAll(resources, exchange);
}
}

private void read(Request request, HttpServerExchange exchange, List<Closeable> resources) throws IOException {
setRequestInfo(request, exchange);
setHeaders(request, exchange);
setQueryParameters(request, exchange);
setFormParameters(request, exchange);
setParts(request, exchange, resources);
setBody(request, exchange, resources);
}

private void setRequestInfo(Request request, HttpServerExchange exchange) {
request.uri(exchange.getRequestURI() + queryComponent(exchange));
request.path(exchange.getRequestPath());
request.remoteIp(exchange.getSourceAddress().getAddress().getHostAddress());
request.remotePort(exchange.getSourceAddress().getPort());
request.remoteHost(exchange.getSourceAddress().getHostName());
request.timestamp(exchange.getRequestStartTime());
request.protocol(exchange.getProtocol().toString());
request.secure(exchange.getRequestScheme().equalsIgnoreCase("https"));
request.method(exchange.getRequestMethod().toString());
}

private String queryComponent(HttpServerExchange exchange) {
return exchange.getQueryString() != null ? "?" + exchange.getQueryString() : "";
}

private void setHeaders(Request request, HttpServerExchange exchange) {
HeaderMap headers = exchange.getRequestHeaders();
for (HeaderValues values : headers) {
for (String value : values) {
request.addHeader(values.getHeaderName().toString(), value);
}
}
}

private void setQueryParameters(Request request, HttpServerExchange exchange) {
Map<String, Deque<String>> parameters =
parseQueryString(exchange.getQueryString(), getQueryParamEncoding(exchange));

for (String name : parameters.keySet()) {
for (String value : parameters.get(name)) {
request.addParameter(name, value);
}
}
}

private void setFormParameters(Request request, HttpServerExchange exchange) throws IOException {
FormEncodedDataDefinition form = new FormEncodedDataDefinition();
form.setDefaultEncoding(StandardCharsets.UTF_8.name());
FormDataParser parser = form.create(exchange);
if (parser == null) return;

FormData data = parser.parseBlocking();
for (String name : data) {
for (FormData.FormValue param : data.get(name)) {
request.addParameter(name, param.getValue());
}
}
parser.close();
}

private void setParts(Request request, HttpServerExchange exchange, List<Closeable> resources)
throws IOException {
MultiPartParserDefinition multipart = new MultiPartParserDefinition();
multipart.setDefaultEncoding(StandardCharsets.UTF_8.name());
FormDataParser parser = multipart.create(exchange);
if (parser == null) return;

FormData data = parser.parseBlocking();
for (String name : data) {
for (FormData.FormValue param : data.get(name)) {
BodyPart part = new BodyPart().filename(param.getFileName())
.contentType(contentTypeOf(param))
.name(name);
if (param.isFile()) {
final FileInputStream content = new FileInputStream(param.getPath().toFile());
resources.add(content);
part.content(content);
} else {
part.content(param.getValue());
}
request.addPart(part);
}
}
parser.close();
}

private String contentTypeOf(FormData.FormValue param) {
HeaderValues contentType = param.getHeaders().get("Content-Type");
return contentType != null ? contentType.getFirst() : null;
}

private void setBody(Request request, HttpServerExchange exchange, List<Closeable> resources)
throws IOException {
final InputStream body = exchange.getInputStream();
resources.add(body);
request.body(body);
}

private Consumer<Response> commitTo(HttpServerExchange exchange) {
return response -> {
try {
commit(exchange, response);
} catch (IOException e) {
throw new CompletionException(e);
}
};
}

private void commit(HttpServerExchange exchange, Response response) throws IOException {
setStatusLine(exchange, response);
setHeaders(exchange, response);
writeBody(exchange, response);
}

private void setStatusLine(HttpServerExchange exchange, Response response) {
exchange.setStatusCode(response.statusCode());
exchange.setReasonPhrase(response.statusText());
}

private void setHeaders(HttpServerExchange exchange, Response response) {
for (String name : response.headerNames()) {
for (String value : response.headers(name)) {
exchange.getResponseHeaders().add(HttpString.tryFromString(name), value);
}
}
}

private void writeBody(HttpServerExchange exchange, Response response) throws IOException {
Body body = response.body();
body.writeTo(exchange.getOutputStream(), response.charset());
body.close();
}

private void closeAll(List<Closeable> resources, HttpServerExchange exchange) {
for (Closeable resource : resources) {
close(resource);
}
end(exchange);
}

private void close(Closeable resource) {
try {
resource.close();
} catch (IOException e) {
failureReporter.errorOccurred(e);
}
}

private void end(HttpServerExchange exchange) {
try {
exchange.endExchange();
} catch (Throwable e) {
failureReporter.errorOccurred(e);
IoUtils.safeClose(exchange.getConnection());
}
}
}
}
33 changes: 33 additions & 0 deletions src/test/java/com/vtence/molecule/servers/UndertowServerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.vtence.molecule.servers;

import com.vtence.molecule.Server;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.util.logging.LogManager;

import static com.vtence.molecule.testing.http.HttpResponseAssert.assertThat;

public class UndertowServerTest extends ServerCompatibilityTests {

@BeforeClass public static void
silenceLogging() {
LogManager.getLogManager().reset();
}

protected Server createServer(int port) {
return new UndertowServer("localhost", port);
}

@Test public void
setsContentLengthAutomaticallyForSmallResponses() throws IOException {
server.run((request, response) -> response.body("<html>...</html>").done());

response = request.send();
assertNoError();
assertThat(response).hasBodyText("<html>...</html>")
.hasHeader("Content-Length", "16")
.isNotChunked();
}
}

0 comments on commit a4b7605

Please sign in to comment.