Skip to content

Commit

Permalink
WIP: Support compression for reactive routes and resteasy reactive
Browse files Browse the repository at this point in the history
  • Loading branch information
mkouba committed Mar 28, 2022
1 parent f919dcd commit 171a9ba
Show file tree
Hide file tree
Showing 24 changed files with 633 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@

import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.vertx.http.runtime.HttpCompression;

public final class AnnotatedRouteHandlerBuildItem extends MultiBuildItem {

private final BeanInfo bean;
private final List<AnnotationInstance> routes;
private final AnnotationInstance routeBase;
private final MethodInfo method;
private final boolean blocking;
private final HttpCompression compression;

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase) {
this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED);
}

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase, boolean blocking, HttpCompression compression) {
super();
this.bean = bean;
this.method = method;
this.routes = routes;
this.routeBase = routeBase;
this.method = method;
this.blocking = blocking;
this.compression = compression;
}

public BeanInfo getBean() {
Expand All @@ -39,4 +50,12 @@ public AnnotationInstance getRouteBase() {
return routeBase;
}

public boolean isBlocking() {
return blocking;
}

public HttpCompression getCompression() {
return compression;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import org.jboss.jandex.DotName;

import io.quarkus.vertx.http.Compressed;
import io.quarkus.vertx.http.Uncompressed;
import io.quarkus.vertx.web.Body;
import io.quarkus.vertx.web.Header;
import io.quarkus.vertx.web.Param;
Expand Down Expand Up @@ -50,5 +52,7 @@ final class DotNames {
static final DotName THROWABLE = DotName.createSimple(Throwable.class.getName());
static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName());
static final DotName COMPLETION_STAGE = DotName.createSimple(CompletionStage.class.getName());
static final DotName COMPRESSED = DotName.createSimple(Compressed.class.getName());
static final DotName UNCOMPRESSED = DotName.createSimple(Uncompressed.class.getName());

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -101,6 +102,7 @@
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem;
import io.quarkus.vertx.http.runtime.HandlerType;
import io.quarkus.vertx.http.runtime.HttpCompression;
import io.quarkus.vertx.web.Param;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.Route.HttpMethod;
Expand Down Expand Up @@ -145,12 +147,12 @@ FeatureBuildItem feature() {
@BuildStep
void unremovableBeans(BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {
unremovableBeans
.produce(UnremovableBeanBuildItem.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE));
.produce(UnremovableBeanBuildItem.beanClassAnnotation(DotNames.ROUTE));
unremovableBeans
.produce(UnremovableBeanBuildItem.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTES));
.produce(UnremovableBeanBuildItem.beanClassAnnotation(DotNames.ROUTES));
unremovableBeans
.produce(UnremovableBeanBuildItem
.beanClassAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER));
.beanClassAnnotation(DotNames.ROUTE_FILTER));
}

@BuildStep
Expand All @@ -168,33 +170,54 @@ void validateBeanDeployment(
// NOTE: inherited business methods are not taken into account
ClassInfo beanClass = bean.getTarget().get().asClass();
AnnotationInstance routeBaseAnnotation = beanClass
.classAnnotation(io.quarkus.vertx.web.deployment.DotNames.ROUTE_BASE);
.classAnnotation(DotNames.ROUTE_BASE);
for (MethodInfo method : beanClass.methods()) {
if (method.isSynthetic() || Modifier.isStatic(method.flags()) || method.name().equals("<init>")) {
continue;
}

List<AnnotationInstance> routes = new LinkedList<>();
AnnotationInstance routeAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTE);
DotNames.ROUTE);
if (routeAnnotation != null) {
validateRouteMethod(bean, method, transformedAnnotations, beanArchive.getIndex(), routeAnnotation);
routes.add(routeAnnotation);
}
if (routes.isEmpty()) {
AnnotationInstance routesAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTES);
DotNames.ROUTES);
if (routesAnnotation != null) {
for (AnnotationInstance annotation : routesAnnotation.value().asNestedArray()) {
validateRouteMethod(bean, method, transformedAnnotations, beanArchive.getIndex(), annotation);
routes.add(annotation);
}
}
}

