Skip to content

Commit

Permalink
Support compression (gzip content encoding)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcdv committed Dec 31, 2023
1 parent f626eb1 commit 07c1df2
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 43 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Flak components | Description
* [Query argument](#query-argument)
* [Form argument](#form-argument)
* [Custom arguments](#custom-arguments)
* [Compression](#compression)
* [Managing apps](#managing-apps)
* [To be continued....](#to-be-continued)
* [Why Flak?](#why-flak)
Expand All @@ -39,7 +40,7 @@ Flak components | Description
* [How to publish locally](#how-to-publish-locally)

<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
<!-- Added by: pcdv, at: Sat Sep 10 06:05:40 2022 -->
<!-- Added by: pcdv, at: Sun Dec 31 19:04:56 2023 -->

<!--te-->
<!-- to update TOC:
Expand Down Expand Up @@ -208,6 +209,17 @@ You can accept other argument types if you:
}
```

### Compression

Gzip compression can be enabled for a given endpoint or all endpoints of a
class by using the `@Compress` annotation.

Files served with `FlakResourceImpl` will be automatically compressed
according to their content type and size.

It is possible to tune compression behavior:
* file size threshold (using system property `flak.compressThreshold`)
* eligible content types (using a custom `ContentTypeProvider`)

### Managing apps

Expand Down
17 changes: 17 additions & 0 deletions flak-api/src/main/java/flak/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public interface Response {

void addHeader(String header, String value);

/**
* Checks whether specified response header is set.
*/
boolean hasResponseHeader(String name);

/**
* Warning: must be called after addHeader().
*
Expand All @@ -27,4 +32,16 @@ public interface Response {
* location.
*/
void redirect(String path);

/**
* Configures whether gzip compression can be applied automatically according
* to Accept-Encoding request header and absence of Content-Encoding response
* header (false by default).
*/
void setCompressionAllowed(boolean compressionAllowed);

/**
* @see #setCompressionAllowed(boolean)
*/
boolean isCompressionAllowed();
}
15 changes: 15 additions & 0 deletions flak-api/src/main/java/flak/annotations/Compress.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package flak.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Enables compression on an endpoint, or all endpoints of a class.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Compress {
int COMPRESS_THRESHOLD = Integer.getInteger("flak.compressThreshold", 1024);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ void addHeadersInto(HttpExchange exchange) {
exchange.getResponseHeaders().add(arr[0], arr[1]);
}
}

public boolean has(String name) {
return list.stream().anyMatch(arr -> name.equals(arr[0]));
}
}
35 changes: 30 additions & 5 deletions flak-backend-jdk/src/main/java/flak/backend/jdk/JdkRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
import flak.Request;
import flak.Response;
import flak.spi.SPRequest;
import flak.spi.SPResponse;
import flak.spi.util.IO;

public class JdkRequest implements SPRequest, Response {
public class JdkRequest implements SPRequest, SPResponse {

private static final String[] EMPTY = {};

Expand All @@ -32,14 +33,15 @@ public class JdkRequest implements SPRequest, Response {

private final String appRelativePath;

private BufferedOutputStream outputStream;
private OutputStream outputStream;

private Form form;
private final HeaderList headers = new HeaderList();
private int status;
private boolean statusFlushed;

private Method handler;
private boolean compressionAllowed;

public JdkRequest(App app,
String appRelativePath,
Expand Down Expand Up @@ -175,6 +177,11 @@ public void addHeader(String header, String value) {
headers.add(header, value);
}

@Override
public boolean hasResponseHeader(String name) {
return headers.has(name);
}

public void setStatus(int status) {
if (statusFlushed && status != this.status)
throw new IllegalStateException("Status has already been sent: " + this.status);
Expand All @@ -190,7 +197,8 @@ public OutputStream getOutputStream() {
if (outputStream == null) {
this.outputStream = new BufferedOutputStream(exchange.getResponseBody(), 8192) {
@Override
public void close() {
public void close() throws IOException {
super.flush();
// disable close, we will do it in finish()
}

Expand All @@ -210,10 +218,22 @@ public void redirect(String location) {
setStatus(HttpURLConnection.HTTP_MOVED_TEMP);
}

@Override
public void setCompressionAllowed(boolean compressionAllowed) {
this.compressionAllowed = compressionAllowed;

}

@Override
public boolean isCompressionAllowed() {
return compressionAllowed;
}

void finish() throws IOException {
flushStatus();
if (outputStream != null)
outputStream.flush();
if (outputStream != null) {
outputStream.close();
}
exchange.getResponseBody().close();
}

Expand All @@ -231,4 +251,9 @@ private void flushStatus() throws IOException {
boolean hasOutputStream() {
return outputStream != null;
}

@Override
public void setOutputStream(OutputStream out) {
this.outputStream = out;
}
}
53 changes: 32 additions & 21 deletions flak-backend-jdk/src/main/java/flak/backend/jdk/MethodHandler.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package flak.backend.jdk;

import flak.Response;
import flak.annotations.Compress;
import flak.spi.AbstractMethodHandler;
import flak.spi.CompressionHelper;
import flak.spi.SPRequest;
import flak.spi.util.IO;
import flak.spi.util.Log;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -57,33 +60,41 @@ protected boolean isApplicable(SPRequest req) {

@SuppressWarnings({"StatementWithEmptyBody", "unchecked"})
protected void processResponse(Response r, Object res) throws Exception {
if (allowCompress)
r.setCompressionAllowed(true);
if (outputFormat != null) {
outputFormat.convert(res, r);
}
else if (res instanceof Response) {
// do nothing: status and headers should already be set
}
else if (res instanceof String) {
r.setStatus(HttpURLConnection.HTTP_OK);
r.getOutputStream().write(((String) res).getBytes(StandardCharsets.UTF_8));
}
else if (res instanceof byte[]) {
r.setStatus(HttpURLConnection.HTTP_OK);
r.getOutputStream().write((byte[]) res);
}
else if (res instanceof InputStream) {
r.setStatus(HttpURLConnection.HTTP_OK);
IO.pipe((InputStream) res, r.getOutputStream(), false);
}
else if (res == null) {
if (!r.isStatusSet())
r.setStatus(200);
}
else
throw new RuntimeException("Unexpected return value: " + res + " from " + javaMethod
.toGenericString());
else {
OutputStream out = r.getOutputStream();
if (res instanceof String) {
r.setStatus(HttpURLConnection.HTTP_OK);
if (((String) res).length() > Compress.COMPRESS_THRESHOLD)
out = CompressionHelper.maybeCompress(r);
out.write(((String) res).getBytes(StandardCharsets.UTF_8));
}
else if (res instanceof byte[]) {
r.setStatus(HttpURLConnection.HTTP_OK);
if (((byte[]) res).length > Compress.COMPRESS_THRESHOLD)
out = CompressionHelper.maybeCompress(r);
out.write((byte[]) res);
}
else if (res instanceof InputStream) {
r.setStatus(HttpURLConnection.HTTP_OK);
out = CompressionHelper.maybeCompress(r);
IO.pipe((InputStream) res, out, false);
}
else if (res == null) {
if (!r.isStatusSet())
r.setStatus(200);
}
else
throw new RuntimeException("Unexpected return value: " + res + " from " + javaMethod
.toGenericString());

}
}


}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package flak.backend.netty;

import flak.Request;
import flak.Response;
import flak.spi.SPResponse;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpHeaders;
Expand All @@ -13,7 +13,7 @@
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;

public class NettyResponse implements Response {
public class NettyResponse implements SPResponse {
private final NettyRequest req;
private final DefaultHttpHeaders headers;
private int status = 200;
Expand All @@ -33,6 +33,11 @@ public void addHeader(String header, String value) {
headers.add(header, value);
}

@Override
public boolean hasResponseHeader(String name) {
return headers.contains(name);
}

@Override
public void setStatus(int status) {
this.status = status;
Expand All @@ -54,6 +59,16 @@ public void redirect(String location) {
setStatus(HttpURLConnection.HTTP_MOVED_TEMP);
}

@Override
public void setCompressionAllowed(boolean compressionAllowed) {

}

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

public int getStatus() {
return status;
}
Expand All @@ -67,4 +82,9 @@ public HttpResponse toHttpResponse() {
//HttpUtil.setKeepAlive(r, false);
//return r;
}

@Override
public void setOutputStream(OutputStream out) {
throw new RuntimeException("TODO");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectWriter;
import flak.OutputFormatter;
import flak.Response;
import flak.spi.CompressionHelper;

/**
* An output formatter using an ObjectWriter, which is the recommended way of
Expand All @@ -25,7 +26,6 @@ public JsonOutputFormatter(ObjectWriter writer) {
@Override
public void convert(T data, Response resp) throws Exception {
resp.addHeader("Content-Type", "application/json");
resp.setStatus(200);
writer.writeValue(resp.getOutputStream(), data);
writer.writeValue(CompressionHelper.maybeCompress(resp), data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import flak.HttpException;
import flak.Request;
import flak.spi.CompressionHelper;
import flak.spi.RestrictedTarget;
import flak.spi.SPResponse;
import flak.spi.util.IO;
import flak.spi.util.Log;

Expand Down Expand Up @@ -47,17 +49,20 @@ public void doGet(Request r, String ignored) throws Exception {

InputStream in;

OutputStream out = r.getResponse().getOutputStream();
try {
in = openPath(path);
in = openPath(path, (SPResponse) r.getResponse());
String contentType = mime.getContentType(path);
if (contentType != null)
if (contentType != null) {
r.getResponse().addHeader("Content-Type", contentType);
if (mime.shouldCompress(contentType))
out = CompressionHelper.maybeCompress(r.getResponse());
}
}
catch (FileNotFoundException e) {
throw new HttpException(404, "Not found");
}

OutputStream out = r.getResponse().getOutputStream();
if (in != null) {
r.getResponse().setStatus(200);
try {
Expand All @@ -76,5 +81,5 @@ public void doGet(Request r, String ignored) throws Exception {
}
}

protected abstract InputStream openPath(String p) throws FileNotFoundException;
protected abstract InputStream openPath(String p, SPResponse resp) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ public interface ContentTypeProvider {
*/
String getContentType(String path);

/**
* Used when serving resources, to help determine if a document should be
* compressed.
*/
default boolean shouldCompress(String contentType) {
return contentType.contains("text") || contentType.contains("/json");
}
}
Loading

0 comments on commit 07c1df2

Please sign in to comment.