package com.vtence.molecule.middlewares;
import com.vtence.molecule.Body;
import com.vtence.molecule.Request;
import com.vtence.molecule.Response;
import com.vtence.molecule.http.AcceptEncoding;
import com.vtence.molecule.http.ContentType;
import com.vtence.molecule.http.MimeTypes;
import com.vtence.molecule.lib.ChunkedBody;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import static com.vtence.molecule.http.HeaderNames.CONTENT_ENCODING;
import static com.vtence.molecule.http.HeaderNames.CONTENT_LENGTH;
import static com.vtence.molecule.http.HttpStatus.NOT_ACCEPTABLE;
import static com.vtence.molecule.http.MimeTypes.TEXT;
import static com.vtence.molecule.middlewares.Compressor.Codings.identity;
public class Compressor extends AbstractMiddleware {
private final Collection<String> compressibleTypes = new ArrayList<>();
enum Codings {
gzip {
public void encode(Response response) {
response.removeHeader(CONTENT_LENGTH);
response.header(CONTENT_ENCODING, name());
response.body(new GZipStream(response.body()));
}
},
deflate {
public void encode(Response response) {
response.removeHeader(CONTENT_LENGTH);
response.header(CONTENT_ENCODING, name());
response.body(new DeflateStream(response.body()));
}
},
identity {
public void encode(Response response) {
}
};
public abstract void encode(Response response);
public static String[] all() {
List<String> all = new ArrayList<>();
for (Codings coding : values()) {
all.add(coding.name());
}
return all.toArray(new String[all.size()]);
}
private static class GZipStream extends ChunkedBody {
private final Body body;
public GZipStream(Body body) {
this.body = body;
}
public void writeTo(OutputStream out, Charset charset) throws IOException {
GZIPOutputStream zip = new GZIPOutputStream(out);
try {
body.writeTo(zip, charset);
} finally {
zip.finish();
}
}
public void close() throws IOException {
body.close();
}
}
private static class DeflateStream extends ChunkedBody {
private final Body body;
public DeflateStream(Body body) {
this.body = body;
}
public void writeTo(OutputStream out, Charset charset) throws IOException {
Deflater zlib = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
DeflaterOutputStream deflate = new DeflaterOutputStream(out, zlib);
try {
body.writeTo(deflate, charset);
} finally {
deflate.finish();
zlib.end();
}
}
public void close() throws IOException {
body.close();
}
}
}
public Compressor compressibleTypes(String... mimeTypes) {
this.compressibleTypes.addAll(Arrays.asList(mimeTypes));
return this;
}
public void handle(Request request, Response response) throws Exception {
forward(request, response).whenSuccessful(compressResponse(selectBestAvailableEncodingFor(request)));
}
private Consumer<Response> compressResponse(String bestEncoding) {
return response -> {
if (unqualified(response)) {
return;
}
if (bestEncoding != null) {
Codings coding = Codings.valueOf(bestEncoding);
coding.encode(response);
} else {
notAcceptable(response);
}
};
}
private boolean unqualified(Response response) {
return empty(response) || alreadyEncoded(response) || !compressible(response);
}
private boolean empty(Response response) {
return response.empty();
}
private boolean alreadyEncoded(Response response) {
String contentEncoding = response.header(CONTENT_ENCODING);
return contentEncoding != null && !isIdentity(contentEncoding);
}
private boolean compressible(Response response) {
return compressibleTypes.isEmpty() || compressible(ContentType.of(response).mediaType());
}
private boolean compressible(String contentType) {
for (String compressible : compressibleTypes) {
if (MimeTypes.matches(contentType, compressible)) return true;
}
return false;
}
private boolean isIdentity(String contentEncoding) {
return contentEncoding.matches(atWordBoundaries(identity.name()));
}
private String atWordBoundaries(String text) {
return "\\b" + text + "\\b";
}
private String selectBestAvailableEncodingFor(Request request) {
AcceptEncoding acceptEncoding = AcceptEncoding.of(request);
return acceptEncoding.selectBestEncoding(Codings.all());
}
private void notAcceptable(Response response) {
response.status(NOT_ACCEPTABLE);
response.contentType(TEXT);
response.body("An acceptable encoding could not be found");
}
}