if (!routes.isEmpty()) {
LOGGER.debugf("Found route handler business method %s declared on %s", method, bean);

HttpCompression compression = HttpCompression.UNDEFINED;
if (annotationStore.hasAnnotation(method, DotNames.COMPRESSED)) {
compression = HttpCompression.ON;
}
if (annotationStore.hasAnnotation(method, DotNames.UNCOMPRESSED)) {
if (compression == HttpCompression.ON) {
errors.produce(new ValidationErrorBuildItem(new IllegalStateException(
String.format(
"@Compressed and @Uncompressed cannot be both declared on business method %s declared on %s",
method, bean))));
} else {
compression = HttpCompression.OFF;
}
}
routeHandlerBusinessMethods
.produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation));
.produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation,
annotationStore.hasAnnotation(method, DotNames.BLOCKING), compression));
}
//
AnnotationInstance filterAnnotation = annotationStore.getAnnotation(method,
io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER);
DotNames.ROUTE_FILTER);
if (filterAnnotation != null) {
if (!routes.isEmpty()) {
errors.produce(new ValidationErrorBuildItem(new IllegalStateException(
Expand Down Expand Up @@ -367,7 +390,7 @@ public boolean test(String name) {
}
}

if (businessMethod.getMethod().annotation(DotNames.BLOCKING) != null) {
if (businessMethod.isBlocking()) {
if (handlerType == HandlerType.NORMAL) {
handlerType = HandlerType.BLOCKING;
} else if (handlerType == HandlerType.FAILURE) {
Expand All @@ -389,6 +412,10 @@ public boolean test(String name) {
routeHandlers.put(routeString, routeHandler);
}

// Wrap the route handler if necessary
// Note that route annotations with the same values share a single handler implementation
routeHandler = recorder.compressRouteHandler(routeHandler, businessMethod.getCompression());

RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order);
matchers.put(matcher, businessMethod.getMethod());
Function<Router, io.vertx.ext.web.Route> routeFunction = recorder.createRouteFunction(matcher,
Expand Down Expand Up @@ -453,9 +480,9 @@ void routeNotFound(Capabilities capabilities, ResourceNotFoundRecorder recorder,
@BuildStep
AutoAddScopeBuildItem autoAddScope() {
return AutoAddScopeBuildItem.builder()
.containsAnnotations(io.quarkus.vertx.web.deployment.DotNames.ROUTE,
io.quarkus.vertx.web.deployment.DotNames.ROUTES,
io.quarkus.vertx.web.deployment.DotNames.ROUTE_FILTER)
.containsAnnotations(DotNames.ROUTE,
DotNames.ROUTES,
DotNames.ROUTE_FILTER)
.defaultScope(BuiltinScope.SINGLETON)
.reason("Found route handler business methods").build();
}
Expand All @@ -467,10 +494,10 @@ private void validateRouteFilterMethod(BeanInfo bean, MethodInfo method) {
}
List<Type> params = method.parameters();
if (params.size() != 1 || !params.get(0).name()
.equals(io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT)) {
.equals(DotNames.ROUTING_CONTEXT)) {
throw new IllegalStateException(String.format(
"Route filter method must accept exactly one parameter of type %s: %s [method: %s, bean: %s]",
io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT, params, method, bean));
DotNames.ROUTING_CONTEXT, params, method, bean));
}
}

Expand Down Expand Up @@ -1252,7 +1279,7 @@ static List<ParameterInjector> initParamInjectors() {
List<ParameterInjector> injectors = new ArrayList<>();

injectors.add(
ParameterInjector.builder().canEndResponse().matchType(io.quarkus.vertx.web.deployment.DotNames.ROUTING_CONTEXT)
ParameterInjector.builder().canEndResponse().matchType(DotNames.ROUTING_CONTEXT)
.resultHandleProvider(new ResultHandleProvider() {
@Override
public ResultHandle get(MethodInfo method, Type paramType, Set<AnnotationInstance> annotations,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package io.quarkus.vertx.web.compress;

import static io.restassured.RestAssured.get;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import javax.enterprise.context.ApplicationScoped;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.Compressed;
import io.quarkus.vertx.http.Uncompressed;
import io.quarkus.vertx.web.Route;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import io.vertx.ext.web.RoutingContext;

public class CompressionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(MyRoutes.class)
.addAsManifestResource(new StringAsset(MyRoutes.MESSAGE), "resources/file.txt")
.addAsManifestResource(new StringAsset(MyRoutes.MESSAGE), "resources/my.doc"))
.overrideConfigKey("quarkus.http.enable-compression", "true");

@Test
public void testRoutes() {
assertCompressed("/compressed");
assertUncompressed("/uncompressed");
assertCompressed("/compressed-content-type");
assertUncompressed("/uncompressed-content-type");
assertCompressed("/content-type-implicitly-compressed");
assertUncompressed("/content-type-implicitly-uncompressed");
assertCompressed("/compression-disabled-manually");
assertCompressed("/file.txt");
assertUncompressed("/my.doc");
}

private void assertCompressed(String path) {
String bodyStr = get(path).then().statusCode(200).header("Content-Encoding", "gzip").extract().asString();
assertEquals("Hello compression!", bodyStr);
}

private void assertUncompressed(String path) {
ExtractableResponse<Response> response = get(path)
.then().statusCode(200).extract();
assertTrue(response.header("Content-Encoding") == null, response.headers().toString());
assertEquals(MyRoutes.MESSAGE, response.asString());
}

@ApplicationScoped
public static class MyRoutes {

static String MESSAGE = "Hello compression!";

@Compressed
@Route
String compressed() {
return MESSAGE;
}

@Uncompressed
@Route
String uncompressed() {
return MESSAGE;
}

@Uncompressed
@Route
void uncompressedContentType(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

@Compressed
@Route
void compressedContentType(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "foo/bar").end(MESSAGE);
}

@Route
void contentTypeImplicitlyCompressed(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

@Route
void contentTypeImplicitlyUncompressed(RoutingContext context) {
context.response().setStatusCode(200).putHeader("Content-type", "foo/bar").end(MESSAGE);
}

@Route
void compressionDisabledManually(RoutingContext context) {
context.response().headers().remove("Content-Encoding");
context.response().setStatusCode(200).putHeader("Content-type", "text/plain").end(MESSAGE);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.quarkus.vertx.web.runtime;

import java.util.Set;

import io.quarkus.vertx.http.runtime.HttpCompression;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;

public class HttpCompressionHandler implements Handler<RoutingContext> {

private final Handler<RoutingContext> routeHandler;
private final HttpCompression compression;
private final Set<String> compressedMediaTypes;

public HttpCompressionHandler(Handler<RoutingContext> routeHandler, HttpCompression compression,
Set<String> compressedMediaTypes) {
this.routeHandler = routeHandler;
this.compression = compression;
this.compressedMediaTypes = compressedMediaTypes;
}

@Override
public void handle(RoutingContext context) {
context.addEndHandler(new Handler<AsyncResult<Void>>() {
@Override
public void handle(AsyncResult<Void> result) {
if (result.succeeded()) {
MultiMap headers = context.response().headers();
String contentEncoding = headers.get(HttpHeaders.CONTENT_ENCODING);
if (contentEncoding != null && HttpHeaders.IDENTITY.toString().equals(contentEncoding)) {
switch (compression) {
case ON:
headers.remove(HttpHeaders.CONTENT_ENCODING);
break;
case UNDEFINED:
String contentType = headers.get(HttpHeaders.CONTENT_TYPE);
if (contentType != null
&& compressedMediaTypes.contains(contentType)) {
headers.remove(HttpHeaders.CONTENT_ENCODING);
}
break;
default:
// OFF - no action is needed because the "Content-Encoding: identity" header is set
break;
}
}
}
}
});
routeHandler.handle(context);
}

}
Loading

0 comments on commit 171a9ba

Please sign in to comment.