Skip to content

Commit

Permalink
Big refactor, all tests passing
Browse files Browse the repository at this point in the history
  • Loading branch information
rferreira committed Mar 15, 2020
1 parent c5abbbf commit 260efe0
Show file tree
Hide file tree
Showing 33 changed files with 380 additions and 254 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.openjdk.jmh.annotations.*;
import org.ophion.jujube.Jujube;
import org.ophion.jujube.config.JujubeConfig;
import org.ophion.jujube.response.JujubeHttpResponse;
import org.ophion.jujube.response.JujubeResponse;
import org.ophion.jujube.util.DataSize;

import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -78,7 +78,7 @@ public void setup() throws KeyStoreException, NoSuchAlgorithmException, IOExcept
.build();

var config = new JujubeConfig();
config.route("/*", ctx -> new JujubeHttpResponse(200));
config.route("/*", (req, ctx) -> new JujubeResponse(200));
server = new Jujube(config);

this.endpoint = URIBuilder.localhost().setPort(config.getServerConfig().getListenPort()).setScheme("https").build();
Expand Down
7 changes: 5 additions & 2 deletions jujube-core/src/main/java/org/ophion/jujube/Jujube.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void start() {
final var banner = Files.readString(Paths.get(Objects.requireNonNull(Jujube.class.getClassLoader().getResource("banner.txt")).toURI()));
System.out.println(banner);

var bootstrap = H2ServerBootstrap.bootstrap()
final var bootstrap = H2ServerBootstrap.bootstrap()
.setH2Config(config.getServerConfig().getH2Config())
.setHttp1Config(config.getServerConfig().getHttp1Config())
.setTlsStrategy(config.getServerConfig().getTlsStrategy())
Expand All @@ -66,7 +66,10 @@ public void start() {
}

config.routes()
.forEach((k, v) -> bootstrap.register(k, () -> new JujubeServerExchangeHandler(config, v)));
.forEach((path, handler) -> {
LOG.info("adding route: {} -> {}", path, handler);
bootstrap.register(path, () -> new JujubeServerExchangeHandler(config, handler));
});

this.instance = bootstrap.create();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import java.util.concurrent.ForkJoinPool;

public class JujubeConfig {
private final ServerConfig serverConfig = new ServerConfig();
private ExecutorService executorService = ForkJoinPool.commonPool();
private Map<String, RouteHandler> routes = new LinkedHashMap<>();

private final ServerConfig serverConfig;
private final Map<String, RouteHandler> routes;
private ExecutorService executorService;
public JujubeConfig() {
this.routes = new LinkedHashMap<>();
this.executorService = ForkJoinPool.commonPool();
this.serverConfig = new ServerConfig();
}

public ExecutorService getExecutorService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
* Configures the underlying Apache HttpCore server.
*/
public class ServerConfig {
private IOReactorConfig ioReactorConfig;
private H2Config h2Config = H2Config.DEFAULT;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.message.BasicHeader;
import org.ophion.jujube.context.JujubeHttpContext;
import org.ophion.jujube.request.JujubeRequest;
import org.ophion.jujube.response.JujubeHttpException;
import org.ophion.jujube.response.JujubeHttpResponse;
import org.ophion.jujube.response.JujubeResponse;

import java.util.function.Supplier;

public class HttpConstraints {
public static void onlyAllowMethod(Method method, JujubeHttpContext ctx) {
onlyAllowMethod(method, ctx, () -> {
var resp = new JujubeHttpResponse(HttpStatus.SC_METHOD_NOT_ALLOWED);
public static void onlyAllowMethod(Method method, JujubeRequest req) {
onlyAllowMethod(method, req, () -> {
var resp = new JujubeResponse(HttpStatus.SC_METHOD_NOT_ALLOWED);
resp.setHeader(new BasicHeader(HttpHeaders.ALLOW, method.toString()));
throw new JujubeHttpException(resp);
});
}

public static void onlyAllowMethod(Method method, JujubeHttpContext ctx, Supplier<RuntimeException> exceptionSupplier) {
if (method.isSame(ctx.getRequest().getMethod())) {
public static void onlyAllowMethod(Method method, JujubeRequest req, Supplier<RuntimeException> exceptionSupplier) {
if (method.isSame(req.getMethod())) {
return;
}
throw exceptionSupplier.get();
}

public static void onlyAllowMediaType(ContentType contentType, JujubeHttpContext ctx) {
ctx.getEntityContentType().ifPresent(ct -> {
if (!ct.isSameMimeType(contentType)) {
public static void onlyAllowMediaType(ContentType contentType, JujubeRequest req) {
req.getHttpEntity().ifPresent(entity -> {
if (!ContentType.parseLenient(entity.getContentType()).isSameMimeType(contentType)) {
throw new JujubeHttpException(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,97 @@
package org.ophion.jujube.internal;

import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
import org.apache.hc.core5.http.nio.AsyncRequestConsumer;
import org.apache.hc.core5.http.nio.AsyncServerRequestHandler;
import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers;
import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler;
import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URLEncodedUtils;
import org.ophion.jujube.config.JujubeConfig;
import org.ophion.jujube.context.FileParameter;
import org.ophion.jujube.context.JujubeHttpContext;
import org.ophion.jujube.context.ParameterSource;
import org.ophion.jujube.context.PrimitiveParameter;
import org.ophion.jujube.http.MultipartEntity;
import org.ophion.jujube.internal.consumers.ContentAwareRequestConsumer;
import org.ophion.jujube.internal.consumers.RequestEntityLimitExceeded;
import org.ophion.jujube.internal.parameters.ParameterExtractor;
import org.ophion.jujube.internal.util.Loggers;
import org.ophion.jujube.response.HttpResponseRequestTooLarge;
import org.ophion.jujube.response.HttpResponseServerError;
import org.ophion.jujube.request.JujubeRequest;
import org.ophion.jujube.request.SessionStore;
import org.ophion.jujube.response.JujubeHttpException;
import org.ophion.jujube.response.JujubeHttpResponse;
import org.ophion.jujube.response.JujubeResponse;
import org.ophion.jujube.response.ResponseRequestTooLarge;
import org.ophion.jujube.response.ResponseServerError;
import org.ophion.jujube.route.RouteHandler;
import org.slf4j.Logger;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;

/**
* Core exchanger (exchanges a request for a response) and dispatch logic.
*/
public class JujubeServerExchangeHandler extends AbstractServerExchangeHandler<Message<HttpRequest, HttpEntity>> {
private static final Logger LOG = Loggers.build();
private final JujubeConfig config;
private final RouteHandler handler;
private final ParameterExtractor parameterExtractor;
private final ExecutorService executor;
private AtomicReference<Exception> exceptionRef;

public JujubeServerExchangeHandler(JujubeConfig config, RouteHandler handler) {
this.config = config;
this.handler = handler;
this.exceptionRef = new AtomicReference<>();
this.parameterExtractor = new ParameterExtractor();
this.executor = config.getExecutorService();
}

@Override
protected AsyncRequestConsumer<Message<HttpRequest, HttpEntity>> supplyConsumer(HttpRequest request, EntityDetails entityDetails, HttpContext context) throws HttpException {
protected AsyncRequestConsumer<Message<HttpRequest, HttpEntity>> supplyConsumer(HttpRequest request, EntityDetails entityDetails, HttpContext context) {
LOG.debug("handling request: {}", request.toString());

return new ContentAwareRequestConsumer(config, entityDetails, exceptionRef);
}

@Override
protected void handle(Message<HttpRequest, HttpEntity> requestMessage, AsyncServerRequestHandler.ResponseTrigger responseTrigger, HttpContext context) throws HttpException, IOException {
protected void handle(Message<HttpRequest, HttpEntity> requestMessage, AsyncServerRequestHandler.ResponseTrigger responseTrigger, HttpContext context) throws HttpException {
try {
JujubeHttpResponse response = null;
final var executor = config.getExecutorService();
final var ctx = new JujubeHttpContext(this.config, context);

JujubeResponse response = null;

// if we errored before getting here, quickly exit:
if (exceptionRef.get() != null) {
var ex = exceptionRef.get();
if (ex instanceof RequestEntityLimitExceeded) {
response = new HttpResponseRequestTooLarge();
response = new ResponseRequestTooLarge();
} else {
response = new HttpResponseServerError();
response = new ResponseServerError();
}
}

// dispatching handler if we do not already have an exception/response
if (response == null) {
try {
response = executor.submit(() -> {
extractParameters(ctx, requestMessage.getHead(), requestMessage.getBody());
return handler.handle(ctx);

response = this.executor.submit(() -> {
// hydrating request:
var entity = requestMessage.getBody();
var parentRequest = requestMessage.getHead();
var params = parameterExtractor.extract(parentRequest, entity);
var req = new JujubeRequest(parentRequest, entity, params, new SessionStore() {
});

// dispatching
return handler.handle(req, context);

// blocking and awaiting response:
}).get();

} catch (ExecutionException e) {
if (e.getCause() instanceof JujubeHttpException) {
response = (JujubeHttpResponse) ((JujubeHttpException) e.getCause()).toHttpResponse();
response = (JujubeResponse) ((JujubeHttpException) e.getCause()).toHttpResponse();
} else {
e.printStackTrace();
response = new HttpResponseServerError();
response = new ResponseServerError();
}
}
}
Expand Down Expand Up @@ -112,39 +121,4 @@ protected void handle(Message<HttpRequest, HttpEntity> requestMessage, AsyncServ
throw new HttpException("error handling request", e);
}
}

private void extractParameters(JujubeHttpContext context, HttpRequest req, HttpEntity entity) {
try {
URLEncodedUtils.parse(req.getUri(), StandardCharsets.UTF_8)
.forEach(nvp -> context.setParameter(ParameterSource.QUERY, new PrimitiveParameter(nvp.getName(), nvp.getValue(), ContentType.TEXT_PLAIN)));
} catch (URISyntaxException e) {
LOG.error("error decoding query string", e);
}

try {
if (entity != null) {
context.setEntity(entity);
final var contentType = context.getEntityContentType().orElseThrow(() -> new IllegalStateException("unable to identify content type for entity"));

if (ContentType.APPLICATION_FORM_URLENCODED.isSameMimeType(contentType)) {
// TODO: need to find a better way to handle this going forward since this double memory usage
EntityUtils.parse(entity)
.forEach(nvp -> context.setParameter(ParameterSource.FORM, new PrimitiveParameter(nvp.getName(), nvp.getValue(), ContentType.TEXT_PLAIN)));
}

if (ContentType.MULTIPART_FORM_DATA.isSameMimeType(contentType)) {
var parts = ((MultipartEntity) entity).getParts();
parts.forEach(p -> {
if (p.isText()) {
context.setParameter(ParameterSource.FORM, new PrimitiveParameter(p.getName(), p.getValue(), p.getContentType()));
} else {
context.setParameter(ParameterSource.FORM, new FileParameter(p.getName(), Paths.get(p.getValue()), p.getContentType(), p.getFilename()));
}
});
}
}
} catch (IOException e) {
LOG.error("error decoding form body", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer;
import org.ophion.jujube.internal.util.Loggers;
import org.ophion.jujube.internal.util.TieredOutputStream;
import org.ophion.jujube.util.DataSize;
import org.slf4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

class SizeAwareEntityConsumer extends AbstractBinAsyncEntityConsumer<HttpEntity> {
Expand All @@ -29,8 +30,28 @@ protected void streamStart(ContentType contentType) throws HttpException, IOExce
}

@Override
protected HttpEntity generateContent() throws IOException {
return new InputStreamEntity(buffer.getContentAsStream(), contentType);
protected HttpEntity generateContent() {
return new AbstractHttpEntity(contentType, null) {
@Override
public InputStream getContent() throws IOException, UnsupportedOperationException {
return buffer.getContentAsStream();
}

@Override
public boolean isStreaming() {
return false;
}

@Override
public void close() throws IOException {
buffer.close();
}

@Override
public long getContentLength() {
return buffer.getSize();
}
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ private void processBuffer(ByteBuffer contents) throws IOException {
// resetting accumulator
currentPartBodyAccumulator = null;

// now we need to assess if we're reach the end body or final delimiter, we do that by looking at the next 2 bytes:
// now we need to assess if we've reached the end body or final delimiter,
// we do that by looking at the next 2 bytes:
var currentSegmentEndIndex = contents.position() + currentDelimiter.length - 1;
var hasReachedFinalDelimiter = Arrays.equals(
contents.array(), currentSegmentEndIndex, currentSegmentEndIndex + TWO_DASHES_CRLF.length - 1,
Expand Down
Loading

0 comments on commit 260efe0

Please sign in to comment.