diff --git a/docs/src/main/asciidoc/reactive-routes.adoc b/docs/src/main/asciidoc/reactive-routes.adoc index 5d2c55ce37b9d..9724cda59b93a 100644 --- a/docs/src/main/asciidoc/reactive-routes.adoc +++ b/docs/src/main/asciidoc/reactive-routes.adoc @@ -682,6 +682,26 @@ public class MyFilters { <1> The `RouteFilter#value()` defines the priority used to sort the filters - filters with higher priority are called first. <2> The filter is likely required to call the `next()` method to continue the chain. +== HTTP Compression + +The body of an HTTP response is not compressed by default. +You can enable the HTTP compression support by means of `quarkus.http.enable-compression=true`. + +If compression support is enabled then the response body is compressed if: + +- the route method is annotated with `@io.quarkus.vertx.http.Compressed`, or +- the `Content-Type` header is set and the value is a compressed media type as configured via `quarkus.http.compress-media-types`. + +The response body is never compressed if: + +- the route method is annotated with `@io.quarkus.vertx.http.Uncompressed`, or +- the `Content-Type` header is not set. + +TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript` and `application/javascript`. + +NOTE: If the client does not support HTTP compression then the response body is not compressed. + + == Adding OpenAPI and Swagger UI You can add support for link:https://www.openapis.org/[OpenAPI] and link:https://swagger.io/tools/swagger-ui/[Swagger UI] by using the `quarkus-smallrye-openapi` extension. diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index e3e202f8f9008..22775b35c4606 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -2177,6 +2177,26 @@ Or plain text: < {"name":"roquefort"} ---- +=== HTTP Compression + +The body of an HTTP response is not compressed by default. +You can enable the HTTP compression support by means of `quarkus.http.enable-compression=true`. + +If compression support is enabled then the response body is compressed if: + +- the resource method is annotated with `@io.quarkus.vertx.http.Compressed`, or +- the `Content-Type` header is set and the value is a compressed media type as configured via `quarkus.http.compress-media-types`. + +The response body is never compressed if: + +- the resource method is annotated with `@io.quarkus.vertx.http.Uncompressed`, or +- the `Content-Type` header is not set. + +TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript` and `application/javascript`. + +NOTE: If the client does not support HTTP compression then the response body is not compressed. + + == Include/Exclude JAX-RS classes with build time conditions Quarkus enables the inclusion or exclusion of JAX-RS Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans. diff --git a/docs/src/main/asciidoc/resteasy.adoc b/docs/src/main/asciidoc/resteasy.adoc index 2cfde22fb22a4..8fb011682fa01 100644 --- a/docs/src/main/asciidoc/resteasy.adoc +++ b/docs/src/main/asciidoc/resteasy.adoc @@ -615,8 +615,7 @@ This configuration option would recognize strings in this format (shown as a reg Once GZip support has been enabled you can use it on an endpoint by adding the `@org.jboss.resteasy.annotations.GZIP` annotation to your endpoint method. -If you want to compress everything then we recommended that you use the `quarkus.http.enable-compression=true` setting instead to globally enable -compression support. +NOTE: The configuration property `quarkus.http.enable-compression` has no effect on compression support of RESTEasy Classic endpoints. == Multipart Support diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java index e9ddd2d5943a9..d600111d7a87c 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/AnnotatedRouteHandlerBuildItem.java @@ -7,6 +7,7 @@ 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 { @@ -14,13 +15,23 @@ public final class AnnotatedRouteHandlerBuildItem extends MultiBuildItem { private final List routes; private final AnnotationInstance routeBase; private final MethodInfo method; + private final boolean blocking; + private final HttpCompression compression; public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List routes, AnnotationInstance routeBase) { + this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED); + } + + public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List 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() { @@ -39,4 +50,12 @@ public AnnotationInstance getRouteBase() { return routeBase; } + public boolean isBlocking() { + return blocking; + } + + public HttpCompression getCompression() { + return compression; + } + } diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java index b0a81b5784a68..6eefc0add80e6 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java @@ -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; @@ -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()); } diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java index 2498aca53363f..1a935705a8f05 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java @@ -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; @@ -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; @@ -145,12 +147,12 @@ FeatureBuildItem feature() { @BuildStep void unremovableBeans(BuildProducer 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 @@ -168,18 +170,22 @@ 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("")) { + continue; + } + List 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); @@ -187,14 +193,31 @@ void validateBeanDeployment( } } } + 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( @@ -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) { @@ -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 routeFunction = recorder.createRouteFunction(matcher, @@ -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(); } @@ -467,10 +494,10 @@ private void validateRouteFilterMethod(BeanInfo bean, MethodInfo method) { } List 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)); } } @@ -1252,7 +1279,7 @@ static List initParamInjectors() { List 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 annotations, diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/compress/CompressionTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/compress/CompressionTest.java new file mode 100644 index 0000000000000..105ed6598c499 --- /dev/null +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/compress/CompressionTest.java @@ -0,0 +1,109 @@ +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"); + assertCompressed("/content-type-with-param-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 = 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 contentTypeWithParamImplicitlyCompressed(RoutingContext context) { + context.response().setStatusCode(200).putHeader("Content-type", "text/plain;charset=UTF-8").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); + } + + } + +} diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/HttpCompressionHandler.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/HttpCompressionHandler.java new file mode 100644 index 0000000000000..8fe850f548dbf --- /dev/null +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/HttpCompressionHandler.java @@ -0,0 +1,60 @@ +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 { + + private final Handler routeHandler; + private final HttpCompression compression; + private final Set compressedMediaTypes; + + public HttpCompressionHandler(Handler routeHandler, HttpCompression compression, + Set compressedMediaTypes) { + this.routeHandler = routeHandler; + this.compression = compression; + this.compressedMediaTypes = compressedMediaTypes; + } + + @Override + public void handle(RoutingContext context) { + context.addEndHandler(new Handler>() { + @Override + public void handle(AsyncResult 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); + int paramIndex = contentType.indexOf(';'); + if (paramIndex > -1) { + contentType = contentType.substring(0, paramIndex); + } + 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); + } + +} diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index c882d214faae7..7c9ceba7a0646 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -1,9 +1,14 @@ package io.quarkus.vertx.web.runtime; import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; import java.util.function.Function; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.runtime.HttpCompression; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Router; @@ -12,6 +17,12 @@ @Recorder public class VertxWebRecorder { + final RuntimeValue httpConfiguration; + + public VertxWebRecorder(RuntimeValue httpConfiguration) { + this.httpConfiguration = httpConfiguration; + } + @SuppressWarnings("unchecked") public Handler createHandler(String handlerClassName) { try { @@ -29,6 +40,17 @@ public Handler createHandler(String handlerClassName) { } } + public Handler compressRouteHandler(Handler routeHandler, HttpCompression compression) { + if (httpConfiguration.getValue().enableCompression) { + return new HttpCompressionHandler(routeHandler, compression, + compression == HttpCompression.UNDEFINED + ? Set.copyOf(httpConfiguration.getValue().compressMediaTypes.orElse(List.of())) + : Set.of()); + } else { + return routeHandler; + } + } + public Function createRouteFunction(RouteMatcher matcher, Handler bodyHandler) { return new Function() { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java index 34d751abd3617..c071b9fd9db75 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java @@ -416,6 +416,12 @@ public String getResponseHeader(String name) { return response.getHeader(name); } + @Override + public void removeResponseHeader(String name) { + // Servlet API does not support this functionality + throw new UnsupportedOperationException(); + } + @Override public boolean closed() { return context.response().closed(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java new file mode 100644 index 0000000000000..e2e24ed6b4597 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CompressionScanner.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.reactive.server.deployment; + +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; +import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; + +import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveCompressionHandler; +import io.quarkus.vertx.http.Compressed; +import io.quarkus.vertx.http.Uncompressed; +import io.quarkus.vertx.http.runtime.HttpCompression; + +public class CompressionScanner implements MethodScanner { + + static final DotName COMPRESSED = DotName.createSimple(Compressed.class.getName()); + static final DotName UNCOMPRESSED = DotName.createSimple(Uncompressed.class.getName()); + + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + AnnotationStore annotationStore = (AnnotationStore) methodContext.get(EndpointIndexer.METHOD_CONTEXT_ANNOTATION_STORE); + HttpCompression compression = HttpCompression.UNDEFINED; + if (annotationStore.hasAnnotation(method, COMPRESSED)) { + compression = HttpCompression.ON; + } + if (annotationStore.hasAnnotation(method, UNCOMPRESSED)) { + if (compression == HttpCompression.ON) { + throw new IllegalStateException( + String.format( + "@Compressed and @Uncompressed cannot be both declared on resource method %s declared on %s", + method, actualEndpointClass)); + } else { + compression = HttpCompression.OFF; + } + } + if (compression == HttpCompression.OFF) { + // No action is needed because the "Content-Encoding: identity" header is set for every request if compression is enabled + return List.of(); + } + ResteasyReactiveCompressionHandler handler = new ResteasyReactiveCompressionHandler(); + handler.setCompression(compression); + return List.of(new FixedHandlerChainCustomizer(handler, HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED)); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java index 63a628c6cdc4b..6014168931a07 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java @@ -89,6 +89,11 @@ public MethodScannerBuildItem cacheControlSupport() { return new MethodScannerBuildItem(new CacheControlScanner()); } + @BuildStep + public MethodScannerBuildItem compressionSupport() { + return new MethodScannerBuildItem(new CompressionScanner()); + } + @BuildStep public ResourceInterceptorsContributorBuildItem scanForInterceptors(CombinedIndexBuildItem combinedIndexBuildItem, ApplicationResultBuildItem applicationResultBuildItem) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java new file mode 100644 index 0000000000000..4f49ffa6ceeac --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/compress/CompressionTest.java @@ -0,0 +1,110 @@ +package io.quarkus.resteasy.reactive.server.test.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.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestResponse; +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.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +public class CompressionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(MyEndpoint.class) + .addAsManifestResource(new StringAsset(MyEndpoint.MESSAGE), "resources/file.txt") + .addAsManifestResource(new StringAsset(MyEndpoint.MESSAGE), "resources/my.doc")) + .overrideConfigKey("quarkus.http.enable-compression", "true"); + + @Test + public void testEndpoint() { + assertCompressed("/endpoint/compressed"); + assertUncompressed("/endpoint/uncompressed"); + assertCompressed("/endpoint/compressed-content-type"); + assertUncompressed("/endpoint/uncompressed-content-type"); + assertCompressed("/endpoint/content-type-implicitly-compressed"); + assertCompressed("/endpoint/content-type-with-param-implicitly-compressed"); + assertUncompressed("/endpoint/content-type-implicitly-uncompressed"); + + 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(MyEndpoint.MESSAGE, bodyStr); + } + + private void assertUncompressed(String path) { + ExtractableResponse response = get(path) + .then().statusCode(200).extract(); + assertTrue(response.header("Content-Encoding") == null, response.headers().toString()); + assertEquals(MyEndpoint.MESSAGE, response.asString()); + } + + @Path("endpoint") + public static class MyEndpoint { + + static String MESSAGE = "Hello compression!"; + + @Compressed + @GET + @Path("compressed") + public String compressed() { + return MESSAGE; + } + + @Uncompressed + @GET + @Path("uncompressed") + public String uncompressed() { + return MESSAGE; + } + + @Uncompressed + @GET + @Path("uncompressed-content-type") + public RestResponse uncompressedContentType() { + return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "text/plain").build(); + } + + @Compressed + @GET + @Path("compressed-content-type") + public RestResponse compressedContentType() { + return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "foo/bar").build(); + } + + @GET + @Path("content-type-implicitly-compressed") + public RestResponse contentTypeImplicitlyCompressed() { + return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "text/plain").build(); + } + + @GET + @Path("content-type-with-param-implicitly-compressed") + public RestResponse contentTypeWithParamImplicitlyCompressed() { + return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "text/plain;charset=UTF-8").build(); + } + + @GET + @Path("content-type-implicitly-uncompressed") + public RestResponse contentTypeImplicitlyUncompressed() { + return RestResponse.ResponseBuilder.ok().entity(MESSAGE).header("Content-type", "foo/bar").build(); + } + + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java new file mode 100644 index 0000000000000..2dca6fd726952 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveCompressionHandler.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.reactive.server.runtime; + +import java.util.Set; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.RuntimeConfigurableServerRestHandler; +import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; +import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +import io.quarkus.vertx.http.runtime.HttpCompression; + +public class ResteasyReactiveCompressionHandler implements ServerRestHandler, RuntimeConfigurableServerRestHandler { + + private HttpCompression compression; + private volatile boolean enableCompression; + private volatile Set compressMediaTypes; + + public ResteasyReactiveCompressionHandler() { + } + + public HttpCompression getCompression() { + return compression; + } + + public void setCompression(HttpCompression compression) { + this.compression = compression; + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + if (enableCompression) { + ServerHttpResponse response = requestContext.serverResponse(); + String contentEncoding = response.getResponseHeader(HttpHeaders.CONTENT_ENCODING); + if (contentEncoding != null && io.vertx.core.http.HttpHeaders.IDENTITY.toString().equals(contentEncoding)) { + switch (compression) { + case ON: + response.removeResponseHeader(HttpHeaders.CONTENT_ENCODING); + break; + case UNDEFINED: + MediaType contentType = requestContext.getResponseContentType().getMediaType(); + if (contentType != null + && compressMediaTypes.contains(contentType.getType() + '/' + contentType.getSubtype())) { + response.removeResponseHeader(HttpHeaders.CONTENT_ENCODING); + } + break; + default: + // OFF - no action is needed because the "Content-Encoding: identity" header is set + break; + } + } + } + } + + @Override + public void configure(RuntimeConfiguration configuration) { + enableCompression = configuration.enableCompression(); + compressMediaTypes = configuration.compressMediaTypes(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java index a229d8dee5119..c11335aaef04b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import org.jboss.resteasy.reactive.server.core.Deployment; import org.jboss.resteasy.reactive.server.spi.DefaultRuntimeConfiguration; @@ -33,7 +34,8 @@ public void configure(RuntimeValue deployment, RuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(httpConf.readTimeout, httpConf.body.deleteUploadedFilesOnEnd, httpConf.body.uploadsDirectory, runtimeConf.multipart.inputPart.defaultCharset, maxBodySize, - httpConf.limits.maxFormAttributeSize.asLongValue()); + httpConf.limits.maxFormAttributeSize.asLongValue(), httpConf.enableCompression, + Set.copyOf(httpConf.compressMediaTypes.orElse(List.of()))); List runtimeConfigurableServerRestHandlers = deployment.getValue() .getRuntimeConfigurableServerRestHandlers(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index 6089c42cb71b5..e210bf6961a71 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -30,6 +30,7 @@ import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.StaticResourcesRecorder; /** @@ -123,9 +124,8 @@ void collectStaticResources(Capabilities capabilities, ApplicationArchivesBuildI @BuildStep @Record(RUNTIME_INIT) public void runtimeInit(Optional staticResources, - StaticResourcesRecorder recorder, - CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, - BuildProducer defaultRoutes) { + StaticResourcesRecorder recorder, CoreVertxBuildItem vertx, BeanContainerBuildItem beanContainer, + BuildProducer defaultRoutes, HttpConfiguration config) { if (staticResources.isPresent()) { defaultRoutes.produce(new DefaultRouteBuildItem(recorder.start(staticResources.get().getPaths()))); } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java index 801627806cfda..e22ce6ba2eb13 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java @@ -13,6 +13,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; +import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.Router; public class CompressionTest { @@ -57,10 +58,13 @@ static class BeanRegisteringRouteUsingObserves { public void register(@Observes Router router) { router.route("/compress").handler(rc -> { + // The content-encoding header must be removed + rc.response().headers().remove(HttpHeaders.CONTENT_ENCODING); rc.response().end(longString); }); router.route("/nocompress").handler(rc -> { - rc.response().headers().set("content-encoding", "identity"); + // This header is set by default + // rc.response().headers().set("content-encoding", "identity"); rc.response().end(longString); }); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Compressed.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Compressed.java new file mode 100644 index 0000000000000..1fe8b30b90153 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Compressed.java @@ -0,0 +1,18 @@ +package io.quarkus.vertx.http; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotation can be used to enable the compression of an HTTP response for a particular method. + * + * @see Uncompressed + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface Compressed { + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Uncompressed.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Uncompressed.java new file mode 100644 index 0000000000000..1fa8ea59536fb --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/Uncompressed.java @@ -0,0 +1,18 @@ +package io.quarkus.vertx.http; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotation can be used to disable the compression of an HTTP response for a particular method. + * + * @see Compressed + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface Uncompressed { + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpCompression.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpCompression.java new file mode 100644 index 0000000000000..3711907c18c56 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpCompression.java @@ -0,0 +1,24 @@ +package io.quarkus.vertx.http.runtime; + +import io.quarkus.vertx.http.Compressed; +import io.quarkus.vertx.http.Uncompressed; + +public enum HttpCompression { + /** + * Compression is explicitly enabled. + * + * @see Compressed + */ + ON, + /** + * Compression is explicitly disabled. + * + * @see Uncompressed + */ + OFF, + /** + * Compression will be enabled if the response has the {@code Content-Type} header set and the value is listed in + * {@link HttpConfiguration#compressMediaTypes}. + */ + UNDEFINED +} \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 3f1db8397250f..1ce4d25e4f193 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; @@ -9,6 +10,8 @@ import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.vertx.http.Compressed; +import io.quarkus.vertx.http.Uncompressed; import io.quarkus.vertx.http.runtime.cors.CORSConfig; @ConfigRoot(phase = ConfigPhase.RUN_TIME) @@ -237,6 +240,19 @@ public class HttpConfiguration { @ConfigItem public boolean enableDecompression; + /** + * List of media types for which the compression should be enabled automatically, unless declared explicitly via + * {@link Compressed} or {@link Uncompressed}. + */ + @ConfigItem(defaultValue = "text/html,text/plain,text/xml,text/css,text/javascript,application/javascript") + public Optional> compressMediaTypes; + + /** + * The compression level used when compression support is enabled. + */ + @ConfigItem + public OptionalInt compressionLevel; + /** * Provides a hint (optional) for the default content type of responses generated for * the errors not handled by the application. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index 621b33d85edec..851b552945ae8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -6,8 +6,11 @@ import java.util.Set; import java.util.function.Consumer; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.impl.MimeMapping; import io.vertx.ext.web.Route; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.StaticHandler; @@ -19,6 +22,12 @@ public class StaticResourcesRecorder { private static volatile List hotDeploymentResourcePaths; + final RuntimeValue httpConfiguration; + + public StaticResourcesRecorder(RuntimeValue httpConfiguration) { + this.httpConfiguration = httpConfiguration; + } + public static void setHotDeploymentResources(List resources) { hotDeploymentResourcePaths = resources; } @@ -61,6 +70,10 @@ public void handle(RoutingContext ctx) { ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length()); if (knownPaths.contains(rel)) { staticHandler.handle(ctx); + if (httpConfiguration.getValue().enableCompression && isCompressed(rel)) { + // Remove the "Content-Encoding: identity" header and enable compression + ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING); + } } else { // make sure we don't lose the correct TCCL to Vert.x... Thread.currentThread().setContextClassLoader(currentCl); @@ -81,4 +94,16 @@ public void accept(Route route) { }; } + private boolean isCompressed(String path) { + String suffix; + int lastDot = path.lastIndexOf('.'); + if (lastDot != -1 && lastDot != path.length() - 1) { + suffix = path.substring(lastDot + 1); + } else { + suffix = null; + } + String contentType = MimeMapping.getMimeTypeForExtension(suffix); + return httpConfiguration.getValue().compressMediaTypes.orElse(List.of()).contains(contentType); + } + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index e9616ef08ad63..18c11d0ccaab9 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -337,6 +337,18 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } + if (httpConfiguration.enableCompression) { + httpRouteRouter.route().order(0).handler(new Handler() { + @Override + public void handle(RoutingContext ctx) { + // Add "Content-Encoding: identity" header that disables the compression + // This header can be removed to enable the compression + ctx.response().putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY); + ctx.next(); + } + }); + } + httpRouteRouter.route().last().failureHandler( new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentTypeDefault)); @@ -767,6 +779,9 @@ private static void applyCommonOptions(HttpServerOptions httpServerOptions, Http httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); httpServerOptions.setCompressionSupported(httpConfiguration.enableCompression); + if (httpConfiguration.compressionLevel.isPresent()) { + httpServerOptions.setCompressionLevel(httpConfiguration.compressionLevel.getAsInt()); + } httpServerOptions.setDecompressionSupported(httpConfiguration.enableDecompression); httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java index 6fe9c4a290ab1..34bb1d35c8f10 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java @@ -3,14 +3,24 @@ import java.nio.charset.Charset; import java.time.Duration; import java.util.Optional; +import java.util.Set; public class DefaultRuntimeConfiguration implements RuntimeConfiguration { final Duration readTimeout; private final Body body; private final Limits limits; + private final boolean enableCompression; + private final Set compressMediaTypes; public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory, Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { + this(readTimeout, deleteUploadedFilesOnEnd, uploadsDirectory, defaultCharset, maxBodySize, maxFormAttributeSize, false, + Set.of()); + } + + public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory, + Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize, boolean enableCompression, + Set compressMediaTypes) { this.readTimeout = readTimeout; body = new Body() { @Override @@ -39,6 +49,8 @@ public long maxFormAttributeSize() { return maxFormAttributeSize; } }; + this.enableCompression = enableCompression; + this.compressMediaTypes = compressMediaTypes; } @Override @@ -55,4 +67,14 @@ public Body body() { public Limits limits() { return limits; } + + @Override + public boolean enableCompression() { + return enableCompression; + } + + @Override + public Set compressMediaTypes() { + return compressMediaTypes; + } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java index 5bdd06534a537..a8e5948fadefc 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java @@ -3,6 +3,7 @@ import java.nio.charset.Charset; import java.time.Duration; import java.util.Optional; +import java.util.Set; public interface RuntimeConfiguration { @@ -12,6 +13,10 @@ public interface RuntimeConfiguration { Limits limits(); + boolean enableCompression(); + + Set compressMediaTypes(); + interface Body { boolean deleteUploadedFilesOnEnd(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java index 0b819c43b57e2..575f65089505c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ServerHttpResponse.java @@ -28,6 +28,8 @@ public interface ServerHttpResponse extends StreamingResponse