From 72801bf55591c24fcb55d0342dd78a350d0cc53c Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:06:41 +0100 Subject: [PATCH 01/35] Force Charset specification for strings String.getBytes() encode a string using the platform default encoding. In RenderableString, the string was encoded without using the Charset specified or didn't specify the default Charset if no Charset was set. Now, when using RenderableString, it will always specify the charset. --- .../wisdom/api/bodies/RenderableString.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java b/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java index 81df917d9..bc50f4daa 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java +++ b/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java @@ -1,16 +1,15 @@ package org.wisdom.api.bodies; -import com.google.common.collect.ImmutableList; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.Charset; + import org.wisdom.api.http.Context; import org.wisdom.api.http.MimeTypes; import org.wisdom.api.http.Renderable; import org.wisdom.api.http.Result; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.StringWriter; -import java.util.List; - /** * A renderable object taking a String as parameter. */ @@ -52,7 +51,13 @@ public RenderableString(String object, String type) { @Override public InputStream render(Context context, Result result) throws Exception { - return new ByteArrayInputStream(rendered.getBytes()); + //Force to specify the platform default encoding to avoid platform dependent encoding + Charset charset = result.getCharset(); + if(charset==null){ + charset = Charset.defaultCharset(); + result.with(charset); + } + return new ByteArrayInputStream(rendered.getBytes(result.getCharset())); } @Override From f4010021d475fb8707191eeb9b5f0c08ca0d0137 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:07:40 +0100 Subject: [PATCH 02/35] Add ContentEncoder interface A ContentEncoder encodes an InputStream to a given compression format --- .../org/wisdom/api/content/ContentEncoder.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java new file mode 100644 index 000000000..dac20e239 --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java @@ -0,0 +1,14 @@ +package org.wisdom.api.content; + +import java.io.IOException; +import java.io.InputStream; + +public interface ContentEncoder { + + public InputStream encode(InputStream toEncode) throws IOException; + + public String getEncodingType(); + + public String getContentEncodingHeaderValue(); + +} From d773dc212530196c2957abeaeb2153925cc773b7 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:08:28 +0100 Subject: [PATCH 03/35] Add content encoders for wisdom - GZIP - DEFLATE - IDENTITY --- .../content/encoders/DeflateEncoder.java | 47 +++++++++++++++++++ .../wisdom/content/encoders/GzipEncoder.java | 47 +++++++++++++++++++ .../content/encoders/IdentityEncoder.java | 31 ++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java create mode 100644 content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java create mode 100644 content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java new file mode 100644 index 000000000..6dda8c561 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java @@ -0,0 +1,47 @@ +package org.wisdom.content.encoders; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.DeflaterOutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.felix.ipojo.annotations.Component; +import org.apache.felix.ipojo.annotations.Instantiate; +import org.apache.felix.ipojo.annotations.Provides; +import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.http.EncodingNames; + +@Component +@Instantiate +@Provides +public class DeflateEncoder implements ContentEncoder { + + @Override + public InputStream encode(InputStream toEncode) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + OutputStream gzout = new DeflaterOutputStream(bout); + gzout.write(IOUtils.toByteArray(toEncode)); + gzout.flush(); + gzout.close(); + toEncode.close(); + + bout.flush(); + InputStream encoded = new ByteArrayInputStream(bout.toByteArray()); + bout.close(); + return encoded; + } + + @Override + public String getEncodingType() { + return EncodingNames.DEFLATE; + } + + @Override + public String getContentEncodingHeaderValue() { + return EncodingNames.DEFLATE; + } +} diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java new file mode 100644 index 000000000..592552832 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java @@ -0,0 +1,47 @@ +package org.wisdom.content.encoders; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.felix.ipojo.annotations.Component; +import org.apache.felix.ipojo.annotations.Instantiate; +import org.apache.felix.ipojo.annotations.Provides; +import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.http.EncodingNames; + +@Component +@Instantiate +@Provides +public class GzipEncoder implements ContentEncoder { + + @Override + public InputStream encode(InputStream toEncode) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + OutputStream gzout = new GZIPOutputStream(bout); + gzout.write(IOUtils.toByteArray(toEncode)); + gzout.flush(); + gzout.close(); + toEncode.close(); + + bout.flush(); + InputStream encoded = new ByteArrayInputStream(bout.toByteArray()); + bout.close(); + return encoded; + } + + @Override + public String getEncodingType(){ + return EncodingNames.GZIP; + } + + @Override + public String getContentEncodingHeaderValue() { + return EncodingNames.GZIP; + } +} diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java new file mode 100644 index 000000000..c03c1b416 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java @@ -0,0 +1,31 @@ +package org.wisdom.content.encoders; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.felix.ipojo.annotations.Component; +import org.apache.felix.ipojo.annotations.Instantiate; +import org.apache.felix.ipojo.annotations.Provides; +import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.http.EncodingNames; + +@Component +@Instantiate +@Provides +public class IdentityEncoder implements ContentEncoder { + + @Override + public InputStream encode(InputStream toEncode) throws IOException { + return toEncode; + } + + @Override + public String getEncodingType() { + return EncodingNames.IDENTITY; + } + + @Override + public String getContentEncodingHeaderValue() { + return null; + } +} From c1ab9d3641225559572e2d6bd98c8163ad26a1aa Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:08:57 +0100 Subject: [PATCH 04/35] Add EncodingNames with constants for encoding methods --- .../java/org/wisdom/api/http/EncodingNames.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 wisdom-api/src/main/java/org/wisdom/api/http/EncodingNames.java diff --git a/wisdom-api/src/main/java/org/wisdom/api/http/EncodingNames.java b/wisdom-api/src/main/java/org/wisdom/api/http/EncodingNames.java new file mode 100644 index 000000000..bbd34353c --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/http/EncodingNames.java @@ -0,0 +1,14 @@ +package org.wisdom.api.http; + +/** + * Defines all standard encoding methods. + */ +public interface EncodingNames { + + String GZIP = "gzip"; + String COMPRESS = "compress"; + String DEFLATE = "deflate"; + String IDENTITY = "identity"; + + String[] ALL_ENCODINGS = {IDENTITY, COMPRESS, DEFLATE, GZIP}; +} From 08857c7f7f803709c3089616b63dec9938fbd83f Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:09:31 +0100 Subject: [PATCH 05/35] Add ContentEncoder methods to ContentEngine --- .../java/org/wisdom/content/engines/Engine.java | 13 +++++++++++++ .../java/org/wisdom/api/content/ContentEngine.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java index aa41d6bb0..fe5ed5d80 100644 --- a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java +++ b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java @@ -5,6 +5,7 @@ import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.content.BodyParser; +import org.wisdom.api.content.ContentEncoder; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.slf4j.LoggerFactory; @@ -23,6 +24,8 @@ public class Engine implements ContentEngine { List parsers; @Requires(specification = ContentSerializer.class, optional = true) List serializers; + @Requires(specification = ContentEncoder.class, optional = true) + List encoders; @Override public BodyParser getBodyParserEngineForContentType(String contentType) { @@ -45,4 +48,14 @@ public ContentSerializer getContentSerializerForContentType(String contentType) LoggerFactory.getLogger(this.getClass()).info("Cannot find a content renderer handling " + contentType); return null; } + + @Override + public ContentEncoder getContentEncoderForEncodingType(String encoding) { + for (ContentEncoder encoder : encoders) { + if (encoder.getEncodingType().equals(encoding)) { + return encoder; + } + } + return null; + } } diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java index c1db52772..c83a55430 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java @@ -5,5 +5,5 @@ public interface ContentEngine { BodyParser getBodyParserEngineForContentType(String contentType); ContentSerializer getContentSerializerForContentType(String contentType); - + ContentEncoder getContentEncoderForEncodingType(String type); } From a6e0ed7233d1cd58aa5005e0c45891f224f9a19b Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:10:18 +0100 Subject: [PATCH 06/35] Add a configuration key to enable / disable encoding globally Default is enabled --- .../wisdom/api/configuration/ApplicationConfiguration.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java index 59ed69df7..31e3d5f6e 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java @@ -29,6 +29,11 @@ public interface ApplicationConfiguration { * The HTTPS port key. */ String HTTPS_PORT = "https.port"; + + /** + * The global encoding activation key. + */ + String ENCODING_GLOBAL = "encoding.global"; /** From a696344cce916a9e33360f739495b776052e6528 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:11:17 +0100 Subject: [PATCH 07/35] Adding EncodingHelper EncodingHelp allows to parse the Accept_Encoding header and sort the encoding methods according (mostly) to HTTP/1.1 --- .../org/wisdom/api/utils/EncodingHelper.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java diff --git a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java new file mode 100644 index 000000000..2e9f1bd85 --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java @@ -0,0 +1,81 @@ +package org.wisdom.api.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.wisdom.api.http.EncodingNames; + +public class EncodingHelper { + + /** + * Parse a string to return an ordered list according to the Accept_Encoding HTTP Header grammar + * @param acceptEncoding String to parse. Should be an Accept_Encoding header + * @return An ordered list of encodings + */ + public static List parseAcceptEncodingHeader(String acceptEncoding){ + List result = new ArrayList(); + List tmp = new ArrayList(); + //Empty or null Accept_Encoding + if(acceptEncoding == null || acceptEncoding.trim().equals("") || acceptEncoding.trim().equals("\n")){ + return result; + } + //TODO That's not the real meaning of "*" + /* + The special "*" symbol in an Accept-Encoding field matches any + available content-coding not explicitly listed in the header + field. + */ + if(acceptEncoding.trim().equals("*")){ + for(String encoding : EncodingNames.ALL_ENCODINGS) + result.add(encoding); + return result; + } + + String[] encodingItems = acceptEncoding.split(","); + + int position = 0; + for(String encodingItem : encodingItems){ + ValuedEncoding encoding = new ValuedEncoding(encodingItem, position++); + //Remove 0 qValued encodings + if(encoding.qValue > 0) + tmp.add(encoding); + } + Collections.sort(tmp); + + for(ValuedEncoding encoding : tmp){ + result.add(encoding.encoding); + } + + return result; + } +} + +class ValuedEncoding implements Comparable{ + String encoding = null; + Double qValue = 1.0; //TODO I am not sure that no qValue means 1.0 + Integer position; + + public ValuedEncoding(String encodingItem, int position){ + this.position = position; + //Split an encoding item between encoding and its qValue + String[] encodingParts = encodingItem.split(";"); + //Grab encoding name + encoding = encodingParts[0].trim().replace("\n", ""); + //Grab encoding's qValue if it exists (default 1.0 otherwise) + if(encodingParts.length > 1){ + qValue = Double.parseDouble(encodingParts[1].trim().replace("\n", "")); + } + } + + @Override + public int compareTo(ValuedEncoding o) { + if(qValue.equals(o.qValue)){ + // TODO not sure its true according to the standard + // In case 2 encodings have the same qValue, the first one has priority + return position.compareTo(o.position); + } + //Highest qValue first, invert default ascending comparison + return qValue.compareTo(o.qValue) * -1; + } +} From 2532659fbdecc80cd2a2f7c3ce181e7f5aa05a24 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 00:12:36 +0100 Subject: [PATCH 08/35] Add encoding handling to the result processing --- .../wisdom/engine/server/WisdomHandler.java | 97 +++++++++++++++---- 1 file changed, 77 insertions(+), 20 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 4086979cd..97f996a4c 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -1,24 +1,42 @@ package org.wisdom.engine.server; -import akka.dispatch.OnComplete; +import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_ENCODING; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaders.Names.HOST; +import static io.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE; +import static io.netty.handler.codec.http.HttpHeaders.Names.TRANSFER_ENCODING; import io.netty.buffer.Unpooled; -import io.netty.channel.*; -import io.netty.handler.codec.http.*; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.multipart.*; -import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.DiskAttribute; +import io.netty.handler.codec.http.multipart.DiskFileUpload; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.handler.stream.ChunkedStream; -import org.apache.commons.io.IOUtils; -import org.wisdom.api.bodies.NoHttpBody; -import org.wisdom.api.content.ContentSerializer; -import org.wisdom.api.error.ErrorHandler; -import org.wisdom.api.http.*; -import org.wisdom.api.router.Route; -import org.wisdom.engine.wrapper.ContextFromNetty; -import org.wisdom.engine.wrapper.cookies.CookieHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import scala.concurrent.Future; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -27,8 +45,27 @@ import java.net.URISyntaxException; import java.util.Map; -import static io.netty.handler.codec.http.HttpHeaders.Names.*; -import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wisdom.api.bodies.NoHttpBody; +import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentSerializer; +import org.wisdom.api.error.ErrorHandler; +import org.wisdom.api.http.AsyncResult; +import org.wisdom.api.http.Context; +import org.wisdom.api.http.HeaderNames; +import org.wisdom.api.http.Renderable; +import org.wisdom.api.http.Result; +import org.wisdom.api.http.Results; +import org.wisdom.api.router.Route; +import org.wisdom.api.utils.EncodingHelper; +import org.wisdom.engine.wrapper.ContextFromNetty; +import org.wisdom.engine.wrapper.cookies.CookieHelper; + +import scala.concurrent.Future; +import akka.dispatch.OnComplete; /** * The Wisdom Channel Handler. @@ -262,7 +299,8 @@ public void onComplete(Throwable failure, Result result) { } private InputStream processResult(Result result) throws Exception { - Renderable renderable = result.getRenderable(); + Renderable renderable = result.getRenderable(); + if (renderable == null) { renderable = new NoHttpBody(); } @@ -283,7 +321,26 @@ private InputStream processResult(Result result) throws Exception { serializer.serialize(renderable); } } - return renderable.render(context, result); + + InputStream processedResult = renderable.render(context, result); + + //TODO The default configuration (true here) should be discussed + //TODO We need annotations to activate / desactivate encoding on route / controllers + if(accessor.configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true)){ + ContentEncoder encoder = null; + + for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ + encoder = accessor.content_engines.getContentEncoderForEncodingType(encoding); + if(encoder != null) + break; + } + + if(encoder != null){ + processedResult = encoder.encode(processedResult); + result.with(CONTENT_ENCODING, encoder.getEncodingType()); + } + } + return processedResult; } private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest request, Context context, From 062cc9a8feab3fce7ba9bbfff355941ffd5df9f9 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 09:54:21 +0100 Subject: [PATCH 09/35] RenderableString.render was not passing UT Result parameter as null was not handled --- .../wisdom/api/bodies/RenderableString.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java b/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java index bc50f4daa..186da69e4 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java +++ b/wisdom-api/src/main/java/org/wisdom/api/bodies/RenderableString.java @@ -51,13 +51,19 @@ public RenderableString(String object, String type) { @Override public InputStream render(Context context, Result result) throws Exception { - //Force to specify the platform default encoding to avoid platform dependent encoding - Charset charset = result.getCharset(); - if(charset==null){ - charset = Charset.defaultCharset(); - result.with(charset); + byte[] bytes = null; + + if(result != null){ // We have a result, charset have to be provided + if(result.getCharset() == null){ // No charset provided + result.with(Charset.defaultCharset()); // Set the default encoding + } + bytes = rendered.getBytes(result.getCharset()); + }else{ + //No Result, use the default platform encoding + bytes = rendered.getBytes(); } - return new ByteArrayInputStream(rendered.getBytes(result.getCharset())); + + return new ByteArrayInputStream(bytes); } @Override From 31b5999507bb31d65548dfeffc40cedfe71205c5 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 17:34:10 +0100 Subject: [PATCH 10/35] Add @AllowEncoding and @DenyEncoding These annotations allow to override the application configuration regarding content encoding. They can be set both on class & methods --- .../api/annotations/encoder/AllowEncoding.java | 12 ++++++++++++ .../wisdom/api/annotations/encoder/DenyEncoding.java | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java create mode 100644 wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/DenyEncoding.java diff --git a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java new file mode 100644 index 000000000..b3bd53cfa --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java @@ -0,0 +1,12 @@ +package org.wisdom.api.annotations.encoder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowEncoding { + +} diff --git a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/DenyEncoding.java b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/DenyEncoding.java new file mode 100644 index 000000000..4cc519bc8 --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/DenyEncoding.java @@ -0,0 +1,12 @@ +package org.wisdom.api.annotations.encoder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DenyEncoding { + +} From 13883e4b9fca6b3d0f10aef44f67f52e2e27675d Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 17:34:48 +0100 Subject: [PATCH 11/35] Refactor process to decide whether to encode response content or not --- .../wisdom/engine/server/WisdomHandler.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 97f996a4c..03435fefd 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -48,6 +48,8 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.annotations.encoder.DenyEncoding; import org.wisdom.api.bodies.NoHttpBody; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.content.ContentEncoder; @@ -326,7 +328,7 @@ private InputStream processResult(Result result) throws Exception { //TODO The default configuration (true here) should be discussed //TODO We need annotations to activate / desactivate encoding on route / controllers - if(accessor.configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true)){ + if(shouldEncode(processedResult)){ ContentEncoder encoder = null; for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ @@ -342,7 +344,36 @@ private InputStream processResult(Result result) throws Exception { } return processedResult; } - + + private boolean shouldEncode(InputStream processedResult){ + //TODO filter on max size + boolean configuration = accessor.configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true); + boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; + + if(context.getRoute() != null){ // + isAllowOnMethod = context.getRoute().getControllerMethod().getAnnotation(AllowEncoding.class) == null ? false : true; + isDenyOnMethod = context.getRoute().getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; + isAllowOnController = context.getRoute().getControllerClass().getAnnotation(AllowEncoding.class) == null ? false : true; + isDenyOnController = context.getRoute().getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; + } + + if(configuration){ // Configuration tells yes + if(isDenyOnMethod) // Method tells no + return false; + if(isDenyOnController && !isAllowOnMethod) // Class tells no & Method doesn't tell yes + return false; + + return true; + }else{ // Configuration tells no + if(isAllowOnMethod) // Method tells yes + return true; + if(isAllowOnController && !isDenyOnMethod) // Class tells yes & Method doesn't tell no + return true; + + return false; + } + } + private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest request, Context context, Result result, boolean handleFlashAndSessionCookie) { From 517c9ea521b03e45553c2996885ecc739d8e4096 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 22:59:04 +0100 Subject: [PATCH 12/35] add size fields on @AllowEncoding annotations * maxSize is the upper boundary for encoding * minSize is the lower boundary for encoding --- .../java/org/wisdom/api/annotations/encoder/AllowEncoding.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java index b3bd53cfa..a10ce0da7 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java +++ b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java @@ -8,5 +8,6 @@ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AllowEncoding { - + int maxSize() default -1; + int minSize() default -1; } From f885c8f988eacdff32eb43bcb436dadf7c9fe7bf Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 23:00:19 +0100 Subject: [PATCH 13/35] Add configuration keys encoding upper and lower boundaries + default values --- .../configuration/ApplicationConfiguration.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java index 31e3d5f6e..d1e68b3e3 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java @@ -34,8 +34,21 @@ public interface ApplicationConfiguration { * The global encoding activation key. */ String ENCODING_GLOBAL = "encoding.global"; + + /** + * The global encoding max activation size. + */ + String ENCODING_MAX_SIZE = "encoding.max.size"; - + /** + * The global encoding min activation size. + */ + String ENCODING_MIN_SIZE = "encoding.min.size"; + + int DEFAULT_ENCODING_MAX_SIZE = 10000 * 1024; // 10Mo + + int DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko + /** * Gets the base directory of the Wisdom application. * @return the base directory From 331b2a58672ae8dcc82011b4a933849648d453c5 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 14 Jan 2014 23:01:35 +0100 Subject: [PATCH 14/35] Adding size computation to shouldEncode * if renderable.length is not in encoding boundaries, abort encoding * shouldEncode now take a renderable as parameter, preparing for filter on extensions --- .../wisdom/engine/server/WisdomHandler.java | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 03435fefd..fb9270b65 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -327,8 +327,7 @@ private InputStream processResult(Result result) throws Exception { InputStream processedResult = renderable.render(context, result); //TODO The default configuration (true here) should be discussed - //TODO We need annotations to activate / desactivate encoding on route / controllers - if(shouldEncode(processedResult)){ + if(shouldEncode(renderable)){ ContentEncoder encoder = null; for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ @@ -345,18 +344,54 @@ private InputStream processResult(Result result) throws Exception { return processedResult; } - private boolean shouldEncode(InputStream processedResult){ - //TODO filter on max size + private boolean shouldEncode(Renderable renderable){ + long renderableLength = renderable.length(); + + //TODO Maybe we should continue but abort size lookup for size == -1 + if(renderableLength <= 0) + return false; + + //TODO Discuss default max size + //TODO Do not request config value each time + int confMaxSize = accessor.configuration.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MAX_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); + int methodMaxSize = -1, controllerMaxSize = -1; + + //TODO Discuss default min size + //TODO Do not request config value each time + int confMinSize = accessor.configuration.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MIN_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); + int methodMinSize = -1, controllerMinSize = -1; + + //TODO Filter by extensions ? + + //TODO Do not request config value each time boolean configuration = accessor.configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true); boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; - if(context.getRoute() != null){ // - isAllowOnMethod = context.getRoute().getControllerMethod().getAnnotation(AllowEncoding.class) == null ? false : true; + if(context.getRoute() != null){ + //Retrieve @AllowEncoding on route method and, if exists, set a flag, compute max & min size + AllowEncoding allowOnMethod = context.getRoute().getControllerMethod().getAnnotation(AllowEncoding.class); + isAllowOnMethod = allowOnMethod == null ? false : true; + methodMaxSize = isAllowOnMethod ? allowOnMethod.maxSize() : -1; + methodMinSize = isAllowOnMethod ? allowOnMethod.minSize() : -1; + //Retrieve @AllowEncoding on route controller and, if exists, set a flag, compute max & min size + AllowEncoding allowOnController = context.getRoute().getControllerClass().getAnnotation(AllowEncoding.class); + isAllowOnController = allowOnController == null ? false : true; + controllerMaxSize = isAllowOnController ? allowOnController.maxSize() : -1; + controllerMinSize = isAllowOnController ? allowOnController.minSize() : -1; + //Retrieve @DenyEncoding on route method and route controller and set a flag isDenyOnMethod = context.getRoute().getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; - isAllowOnController = context.getRoute().getControllerClass().getAnnotation(AllowEncoding.class) == null ? false : true; isDenyOnController = context.getRoute().getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; } + // Find max size first on method, then on controller and, if none, use default + int maxSize = methodMaxSize != -1 ? methodMaxSize : controllerMaxSize != -1 ? controllerMaxSize : confMaxSize; + // Find min size first on method, then on controller and, if none, use default + int minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; + + // Ensure renderableLength is in min - max boundaries + if(renderableLength > maxSize || renderableLength < minSize) + return false; + if(configuration){ // Configuration tells yes if(isDenyOnMethod) // Method tells no return false; From 10db4762c86786a32ac14c96303cfb96edeb9623 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 10:18:35 +0100 Subject: [PATCH 15/35] Refactoring encoders to codecs --- ...{DeflateEncoder.java => DeflateCodec.java} | 21 +++++++++++++------ .../{GzipEncoder.java => GzipCodec.java} | 10 +++++++-- ...dentityEncoder.java => IdentityCodec.java} | 9 ++++++-- .../org/wisdom/content/engines/Engine.java | 10 ++++----- ...{ContentEncoder.java => ContentCodec.java} | 4 +++- .../org/wisdom/api/content/ContentEngine.java | 2 +- .../wisdom/engine/server/WisdomHandler.java | 4 ++-- 7 files changed, 41 insertions(+), 19 deletions(-) rename content-manager/src/main/java/org/wisdom/content/encoders/{DeflateEncoder.java => DeflateCodec.java} (64%) rename content-manager/src/main/java/org/wisdom/content/encoders/{GzipEncoder.java => GzipCodec.java} (81%) rename content-manager/src/main/java/org/wisdom/content/encoders/{IdentityEncoder.java => IdentityCodec.java} (74%) rename wisdom-api/src/main/java/org/wisdom/api/content/{ContentEncoder.java => ContentCodec.java} (70%) diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java similarity index 64% rename from content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java rename to content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java index 6dda8c561..66b4a4c9f 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/DeflateEncoder.java +++ b/content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java @@ -5,28 +5,32 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; import org.apache.commons.io.IOUtils; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; -import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; @Component @Instantiate @Provides -public class DeflateEncoder implements ContentEncoder { +public class DeflateCodec implements ContentCodec { + //TODO What level to set Deflater codec ? + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); @Override public InputStream encode(InputStream toEncode) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); - OutputStream gzout = new DeflaterOutputStream(bout); - gzout.write(IOUtils.toByteArray(toEncode)); - gzout.flush(); - gzout.close(); + OutputStream dout = new DeflaterOutputStream(bout); + dout.write(IOUtils.toByteArray(toEncode)); + dout.flush(); + dout.close(); toEncode.close(); bout.flush(); @@ -34,6 +38,11 @@ public InputStream encode(InputStream toEncode) throws IOException { bout.close(); return encoded; } + + @Override + public InputStream decode(InputStream toDecode) throws IOException { + return new InflaterInputStream(toDecode); + } @Override public String getEncodingType() { diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java similarity index 81% rename from content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java rename to content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java index 592552832..32787ff4b 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/GzipEncoder.java +++ b/content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java @@ -5,19 +5,20 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.apache.commons.io.IOUtils; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; -import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; @Component @Instantiate @Provides -public class GzipEncoder implements ContentEncoder { +public class GzipCodec implements ContentCodec { @Override public InputStream encode(InputStream toEncode) throws IOException { @@ -35,6 +36,11 @@ public InputStream encode(InputStream toEncode) throws IOException { return encoded; } + @Override + public InputStream decode(InputStream toDecode) throws IOException{ + return new GZIPInputStream(toDecode); + } + @Override public String getEncodingType(){ return EncodingNames.GZIP; diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java b/content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java similarity index 74% rename from content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java rename to content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java index c03c1b416..4b8a6c9c1 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/IdentityEncoder.java +++ b/content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java @@ -6,18 +6,23 @@ import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; -import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; @Component @Instantiate @Provides -public class IdentityEncoder implements ContentEncoder { +public class IdentityCodec implements ContentCodec { @Override public InputStream encode(InputStream toEncode) throws IOException { return toEncode; } + + @Override + public InputStream decode(InputStream toDecode) throws IOException { + return toDecode; + } @Override public String getEncodingType() { diff --git a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java index fe5ed5d80..90586d603 100644 --- a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java +++ b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java @@ -5,7 +5,7 @@ import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.content.BodyParser; -import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentCodec; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.slf4j.LoggerFactory; @@ -24,8 +24,8 @@ public class Engine implements ContentEngine { List parsers; @Requires(specification = ContentSerializer.class, optional = true) List serializers; - @Requires(specification = ContentEncoder.class, optional = true) - List encoders; + @Requires(specification = ContentCodec.class, optional = true) + List encoders; @Override public BodyParser getBodyParserEngineForContentType(String contentType) { @@ -50,8 +50,8 @@ public ContentSerializer getContentSerializerForContentType(String contentType) } @Override - public ContentEncoder getContentEncoderForEncodingType(String encoding) { - for (ContentEncoder encoder : encoders) { + public ContentCodec getContentEncoderForEncodingType(String encoding) { + for (ContentCodec encoder : encoders) { if (encoder.getEncodingType().equals(encoding)) { return encoder; } diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java similarity index 70% rename from wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java rename to wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java index dac20e239..c6b9697e2 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncoder.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java @@ -3,10 +3,12 @@ import java.io.IOException; import java.io.InputStream; -public interface ContentEncoder { +public interface ContentCodec { public InputStream encode(InputStream toEncode) throws IOException; + public InputStream decode(InputStream toDecode) throws IOException; + public String getEncodingType(); public String getContentEncodingHeaderValue(); diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java index c83a55430..de840a2a4 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java @@ -5,5 +5,5 @@ public interface ContentEngine { BodyParser getBodyParserEngineForContentType(String contentType); ContentSerializer getContentSerializerForContentType(String contentType); - ContentEncoder getContentEncoderForEncodingType(String type); + ContentCodec getContentEncoderForEncodingType(String type); } diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index fb9270b65..62f07da2e 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -52,7 +52,7 @@ import org.wisdom.api.annotations.encoder.DenyEncoding; import org.wisdom.api.bodies.NoHttpBody; import org.wisdom.api.configuration.ApplicationConfiguration; -import org.wisdom.api.content.ContentEncoder; +import org.wisdom.api.content.ContentCodec; import org.wisdom.api.content.ContentSerializer; import org.wisdom.api.error.ErrorHandler; import org.wisdom.api.http.AsyncResult; @@ -328,7 +328,7 @@ private InputStream processResult(Result result) throws Exception { //TODO The default configuration (true here) should be discussed if(shouldEncode(renderable)){ - ContentEncoder encoder = null; + ContentCodec encoder = null; for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ encoder = accessor.content_engines.getContentEncoderForEncodingType(encoding); From 6520fdd5eb0b8c45c3c2fed567ae712ed0862629 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 10:18:48 +0100 Subject: [PATCH 16/35] Adding unit tests for codecs --- .../wisdom/content/encoders/CodecsTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java b/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java new file mode 100644 index 000000000..d0511de75 --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java @@ -0,0 +1,76 @@ +package org.wisdom.content.encoders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.wisdom.api.content.ContentCodec; +import org.wisdom.api.http.EncodingNames; + +public class CodecsTest { + + @Test + public void testGzipCodec(){ + ContentCodec codec = testCodec(GzipCodec.class); + if(codec == null) + return; + + assertThat(codec.getContentEncodingHeaderValue()).isEqualTo(EncodingNames.GZIP); + assertThat(codec.getEncodingType()).isEqualTo(EncodingNames.GZIP); + } + + @Test + public void testDeflateCodec(){ + ContentCodec codec = testCodec(DeflateCodec.class); + if(codec == null) + return; + + assertThat(codec.getContentEncodingHeaderValue()).isEqualTo(EncodingNames.DEFLATE); + assertThat(codec.getEncodingType()).isEqualTo(EncodingNames.DEFLATE); + } + + @Test + public void testIdentityCodec(){ + ContentCodec codec = testCodec(IdentityCodec.class); + if(codec == null) + return; + + assertThat(codec.getContentEncodingHeaderValue()).isEqualTo(null); + assertThat(codec.getEncodingType()).isEqualTo(EncodingNames.IDENTITY); + } + + private ContentCodec testCodec(Class codecClass) { + String data = "abcdefghijklmonpqrstuvwxyz"; + + ContentCodec codec; + try { + codec = codecClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e1) { + fail("Unable to instantiate " + codecClass); + e1.printStackTrace(); + return null; + } + + InputStream inputData, encodedData = null, decodedData = null; + inputData = IOUtils.toInputStream(data); + + try { + encodedData = codec.encode(inputData); + decodedData = codec.decode(encodedData); + assertThat(IOUtils.toString(decodedData)).isEqualTo(data); + } catch (IOException e) { + assertThat(false); + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(inputData); + IOUtils.closeQuietly(encodedData); + IOUtils.closeQuietly(decodedData); + } + + return codec; + } +} From 5abf514d240961f55f47e4114312a0e8dce034b0 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 11:36:44 +0100 Subject: [PATCH 17/35] Finalizing encoders to codecs refactoring Changing package names to org.wisdom.content.codecs --- .../org/wisdom/content/{encoders => codecs}/DeflateCodec.java | 2 +- .../org/wisdom/content/{encoders => codecs}/GzipCodec.java | 2 +- .../org/wisdom/content/{encoders => codecs}/IdentityCodec.java | 2 +- .../src/test/java/org/wisdom/content/encoders/CodecsTest.java | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) rename content-manager/src/main/java/org/wisdom/content/{encoders => codecs}/DeflateCodec.java (97%) rename content-manager/src/main/java/org/wisdom/content/{encoders => codecs}/GzipCodec.java (97%) rename content-manager/src/main/java/org/wisdom/content/{encoders => codecs}/IdentityCodec.java (95%) diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java similarity index 97% rename from content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java rename to content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java index 66b4a4c9f..fd46b31c6 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/DeflateCodec.java +++ b/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java @@ -1,4 +1,4 @@ -package org.wisdom.content.encoders; +package org.wisdom.content.codecs; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java similarity index 97% rename from content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java rename to content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java index 32787ff4b..353b9b651 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/GzipCodec.java +++ b/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java @@ -1,4 +1,4 @@ -package org.wisdom.content.encoders; +package org.wisdom.content.codecs; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java similarity index 95% rename from content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java rename to content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java index 4b8a6c9c1..a4e2aabbd 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoders/IdentityCodec.java +++ b/content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java @@ -1,4 +1,4 @@ -package org.wisdom.content.encoders; +package org.wisdom.content.codecs; import java.io.IOException; import java.io.InputStream; diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java b/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java index d0511de75..c469d2fbc 100644 --- a/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java +++ b/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java @@ -10,6 +10,9 @@ import org.junit.Test; import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; +import org.wisdom.content.codecs.DeflateCodec; +import org.wisdom.content.codecs.GzipCodec; +import org.wisdom.content.codecs.IdentityCodec; public class CodecsTest { From edfff79b35ac6342b93f1345ab918f2df8881043 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 15:21:33 +0100 Subject: [PATCH 18/35] Codecs refactoring * Adding AbstractDefInfCodec for Gzip and Deflate * Adding in file documentation --- .../content/codecs/AbstractDefInfCodec.java | 84 +++++++++++++++++++ .../wisdom/content/codecs/DeflateCodec.java | 43 +++------- .../org/wisdom/content/codecs/GzipCodec.java | 42 +++------- .../org/wisdom/api/content/ContentCodec.java | 22 ++++- 4 files changed, 129 insertions(+), 62 deletions(-) create mode 100644 content-manager/src/main/java/org/wisdom/content/codecs/AbstractDefInfCodec.java diff --git a/content-manager/src/main/java/org/wisdom/content/codecs/AbstractDefInfCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/AbstractDefInfCodec.java new file mode 100644 index 000000000..2c4217547 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/codecs/AbstractDefInfCodec.java @@ -0,0 +1,84 @@ +package org.wisdom.content.codecs; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.io.IOUtils; +import org.wisdom.api.content.ContentCodec; + +/** + * Abstract codec using an {@link DeflaterOutputStream} instance to encode and {@link InflaterInputStream} instance to decode. + * Subclasses of this two classes can also be used, for instance ({@link GZIPOutputStream and {@link GZIPInputStream}} + *
+ * Subclasses should implements {@link #getEncoderClass} and {@link #getDecoderClass} to return the chosen encoder classes. + *
+ * @see ContentCodec + */ +public abstract class AbstractDefInfCodec implements ContentCodec { + + @Override + public InputStream encode(InputStream toEncode) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + OutputStream encoderout; + try { + encoderout = getEncoderClass().getConstructor(OutputStream.class).newInstance(bout); + encoderout.write(IOUtils.toByteArray(toEncode)); + encoderout.flush(); + encoderout.close(); + } + catch (InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + //TODO notify encoding has not been done + return toEncode; + } + + toEncode.close(); + + bout.flush(); + InputStream encoded = new ByteArrayInputStream(bout.toByteArray()); + bout.close(); + return encoded; + } + + @Override + public InputStream decode(InputStream toDecode) throws IOException { + InputStream decoderin; + try { + decoderin = getDecoderClass().getConstructor(InputStream.class).newInstance(toDecode); + } catch (InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + //TODO notify encoding has not been done + return toDecode; + } + return decoderin; + } + + @Override + public abstract String getEncodingType(); + + @Override + public abstract String getContentEncodingHeaderValue(); + + /** + * @return Encoder class the codec use to encode data + */ + public abstract Class getEncoderClass(); + + /** + * @return Decoder class the codec use to decode data + */ + public abstract Class getDecoderClass(); +} diff --git a/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java index fd46b31c6..cc2b25fe4 100644 --- a/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java +++ b/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java @@ -1,48 +1,17 @@ package org.wisdom.content.codecs; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; -import org.apache.commons.io.IOUtils; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; -import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; @Component @Instantiate @Provides -public class DeflateCodec implements ContentCodec { - //TODO What level to set Deflater codec ? - Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); - - @Override - public InputStream encode(InputStream toEncode) throws IOException { - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - - OutputStream dout = new DeflaterOutputStream(bout); - dout.write(IOUtils.toByteArray(toEncode)); - dout.flush(); - dout.close(); - toEncode.close(); - - bout.flush(); - InputStream encoded = new ByteArrayInputStream(bout.toByteArray()); - bout.close(); - return encoded; - } - - @Override - public InputStream decode(InputStream toDecode) throws IOException { - return new InflaterInputStream(toDecode); - } +public class DeflateCodec extends AbstractDefInfCodec { @Override public String getEncodingType() { @@ -53,4 +22,14 @@ public String getEncodingType() { public String getContentEncodingHeaderValue() { return EncodingNames.DEFLATE; } + + @Override + public Class getEncoderClass() { + return DeflaterOutputStream.class; + } + + @Override + public Class getDecoderClass() { + return InflaterInputStream.class; + } } diff --git a/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java index 353b9b651..36ba7e212 100644 --- a/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java +++ b/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java @@ -1,45 +1,19 @@ package org.wisdom.content.codecs; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import java.util.zip.InflaterInputStream; -import org.apache.commons.io.IOUtils; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; -import org.wisdom.api.content.ContentCodec; import org.wisdom.api.http.EncodingNames; @Component @Instantiate @Provides -public class GzipCodec implements ContentCodec { - - @Override - public InputStream encode(InputStream toEncode) throws IOException { - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - - OutputStream gzout = new GZIPOutputStream(bout); - gzout.write(IOUtils.toByteArray(toEncode)); - gzout.flush(); - gzout.close(); - toEncode.close(); - - bout.flush(); - InputStream encoded = new ByteArrayInputStream(bout.toByteArray()); - bout.close(); - return encoded; - } - - @Override - public InputStream decode(InputStream toDecode) throws IOException{ - return new GZIPInputStream(toDecode); - } +public class GzipCodec extends AbstractDefInfCodec { @Override public String getEncodingType(){ @@ -50,4 +24,14 @@ public String getEncodingType(){ public String getContentEncodingHeaderValue() { return EncodingNames.GZIP; } + + @Override + public Class getEncoderClass() { + return GZIPOutputStream.class; + } + + @Override + public Class getDecoderClass() { + return GZIPInputStream.class; + } } diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java index c6b9697e2..2c05110bb 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java @@ -3,14 +3,34 @@ import java.io.IOException; import java.io.InputStream; +/** + * Service exposed by content manager allowing data, as InputStream, to be processed to and from a given compression format. + */ public interface ContentCodec { + /** + * Encode data to this codec format + * @param toEncode Data to encode + * @return Encoded data + * @throws IOException + */ public InputStream encode(InputStream toEncode) throws IOException; + /** + * Decode data to this codec format + * @param toDecode Data to decode + * @return Decoded data + * @throws IOException + */ public InputStream decode(InputStream toDecode) throws IOException; + /** + * @return Standard name for this encoding, according to RFC2616 + */ public String getEncodingType(); - + /** + * @return Encoding_Type content used for this encoding, according to RFC2616 + */ public String getContentEncodingHeaderValue(); } From 845eccbf61528451e98539d4519c2168451d4ff2 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 23:15:16 +0100 Subject: [PATCH 19/35] Refactoring shouldEncode Moving shouldEncode from WisdomHandler to EncodingHelper --- .../wisdom/engine/server/WisdomHandler.java | 68 +------------------ 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 62f07da2e..dfc0a3027 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -326,8 +326,7 @@ private InputStream processResult(Result result) throws Exception { InputStream processedResult = renderable.render(context, result); - //TODO The default configuration (true here) should be discussed - if(shouldEncode(renderable)){ + if(EncodingHelper.shouldEncode(renderable, accessor.configuration, context.getRoute())){ ContentCodec encoder = null; for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ @@ -344,71 +343,6 @@ private InputStream processResult(Result result) throws Exception { return processedResult; } - private boolean shouldEncode(Renderable renderable){ - long renderableLength = renderable.length(); - - //TODO Maybe we should continue but abort size lookup for size == -1 - if(renderableLength <= 0) - return false; - - //TODO Discuss default max size - //TODO Do not request config value each time - int confMaxSize = accessor.configuration.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MAX_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); - int methodMaxSize = -1, controllerMaxSize = -1; - - //TODO Discuss default min size - //TODO Do not request config value each time - int confMinSize = accessor.configuration.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MIN_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); - int methodMinSize = -1, controllerMinSize = -1; - - //TODO Filter by extensions ? - - //TODO Do not request config value each time - boolean configuration = accessor.configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true); - boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; - - if(context.getRoute() != null){ - //Retrieve @AllowEncoding on route method and, if exists, set a flag, compute max & min size - AllowEncoding allowOnMethod = context.getRoute().getControllerMethod().getAnnotation(AllowEncoding.class); - isAllowOnMethod = allowOnMethod == null ? false : true; - methodMaxSize = isAllowOnMethod ? allowOnMethod.maxSize() : -1; - methodMinSize = isAllowOnMethod ? allowOnMethod.minSize() : -1; - //Retrieve @AllowEncoding on route controller and, if exists, set a flag, compute max & min size - AllowEncoding allowOnController = context.getRoute().getControllerClass().getAnnotation(AllowEncoding.class); - isAllowOnController = allowOnController == null ? false : true; - controllerMaxSize = isAllowOnController ? allowOnController.maxSize() : -1; - controllerMinSize = isAllowOnController ? allowOnController.minSize() : -1; - //Retrieve @DenyEncoding on route method and route controller and set a flag - isDenyOnMethod = context.getRoute().getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; - isDenyOnController = context.getRoute().getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; - } - - // Find max size first on method, then on controller and, if none, use default - int maxSize = methodMaxSize != -1 ? methodMaxSize : controllerMaxSize != -1 ? controllerMaxSize : confMaxSize; - // Find min size first on method, then on controller and, if none, use default - int minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; - - // Ensure renderableLength is in min - max boundaries - if(renderableLength > maxSize || renderableLength < minSize) - return false; - - if(configuration){ // Configuration tells yes - if(isDenyOnMethod) // Method tells no - return false; - if(isDenyOnController && !isAllowOnMethod) // Class tells no & Method doesn't tell yes - return false; - - return true; - }else{ // Configuration tells no - if(isAllowOnMethod) // Method tells yes - return true; - if(isAllowOnController && !isDenyOnMethod) // Class tells yes & Method doesn't tell no - return true; - - return false; - } - } - private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest request, Context context, Result result, boolean handleFlashAndSessionCookie) { From 6a779548de8742dff9b02253425d2aa5cc61c151 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 15 Jan 2014 23:16:38 +0100 Subject: [PATCH 20/35] Refactoring shouldEncode and parseAcceptEncodingHeader * shouldEncode is now in EncodingHelper * parseAcceptEncodingHeader now correctly handle wildcard "*" --- .../org/wisdom/api/utils/EncodingHelper.java | 138 +++++++++++++++--- 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java index 2e9f1bd85..57712f43a 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java +++ b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java @@ -1,61 +1,165 @@ package org.wisdom.api.utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.annotations.encoder.DenyEncoding; +import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.http.EncodingNames; +import org.wisdom.api.http.Renderable; +import org.wisdom.api.router.Route; +//TODO Maybe org.wisdom.api.utils is not the place for this class public class EncodingHelper { /** * Parse a string to return an ordered list according to the Accept_Encoding HTTP Header grammar - * @param acceptEncoding String to parse. Should be an Accept_Encoding header + * @param acceptEncoding String to parse. Should be an Accept_Encoding header. * @return An ordered list of encodings */ public static List parseAcceptEncodingHeader(String acceptEncoding){ List result = new ArrayList(); + // Intermediate list to sort encoding types List tmp = new ArrayList(); - //Empty or null Accept_Encoding + + //Empty or null Accept_Encoding => return empty list if(acceptEncoding == null || acceptEncoding.trim().equals("") || acceptEncoding.trim().equals("\n")){ return result; } - //TODO That's not the real meaning of "*" - /* - The special "*" symbol in an Accept-Encoding field matches any - available content-coding not explicitly listed in the header - field. - */ - if(acceptEncoding.trim().equals("*")){ - for(String encoding : EncodingNames.ALL_ENCODINGS) - result.add(encoding); - return result; - } + //Parse Accept_Encoding for different encodings declarations String[] encodingItems = acceptEncoding.split(","); - int position = 0; + for(String encodingItem : encodingItems){ - ValuedEncoding encoding = new ValuedEncoding(encodingItem, position++); - //Remove 0 qValued encodings + // Build valued encoding from the current item ("gzip", "gzp;q=0.5", ...) + ValuedEncoding encoding = new ValuedEncoding(encodingItem, position); + // Don't insert 0 qValued encodings if(encoding.qValue > 0) tmp.add(encoding); + //High increment pace for wildcard insertion + position += 100; } + + ValuedEncoding wildCard = null; + + //Search for wildcard + for(ValuedEncoding valuedEncoding : tmp){ + //wildcard found + if(valuedEncoding.encoding.equals("*")){ + wildCard = valuedEncoding; + break; + } + } + + // Wildcard found + if(wildCard != null){ + // Retrieve all possible encodings + List encodingsToAdd = Arrays.asList(EncodingNames.ALL_ENCODINGS); + // Remove wildcard from encodings, it will be replaced by encodings not yet found + tmp.remove(wildCard); + // Remove all already found encodings from available encodings + for(ValuedEncoding valuedEncoding : tmp){ + encodingsToAdd.remove(valuedEncoding.encoding); + } + // Add remaining encodings with wildCard qValue and position (still incremented by 1 for each) + for(String remainingEncoding : encodingsToAdd){ + tmp.add(new ValuedEncoding(remainingEncoding, wildCard.qValue, wildCard.position++)); + } + } + + // Sort ValuedEncodings Collections.sort(tmp); + //Create the result List for(ValuedEncoding encoding : tmp){ result.add(encoding.encoding); } return result; } + + public static boolean shouldEncode(Renderable renderable, ApplicationConfiguration config, Route route){ + long renderableLength = renderable.length(); + + //TODO Maybe we should continue but abort size lookup for size == -1 + if(renderableLength <= 0) + return false; + + //TODO Discuss default max size + //TODO Do not request config value each time + int confMaxSize = config.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MAX_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); + int methodMaxSize = -1, controllerMaxSize = -1; + + //TODO Discuss default min size + //TODO Do not request config value each time + int confMinSize = config.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MIN_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); + int methodMinSize = -1, controllerMinSize = -1; + + //TODO Filter by extensions ? + + //TODO The default configuration (true here) should be discussed + //TODO Do not request config value each time + boolean configuration = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true); + boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; + + if(route != null){ + //Retrieve @AllowEncoding on route method and, if exists, set a flag, compute max & min size + AllowEncoding allowOnMethod = route.getControllerMethod().getAnnotation(AllowEncoding.class); + isAllowOnMethod = allowOnMethod == null ? false : true; + methodMaxSize = isAllowOnMethod ? allowOnMethod.maxSize() : -1; + methodMinSize = isAllowOnMethod ? allowOnMethod.minSize() : -1; + //Retrieve @AllowEncoding on route controller and, if exists, set a flag, compute max & min size + AllowEncoding allowOnController = route.getControllerClass().getAnnotation(AllowEncoding.class); + isAllowOnController = allowOnController == null ? false : true; + controllerMaxSize = isAllowOnController ? allowOnController.maxSize() : -1; + controllerMinSize = isAllowOnController ? allowOnController.minSize() : -1; + //Retrieve @DenyEncoding on route method and route controller and set a flag + isDenyOnMethod = route.getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; + isDenyOnController = route.getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; + } + + // Find max size first on method, then on controller and, if none, use default + int maxSize = methodMaxSize != -1 ? methodMaxSize : controllerMaxSize != -1 ? controllerMaxSize : confMaxSize; + // Find min size first on method, then on controller and, if none, use default + int minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; + + // Ensure renderableLength is in min - max boundaries + if(renderableLength > maxSize || renderableLength < minSize) + return false; + + if(configuration){ // Configuration tells yes + if(isDenyOnMethod) // Method tells no + return false; + if(isDenyOnController && !isAllowOnMethod) // Class tells no & Method doesn't tell yes + return false; + + return true; + }else{ // Configuration tells no + if(isAllowOnMethod) // Method tells yes + return true; + if(isAllowOnController && !isDenyOnMethod) // Class tells yes & Method doesn't tell no + return true; + + return false; + } + } } class ValuedEncoding implements Comparable{ String encoding = null; - Double qValue = 1.0; //TODO I am not sure that no qValue means 1.0 + Double qValue = 1.0; //TODO Not sure that no qValue means 1.0 Integer position; + public ValuedEncoding(String encodingName, Double qValue, int position){ + this.encoding = encodingName; + this.qValue = qValue; + this.position = position; + } + public ValuedEncoding(String encodingItem, int position){ this.position = position; //Split an encoding item between encoding and its qValue From 9b0641ef846afc9af7262ae2dd57daa68515cb0b Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 16 Jan 2014 00:18:02 +0100 Subject: [PATCH 21/35] Adding documentation for content encoding --- .../src/main/resources/assets/http.ad | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/documentation/src/main/resources/assets/http.ad b/documentation/src/main/resources/assets/http.ad index a9b34e5b7..4b336c023 100644 --- a/documentation/src/main/resources/assets/http.ad +++ b/documentation/src/main/resources/assets/http.ad @@ -78,3 +78,27 @@ The default is to use a 303 SEE_OTHER response type, but you can also specify a ---- include::{sourcedir}/controllers/Redirect.java[tags=temporary-redirect] ---- + +=== Response encoding + +Wisdom can encode automatically responses content according to the client +Accept_Encoding+ header. The default behavior +is to automatically encode all traffic which size is between 1KB and 10MB. Encoders available are +gzip+ and +deflate+ + +==== Activate / Deactivate encoding + +You can disable encoding globally by setting the key +global.encoding+ to false in the configuration file. + +Wisdom also provides 2 annotations allowing you to finely tune encoding behaviors. + +* +@DenyEncoding+ can be used to disable encoding on +Controllers+ and +Routes+ ; +* +@AllowEncoding+ can be used to enable encoding on +Controllers+ and +Routes+. + +In case of multiple instructions, priority is as follow : Route > Method > Configuration. + +=== Control activation threshold + +By default, Wisdom encode contents which size are between 1KB and 10MB. If you want a different behavior, you can tune +theses values with : + +* configuration keys : +encoding.max.size+ and +encoding.min.size+ (in bytes) +* +@AllowEncoding+ parameters : +maxSize+ and +minSize+ (in bytes) From 8e8cea6edd4a6cc18136bf6a3a9a5694974bdd99 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 16 Jan 2014 00:26:39 +0100 Subject: [PATCH 22/35] Fix documentation --- documentation/src/main/resources/assets/http.ad | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/src/main/resources/assets/http.ad b/documentation/src/main/resources/assets/http.ad index 4b336c023..23a8c8d94 100644 --- a/documentation/src/main/resources/assets/http.ad +++ b/documentation/src/main/resources/assets/http.ad @@ -102,3 +102,4 @@ theses values with : * configuration keys : +encoding.max.size+ and +encoding.min.size+ (in bytes) * +@AllowEncoding+ parameters : +maxSize+ and +minSize+ (in bytes) + From d16c8a36c1bc9c86791d4c076785cea6cadfe030 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 16 Jan 2014 11:22:47 +0100 Subject: [PATCH 23/35] Add configuration values for encoding * Add default value for encoding.global : true * Add encoding.stream configuration key to allow stream encoding (no size check !) * Add default value for encoding.stream : true --- .../api/configuration/ApplicationConfiguration.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java index d1e68b3e3..1e7b1c27c 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java @@ -35,6 +35,8 @@ public interface ApplicationConfiguration { */ String ENCODING_GLOBAL = "encoding.global"; + boolean DEFAULT_ENCODING_GLOBAL = true; + /** * The global encoding max activation size. */ @@ -49,6 +51,13 @@ public interface ApplicationConfiguration { int DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko + /** + * The global encoding min activation size. + */ + String ENCODING_STREAM = "encoding.stream"; + + boolean DEFAULT_ENCODING_STREAM = true; + /** * Gets the base directory of the Wisdom application. * @return the base directory From fa7a06ae9ab16e3c4a35f0ff21d0bee17a0a82ca Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 16 Jan 2014 11:24:57 +0100 Subject: [PATCH 24/35] Refactor shouldEncode do handle stream Should encode will not check stream size in encoding.stream is set to true in the configuration --- .../java/org/wisdom/api/utils/EncodingHelper.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java index 57712f43a..a2cf15d22 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java +++ b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java @@ -7,6 +7,7 @@ import org.wisdom.api.annotations.encoder.AllowEncoding; import org.wisdom.api.annotations.encoder.DenyEncoding; +import org.wisdom.api.bodies.RenderableStream; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.http.EncodingNames; import org.wisdom.api.http.Renderable; @@ -35,7 +36,7 @@ public static List parseAcceptEncodingHeader(String acceptEncoding){ int position = 0; for(String encodingItem : encodingItems){ - // Build valued encoding from the current item ("gzip", "gzp;q=0.5", ...) + // Build valued encoding from the current item ("gzip", "gzip;q=0.5", ...) ValuedEncoding encoding = new ValuedEncoding(encodingItem, position); // Don't insert 0 qValued encodings if(encoding.qValue > 0) @@ -84,9 +85,15 @@ public static List parseAcceptEncodingHeader(String acceptEncoding){ public static boolean shouldEncode(Renderable renderable, ApplicationConfiguration config, Route route){ long renderableLength = renderable.length(); + boolean checkSize = false; + + // Do not check size of stream + if(renderable instanceof RenderableStream) + checkSize = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_STREAM, ApplicationConfiguration.DEFAULT_ENCODING_STREAM); //TODO Maybe we should continue but abort size lookup for size == -1 - if(renderableLength <= 0) + //TODO Toutes les routes /assets retournent -1 en taille .... + if(checkSize && renderableLength <= 0) return false; //TODO Discuss default max size @@ -103,7 +110,7 @@ public static boolean shouldEncode(Renderable renderable, ApplicationConfigur //TODO The default configuration (true here) should be discussed //TODO Do not request config value each time - boolean configuration = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, true); + boolean configuration = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, ApplicationConfiguration.DEFAULT_ENCODING_GLOBAL); boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; if(route != null){ @@ -128,7 +135,7 @@ public static boolean shouldEncode(Renderable renderable, ApplicationConfigur int minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; // Ensure renderableLength is in min - max boundaries - if(renderableLength > maxSize || renderableLength < minSize) + if(checkSize && (renderableLength > maxSize || renderableLength < minSize)) return false; if(configuration){ // Configuration tells yes From b5bd94717bf4443f9ec154d6907fc8bf7bbbaff1 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 21 Jan 2014 14:47:42 +0100 Subject: [PATCH 25/35] Add functions to handle Long properties * Adding get functions * Update Configuration Unit Test --- .../ApplicationConfiguration.java | 42 +++++++++++++++++-- .../ApplicationConfigurationTest.java | 10 +++++ .../src/test/resources/conf/regular.conf | 2 + .../ApplicationConfiguration.java | 34 ++++++++++++++- 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfiguration.java b/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfiguration.java index a1be89e05..7dbbd5e07 100644 --- a/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfiguration.java +++ b/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfiguration.java @@ -1,5 +1,9 @@ package org.wisdom.configuration; +import java.io.File; +import java.util.NoSuchElementException; +import java.util.Properties; + import org.apache.commons.configuration.ConfigurationConverter; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; @@ -9,10 +13,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.util.NoSuchElementException; -import java.util.Properties; - /** * Implementation of the configuration service reading application/conf and an external (optional) property. */ @@ -362,4 +362,38 @@ public File getFileWithDefault(String key, File file) { return new File(baseDirectory, value); } } + + @Override + public Long getLong(String key) { + Long v = Long.getLong(key); + if (v == null) { + try { + return configuration.getLong(key); + } catch (NoSuchElementException e) { //NOSONAR + return null; + } + } else { + return v; + } + } + + @Override + public Long getLongWithDefault(String key, Long defaultValue) { + Long value = Long.getLong(key); + if (value == null) { + return configuration.getLong(key, defaultValue); + } + return value; + } + + @Override + public Long getLongOrDie(String key) { + Long value = Long.getLong(key); + if (value == null) { + logger.error(String.format(ERROR_KEY_NOT_FOUND, key)); + throw new IllegalArgumentException(String.format(ERROR_KEY_NOT_FOUND, key)); + } else { + return value; + } + } } diff --git a/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java b/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java index 647fc6840..054df99c4 100644 --- a/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java +++ b/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java @@ -83,6 +83,16 @@ public void testGetInteger() { assertThat(configuration.getIntegerWithDefault("key.int.no", 2)).isEqualTo(2); assertThat(configuration.get("key.int")).isEqualTo("1"); } + + @Test + public void testGetLong() { + System.setProperty(ApplicationConfiguration.APPLICATION_CONFIGURATION, "target/test-classes/conf/regular.conf"); + ApplicationConfiguration configuration = new ApplicationConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration.getLong("key.long")).isEqualTo(9999999999999L); + assertThat(configuration.getLongWithDefault("key.long", 2L)).isEqualTo(9999999999999L); + assertThat(configuration.getLongWithDefault("key.long.no", 2L)).isEqualTo(2L); + } @Test public void testGetBoolean() { diff --git a/application-configuration/src/test/resources/conf/regular.conf b/application-configuration/src/test/resources/conf/regular.conf index ad7949859..a3aa85c15 100644 --- a/application-configuration/src/test/resources/conf/regular.conf +++ b/application-configuration/src/test/resources/conf/regular.conf @@ -28,3 +28,5 @@ key.array = a,b,c other.conf = a_file.txt +key.long = 9999999999999 + diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java index 1e7b1c27c..fff0558dd 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java @@ -47,9 +47,9 @@ public interface ApplicationConfiguration { */ String ENCODING_MIN_SIZE = "encoding.min.size"; - int DEFAULT_ENCODING_MAX_SIZE = 10000 * 1024; // 10Mo + long DEFAULT_ENCODING_MAX_SIZE = 10000 * 1024; // 10Mo - int DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko + long DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko /** * The global encoding min activation size. @@ -105,6 +105,27 @@ public interface ApplicationConfiguration { * @return the value of the key or the default value. */ Integer getIntegerWithDefault(String key, Integer defaultValue); + + /** + * Get a property as Long or null if not there / or property no long + * + * @param key + * @return the property or null if not there or property no long + */ + Long getLong(String key); + + /** + * Get a Long property or a default value when property cannot be found + * in any configuration file. + * + * @param key + * the key used in the configuration file. + * @param defaultValue + * Default value returned, when value cannot be found in + * configuration. + * @return the value of the key or the default value. + */ + Long getLongWithDefault(String key, Long defaultValue); /** * @@ -143,6 +164,15 @@ public interface ApplicationConfiguration { * @return the Integer or a RuntimeException will be thrown. */ Integer getIntegerOrDie(String key); + + /** + * The "die" method forces this key to be set. Otherwise a runtime exception + * will be thrown. + * + * @param key + * @return the Long or a RuntimeException will be thrown. + */ + Long getLongOrDie(String key); /** * The "die" method forces this key to be set. Otherwise a runtime exception From 6585624d2a5fa1f6044daebaadf28796a772a744 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 21 Jan 2014 15:00:23 +0100 Subject: [PATCH 26/35] Finalize the refactoring of ContentEncoder to ContentCodec --- .../src/main/java/org/wisdom/content/engines/Engine.java | 8 ++++---- .../main/java/org/wisdom/api/content/ContentEngine.java | 2 +- .../main/java/org/wisdom/engine/server/WisdomHandler.java | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java index 90586d603..f63559f16 100644 --- a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java +++ b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java @@ -50,10 +50,10 @@ public ContentSerializer getContentSerializerForContentType(String contentType) } @Override - public ContentCodec getContentEncoderForEncodingType(String encoding) { - for (ContentCodec encoder : encoders) { - if (encoder.getEncodingType().equals(encoding)) { - return encoder; + public ContentCodec getContentCodecForEncodingType(String encoding) { + for (ContentCodec codec : encoders) { + if (codec.getEncodingType().equals(encoding)) { + return codec; } } return null; diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java index de840a2a4..7334a130f 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java @@ -5,5 +5,5 @@ public interface ContentEngine { BodyParser getBodyParserEngineForContentType(String contentType); ContentSerializer getContentSerializerForContentType(String contentType); - ContentCodec getContentEncoderForEncodingType(String type); + ContentCodec getContentCodecForEncodingType(String type); } diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index dfc0a3027..1bc7f028d 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -335,9 +335,9 @@ private InputStream processResult(Result result) throws Exception { break; } - if(encoder != null){ - processedResult = encoder.encode(processedResult); - result.with(CONTENT_ENCODING, encoder.getEncodingType()); + if(codec != null){ + processedResult = codec.encode(processedResult); + result.with(CONTENT_ENCODING, codec.getEncodingType()); } } return processedResult; From 287e1dd240e8197231cd50308398cda0211a7ac5 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Tue, 21 Jan 2014 17:41:51 +0100 Subject: [PATCH 27/35] Add already compressed MimeTypes --- .../org/wisdom/api/utils/KnownMimeTypes.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/utils/KnownMimeTypes.java b/wisdom-api/src/main/java/org/wisdom/api/utils/KnownMimeTypes.java index 2d49d4b2c..a655f46fb 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/utils/KnownMimeTypes.java +++ b/wisdom-api/src/main/java/org/wisdom/api/utils/KnownMimeTypes.java @@ -1,6 +1,8 @@ package org.wisdom.api.utils; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -560,6 +562,56 @@ public static String getMimeTypeByExtension(String extension) { //Extensions for Mozilla apps (Firefox and friends) EXTENSIONS.put("xpi", "application/x-xpinstall"); } + + public static List COMPRESSED_MIME; + + static{ + //From http://en.wikipedia.org/wiki/List_of_archive_formats + COMPRESSED_MIME = new ArrayList(); + addMimeToCompressedWithExtension("bz2"); + addMimeToCompressedWithExtension("gz"); + addMimeToCompressedWithExtension("gzip"); + addMimeToCompressedWithExtension("lzma"); + addMimeToCompressedWithExtension("z"); + addMimeToCompressedWithExtension("7z"); + addMimeToCompressedWithExtension("s7z"); + addMimeToCompressedWithExtension("ace"); + addMimeToCompressedWithExtension("alz"); + addMimeToCompressedWithExtension("arc"); + addMimeToCompressedWithExtension("arj"); + addMimeToCompressedWithExtension("cab"); + addMimeToCompressedWithExtension("cpt"); + addMimeToCompressedWithExtension("dar"); + addMimeToCompressedWithExtension("dmg"); + addMimeToCompressedWithExtension("ice"); + addMimeToCompressedWithExtension("lha"); + addMimeToCompressedWithExtension("lzx"); + addMimeToCompressedWithExtension("rar"); + addMimeToCompressedWithExtension("sit"); + addMimeToCompressedWithExtension("sitx"); + addMimeToCompressedWithExtension("tar"); + addMimeToCompressedWithExtension("tgz"); + addMimeToCompressedWithExtension("zip"); + addMimeToCompressedWithExtension("zoo"); + + //TODO complete list or group below (binary formats) + addMimeGroups("video/", "image/", "audio/"); + } + + private static void addMimeToCompressedWithExtension(String extension){ + String mime = EXTENSIONS.get(extension); + if(mime != null && !COMPRESSED_MIME.contains(mime)){ + COMPRESSED_MIME.add(mime); + } + } - + private static void addMimeGroups(String... groups){ + for(String mimeType : EXTENSIONS.values()){ + for(String group : groups){ + if(mimeType.startsWith(group) && !COMPRESSED_MIME.contains(mimeType)){ + COMPRESSED_MIME.add(mimeType); + } + } + } + } } From 6690ed34b1465ca77d04f5eee561968eab69ae0c Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 22 Jan 2014 10:10:03 +0100 Subject: [PATCH 28/35] ContentEncodingHelper refactoring * Added a ContentEncodingHelper interface * Provide a ContentEncodingHelperImpl in content-manager as a service * Retrieval through content-engine * Updated UT (was throwing null pointer) --- .../encoding/ContentEncodingHelperImpl.java | 248 ++++++++++++++++++ .../org/wisdom/content/engines/Engine.java | 8 + .../api/content/ContentEncodingHelper.java | 24 ++ .../org/wisdom/api/content/ContentEngine.java | 1 + .../org/wisdom/api/utils/EncodingHelper.java | 192 -------------- .../wisdom/engine/server/WisdomHandler.java | 14 +- .../engine/server/WisdomServerTest.java | 119 ++++++++- 7 files changed, 393 insertions(+), 213 deletions(-) create mode 100644 content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java create mode 100644 wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java delete mode 100644 wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java diff --git a/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java new file mode 100644 index 000000000..3ced441b0 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java @@ -0,0 +1,248 @@ +package org.wisdom.content.encoding; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.ipojo.annotations.Component; +import org.apache.felix.ipojo.annotations.Instantiate; +import org.apache.felix.ipojo.annotations.Provides; +import org.apache.felix.ipojo.annotations.Requires; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.annotations.encoder.DenyEncoding; +import org.wisdom.api.bodies.RenderableStream; +import org.wisdom.api.bodies.RenderableURL; +import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.content.ContentEncodingHelper; +import org.wisdom.api.http.EncodingNames; +import org.wisdom.api.http.Renderable; +import org.wisdom.api.router.Route; +import org.wisdom.api.utils.KnownMimeTypes; + +@Component +@Instantiate +@Provides +public class ContentEncodingHelperImpl implements ContentEncodingHelper{ + + @Requires(specification=ApplicationConfiguration.class, optional=false) + ApplicationConfiguration configuration; + + Boolean allowEncodingGlobalSetting = null; + + Boolean allowStreamEncodingGlobalSetting = null; + + Long maxSizeGlobalSetting = null; + + Long minSizeGlobalSetting = null; + + public boolean getAllowEncodingGlobalSetting(){ + if(allowEncodingGlobalSetting == null) + allowEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, ApplicationConfiguration.DEFAULT_ENCODING_GLOBAL); + return allowEncodingGlobalSetting; + } + + public boolean getAllowStreamEncodingGlobalSetting(){ + if(allowStreamEncodingGlobalSetting == null) + allowStreamEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_STREAM, ApplicationConfiguration.DEFAULT_ENCODING_STREAM); + return allowStreamEncodingGlobalSetting; + } + + public long getMaxSizeGlobalSetting(){ + if(maxSizeGlobalSetting == null) + maxSizeGlobalSetting = configuration.getLongWithDefault(ApplicationConfiguration.ENCODING_MAX_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); + return maxSizeGlobalSetting; + } + + public long getMinSizeGlobalSetting(){ + if(minSizeGlobalSetting == null) + minSizeGlobalSetting = configuration.getLongWithDefault(ApplicationConfiguration.ENCODING_MIN_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); + return minSizeGlobalSetting; + } + + @Override + public boolean shouldEncode(Route route, Renderable renderable) { + return shouldEncodeWithRoute(route) && shouldEncodeWithSize(route, renderable) && shouldEncodeWithMimeType(renderable); + } + + @Override + public boolean shouldEncodeWithMimeType(Renderable renderable){ + String mime = renderable.mimetype(); + + if(mime == null){ + //TODO What to do when we can't know the mime type ? drop or continue ? + return false; + } + + if(KnownMimeTypes.COMPRESSED_MIME.contains(mime)){ + return false; + } + + return true; + } + + @Override + public boolean shouldEncodeWithSize(Route route, Renderable renderable){ + long renderableLength = renderable.length(); + // Renderable is stream, return config value + if(renderable instanceof RenderableStream || renderable instanceof RenderableURL){ + return getAllowStreamEncodingGlobalSetting(); + } + // Not a stream and value is -1 or 0 + if(renderableLength <= 0) + return false; + + long confMaxSize = getMaxSizeGlobalSetting(); + long confMinSize = getMinSizeGlobalSetting(); + long methodMaxSize = -1, controllerMaxSize = -1, methodMinSize = -1, controllerMinSize = -1; + + if(route != null){ + // Retrieve size limitation on method if any + AllowEncoding allowOnMethod = route.getControllerMethod().getAnnotation(AllowEncoding.class); + methodMaxSize = allowOnMethod != null ? allowOnMethod.maxSize() : -1; + methodMinSize = allowOnMethod != null ? allowOnMethod.minSize() : -1; + // Retrieve size limitation on class if any + AllowEncoding allowOnController = route.getControllerClass().getAnnotation(AllowEncoding.class); + controllerMaxSize = allowOnController != null ? allowOnController.maxSize() : -1; + controllerMinSize = allowOnController != null ? allowOnController.minSize() : -1; + } + + // Find max size first on method, then on controller and, if none, use default + long maxSize = methodMaxSize != -1 ? methodMaxSize : controllerMaxSize != -1 ? controllerMaxSize : confMaxSize; + // Find min size first on method, then on controller and, if none, use default + long minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; + + // Ensure renderableLength is in min - max boundaries + if(renderableLength > maxSize || renderableLength < minSize) + return false; + + return true; + } + + @Override + public boolean shouldEncodeWithRoute(Route route){ + boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; + + if(route != null){ + // Retrieve @AllowEncoding annotations + isAllowOnMethod = route.getControllerMethod().getAnnotation(AllowEncoding.class) == null ? false : true; + isAllowOnController = route.getControllerClass().getAnnotation(AllowEncoding.class) == null ? false : true; + // Retrieve @DenyEncoding annotations + isDenyOnMethod = route.getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; + isDenyOnController = route.getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; + } + + if(getAllowEncodingGlobalSetting()){ // Configuration tells yes + if(isDenyOnMethod) // Method tells no + return false; + if(isDenyOnController && !isAllowOnMethod) // Class tells no & Method doesn't tell yes + return false; + + return true; + }else{ // Configuration tells no + if(isAllowOnMethod) // Method tells yes + return true; + if(isAllowOnController && !isDenyOnMethod) // Class tells yes & Method doesn't tell no + return true; + + return false; + } + } + + @Override + public List parseAcceptEncodingHeader(String headerContent){ + List result = new ArrayList(); + // Intermediate list to sort encoding types + List tmp = new ArrayList(); + + //Empty or null Accept_Encoding => return empty list + if(headerContent == null || headerContent.trim().equals("") || headerContent.trim().equals("\n")){ + return result; + } + + //Parse Accept_Encoding for different encodings declarations + String[] encodingItems = headerContent.split(","); + int position = 0; + + for(String encodingItem : encodingItems){ + // Build valued encoding from the current item ("gzip", "gzip;q=0.5", ...) + ValuedEncoding encoding = new ValuedEncoding(encodingItem, position); + // Don't insert 0 qValued encodings + if(encoding.qValue > 0) + tmp.add(encoding); + //High increment pace for wildcard insertion + position += 100; + } + + ValuedEncoding wildCard = null; + + //Search for wildcard + for(ValuedEncoding valuedEncoding : tmp){ + //wildcard found + if(valuedEncoding.encoding.equals("*")){ + wildCard = valuedEncoding; + break; + } + } + + // Wildcard found + if(wildCard != null){ + // Retrieve all possible encodings + List encodingsToAdd = Arrays.asList(EncodingNames.ALL_ENCODINGS); + // Remove wildcard from encodings, it will be replaced by encodings not yet found + tmp.remove(wildCard); + // Remove all already found encodings from available encodings + for(ValuedEncoding valuedEncoding : tmp){ + encodingsToAdd.remove(valuedEncoding.encoding); + } + // Add remaining encodings with wildCard qValue and position (still incremented by 1 for each) + for(String remainingEncoding : encodingsToAdd){ + tmp.add(new ValuedEncoding(remainingEncoding, wildCard.qValue, wildCard.position++)); + } + } + + // Sort ValuedEncodings + Collections.sort(tmp); + + //Create the result List + for(ValuedEncoding encoding : tmp){ + result.add(encoding.encoding); + } + + return result; + } +} + +class ValuedEncoding implements Comparable{ + String encoding = null; + Double qValue = 1.0; + Integer position; + + public ValuedEncoding(String encodingName, Double qValue, int position){ + this.encoding = encodingName; + this.qValue = qValue; + this.position = position; + } + + public ValuedEncoding(String encodingItem, int position){ + this.position = position; + //Split an encoding item between encoding and its qValue + String[] encodingParts = encodingItem.split(";"); + //Grab encoding name + encoding = encodingParts[0].trim().replace("\n", ""); + //Grab encoding's qValue if it exists (default 1.0 otherwise) + if(encodingParts.length > 1){ + qValue = Double.parseDouble(encodingParts[1].trim().replace("\n", "")); + } + } + + @Override + public int compareTo(ValuedEncoding o) { + if(qValue.equals(o.qValue)){ + // In case 2 encodings have the same qValue, the first one has priority + return position.compareTo(o.position); + } + //Highest qValue first, invert default ascending comparison + return qValue.compareTo(o.qValue) * -1; + } +} diff --git a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java index f63559f16..b38a0a6f5 100644 --- a/content-manager/src/main/java/org/wisdom/content/engines/Engine.java +++ b/content-manager/src/main/java/org/wisdom/content/engines/Engine.java @@ -6,6 +6,7 @@ import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.content.BodyParser; import org.wisdom.api.content.ContentCodec; +import org.wisdom.api.content.ContentEncodingHelper; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.slf4j.LoggerFactory; @@ -26,6 +27,8 @@ public class Engine implements ContentEngine { List serializers; @Requires(specification = ContentCodec.class, optional = true) List encoders; + @Requires(specification = ContentEncodingHelper.class, optional = true) + ContentEncodingHelper encodingHelper; @Override public BodyParser getBodyParserEngineForContentType(String contentType) { @@ -58,4 +61,9 @@ public ContentCodec getContentCodecForEncodingType(String encoding) { } return null; } + + @Override + public ContentEncodingHelper getContentEncodingHelper() { + return encodingHelper; + } } diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java new file mode 100644 index 000000000..873d7eedc --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java @@ -0,0 +1,24 @@ +package org.wisdom.api.content; + +import java.util.List; + +import org.wisdom.api.http.Renderable; +import org.wisdom.api.router.Route; + +public interface ContentEncodingHelper { + + /** + * Parse a string to return an ordered list according to the Accept_Encoding HTTP Header grammar + * @param acceptEncoding String to parse. Should be an Accept_Encoding header. + * @return An ordered list of encodings + */ + public List parseAcceptEncodingHeader(String headerContent); + + public boolean shouldEncode(Route route, Renderable renderable); + + public boolean shouldEncodeWithRoute(Route route); + + public boolean shouldEncodeWithSize(Route route, Renderable renderable); + + public boolean shouldEncodeWithMimeType(Renderable renderable); +} diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java index 7334a130f..659c53c8c 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java @@ -6,4 +6,5 @@ public interface ContentEngine { BodyParser getBodyParserEngineForContentType(String contentType); ContentSerializer getContentSerializerForContentType(String contentType); ContentCodec getContentCodecForEncodingType(String type); + ContentEncodingHelper getContentEncodingHelper(); } diff --git a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java deleted file mode 100644 index a2cf15d22..000000000 --- a/wisdom-api/src/main/java/org/wisdom/api/utils/EncodingHelper.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.wisdom.api.utils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.wisdom.api.annotations.encoder.AllowEncoding; -import org.wisdom.api.annotations.encoder.DenyEncoding; -import org.wisdom.api.bodies.RenderableStream; -import org.wisdom.api.configuration.ApplicationConfiguration; -import org.wisdom.api.http.EncodingNames; -import org.wisdom.api.http.Renderable; -import org.wisdom.api.router.Route; - -//TODO Maybe org.wisdom.api.utils is not the place for this class -public class EncodingHelper { - - /** - * Parse a string to return an ordered list according to the Accept_Encoding HTTP Header grammar - * @param acceptEncoding String to parse. Should be an Accept_Encoding header. - * @return An ordered list of encodings - */ - public static List parseAcceptEncodingHeader(String acceptEncoding){ - List result = new ArrayList(); - // Intermediate list to sort encoding types - List tmp = new ArrayList(); - - //Empty or null Accept_Encoding => return empty list - if(acceptEncoding == null || acceptEncoding.trim().equals("") || acceptEncoding.trim().equals("\n")){ - return result; - } - - //Parse Accept_Encoding for different encodings declarations - String[] encodingItems = acceptEncoding.split(","); - int position = 0; - - for(String encodingItem : encodingItems){ - // Build valued encoding from the current item ("gzip", "gzip;q=0.5", ...) - ValuedEncoding encoding = new ValuedEncoding(encodingItem, position); - // Don't insert 0 qValued encodings - if(encoding.qValue > 0) - tmp.add(encoding); - //High increment pace for wildcard insertion - position += 100; - } - - ValuedEncoding wildCard = null; - - //Search for wildcard - for(ValuedEncoding valuedEncoding : tmp){ - //wildcard found - if(valuedEncoding.encoding.equals("*")){ - wildCard = valuedEncoding; - break; - } - } - - // Wildcard found - if(wildCard != null){ - // Retrieve all possible encodings - List encodingsToAdd = Arrays.asList(EncodingNames.ALL_ENCODINGS); - // Remove wildcard from encodings, it will be replaced by encodings not yet found - tmp.remove(wildCard); - // Remove all already found encodings from available encodings - for(ValuedEncoding valuedEncoding : tmp){ - encodingsToAdd.remove(valuedEncoding.encoding); - } - // Add remaining encodings with wildCard qValue and position (still incremented by 1 for each) - for(String remainingEncoding : encodingsToAdd){ - tmp.add(new ValuedEncoding(remainingEncoding, wildCard.qValue, wildCard.position++)); - } - } - - // Sort ValuedEncodings - Collections.sort(tmp); - - //Create the result List - for(ValuedEncoding encoding : tmp){ - result.add(encoding.encoding); - } - - return result; - } - - public static boolean shouldEncode(Renderable renderable, ApplicationConfiguration config, Route route){ - long renderableLength = renderable.length(); - boolean checkSize = false; - - // Do not check size of stream - if(renderable instanceof RenderableStream) - checkSize = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_STREAM, ApplicationConfiguration.DEFAULT_ENCODING_STREAM); - - //TODO Maybe we should continue but abort size lookup for size == -1 - //TODO Toutes les routes /assets retournent -1 en taille .... - if(checkSize && renderableLength <= 0) - return false; - - //TODO Discuss default max size - //TODO Do not request config value each time - int confMaxSize = config.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MAX_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); - int methodMaxSize = -1, controllerMaxSize = -1; - - //TODO Discuss default min size - //TODO Do not request config value each time - int confMinSize = config.getIntegerWithDefault(ApplicationConfiguration.ENCODING_MIN_SIZE, ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); - int methodMinSize = -1, controllerMinSize = -1; - - //TODO Filter by extensions ? - - //TODO The default configuration (true here) should be discussed - //TODO Do not request config value each time - boolean configuration = config.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, ApplicationConfiguration.DEFAULT_ENCODING_GLOBAL); - boolean isAllowOnMethod = false, isDenyOnMethod = false, isAllowOnController = false, isDenyOnController = false; - - if(route != null){ - //Retrieve @AllowEncoding on route method and, if exists, set a flag, compute max & min size - AllowEncoding allowOnMethod = route.getControllerMethod().getAnnotation(AllowEncoding.class); - isAllowOnMethod = allowOnMethod == null ? false : true; - methodMaxSize = isAllowOnMethod ? allowOnMethod.maxSize() : -1; - methodMinSize = isAllowOnMethod ? allowOnMethod.minSize() : -1; - //Retrieve @AllowEncoding on route controller and, if exists, set a flag, compute max & min size - AllowEncoding allowOnController = route.getControllerClass().getAnnotation(AllowEncoding.class); - isAllowOnController = allowOnController == null ? false : true; - controllerMaxSize = isAllowOnController ? allowOnController.maxSize() : -1; - controllerMinSize = isAllowOnController ? allowOnController.minSize() : -1; - //Retrieve @DenyEncoding on route method and route controller and set a flag - isDenyOnMethod = route.getControllerMethod().getAnnotation(DenyEncoding.class) == null ? false : true; - isDenyOnController = route.getControllerClass().getAnnotation(DenyEncoding.class) == null ? false : true; - } - - // Find max size first on method, then on controller and, if none, use default - int maxSize = methodMaxSize != -1 ? methodMaxSize : controllerMaxSize != -1 ? controllerMaxSize : confMaxSize; - // Find min size first on method, then on controller and, if none, use default - int minSize = methodMinSize != -1 ? methodMinSize : controllerMinSize != -1 ? controllerMinSize : confMinSize; - - // Ensure renderableLength is in min - max boundaries - if(checkSize && (renderableLength > maxSize || renderableLength < minSize)) - return false; - - if(configuration){ // Configuration tells yes - if(isDenyOnMethod) // Method tells no - return false; - if(isDenyOnController && !isAllowOnMethod) // Class tells no & Method doesn't tell yes - return false; - - return true; - }else{ // Configuration tells no - if(isAllowOnMethod) // Method tells yes - return true; - if(isAllowOnController && !isDenyOnMethod) // Class tells yes & Method doesn't tell no - return true; - - return false; - } - } -} - -class ValuedEncoding implements Comparable{ - String encoding = null; - Double qValue = 1.0; //TODO Not sure that no qValue means 1.0 - Integer position; - - public ValuedEncoding(String encodingName, Double qValue, int position){ - this.encoding = encodingName; - this.qValue = qValue; - this.position = position; - } - - public ValuedEncoding(String encodingItem, int position){ - this.position = position; - //Split an encoding item between encoding and its qValue - String[] encodingParts = encodingItem.split(";"); - //Grab encoding name - encoding = encodingParts[0].trim().replace("\n", ""); - //Grab encoding's qValue if it exists (default 1.0 otherwise) - if(encodingParts.length > 1){ - qValue = Double.parseDouble(encodingParts[1].trim().replace("\n", "")); - } - } - - @Override - public int compareTo(ValuedEncoding o) { - if(qValue.equals(o.qValue)){ - // TODO not sure its true according to the standard - // In case 2 encodings have the same qValue, the first one has priority - return position.compareTo(o.position); - } - //Highest qValue first, invert default ascending comparison - return qValue.compareTo(o.qValue) * -1; - } -} diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 1bc7f028d..fb14123bd 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -48,10 +48,7 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.wisdom.api.annotations.encoder.AllowEncoding; -import org.wisdom.api.annotations.encoder.DenyEncoding; import org.wisdom.api.bodies.NoHttpBody; -import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.content.ContentCodec; import org.wisdom.api.content.ContentSerializer; import org.wisdom.api.error.ErrorHandler; @@ -62,7 +59,6 @@ import org.wisdom.api.http.Result; import org.wisdom.api.http.Results; import org.wisdom.api.router.Route; -import org.wisdom.api.utils.EncodingHelper; import org.wisdom.engine.wrapper.ContextFromNetty; import org.wisdom.engine.wrapper.cookies.CookieHelper; @@ -326,12 +322,12 @@ private InputStream processResult(Result result) throws Exception { InputStream processedResult = renderable.render(context, result); - if(EncodingHelper.shouldEncode(renderable, accessor.configuration, context.getRoute())){ - ContentCodec encoder = null; + if(accessor.content_engines.getContentEncodingHelper().shouldEncode(context.getRoute(), renderable)){ + ContentCodec codec = null; - for(String encoding : EncodingHelper.parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ - encoder = accessor.content_engines.getContentEncoderForEncodingType(encoding); - if(encoder != null) + for(String encoding : accessor.content_engines.getContentEncodingHelper().parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ + codec = accessor.content_engines.getContentCodecForEncodingType(encoding); + if(codec != null) break; } diff --git a/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java b/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java index 4b2f76e88..0c68c6f2c 100644 --- a/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java +++ b/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java @@ -1,11 +1,26 @@ package org.wisdom.engine.server; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Test; import org.wisdom.api.Controller; import org.wisdom.api.DefaultController; import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.content.ContentEncodingHelper; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.wisdom.api.error.ErrorHandler; @@ -16,16 +31,6 @@ import org.wisdom.api.router.RouteBuilder; import org.wisdom.api.router.Router; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * Check the wisdom server behavior. * This class is listening for http requests on the port 9001. @@ -51,13 +56,44 @@ public void testServerStartSequence() throws InterruptedException, IOException { // Prepare an empty router. Router router = mock(Router.class); + + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + + @Override + public boolean shouldEncode(Route route, Renderable renderable) { + return true; + } + + @Override + public List parseAcceptEncodingHeader(String headerContent) { + return new ArrayList(); + } + + @Override + public boolean shouldEncodeWithRoute(Route route) { + return true; + } + + @Override + public boolean shouldEncodeWithSize(Route route, + Renderable renderable) { + return true; + } + + @Override + public boolean shouldEncodeWithMimeType(Renderable renderable) { + return true; + } + }; + ContentEngine contentEngine = mock(ContentEngine.class); + when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); // Configure the server. server = new WisdomServer(new ServiceAccessor( null, configuration, router, - null, + contentEngine, null, Collections.emptyList(), null @@ -91,13 +127,43 @@ public Result index() { .on("/") .to(controller, "index"); when(router.getRouteFor("GET", "/")).thenReturn(route); + + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + @Override + public boolean shouldEncode(Route route, Renderable renderable) { + return true; + } + + @Override + public List parseAcceptEncodingHeader(String headerContent) { + return new ArrayList(); + } + + @Override + public boolean shouldEncodeWithRoute(Route route) { + return true; + } + + @Override + public boolean shouldEncodeWithSize(Route route, + Renderable renderable) { + return true; + } + + @Override + public boolean shouldEncodeWithMimeType(Renderable renderable) { + return true; + } + }; + ContentEngine contentEngine = mock(ContentEngine.class); + when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); // Configure the server. server = new WisdomServer(new ServiceAccessor( null, configuration, router, - null, + contentEngine, null, Collections.emptyList(), null @@ -144,7 +210,36 @@ public void serialize(Renderable renderable) { } } }; + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + + @Override + public boolean shouldEncode(Route route, Renderable renderable) { + return true; + } + + @Override + public List parseAcceptEncodingHeader(String headerContent) { + return new ArrayList(); + } + + @Override + public boolean shouldEncodeWithRoute(Route route) { + return true; + } + + @Override + public boolean shouldEncodeWithSize(Route route, + Renderable renderable) { + return true; + } + + @Override + public boolean shouldEncodeWithMimeType(Renderable renderable) { + return true; + } + }; ContentEngine contentEngine = mock(ContentEngine.class); + when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); when(contentEngine.getContentSerializerForContentType(anyString())).thenReturn(serializer); // Configure the server. From 8543a0128f3619707880d268be4146fd929cd66c Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Wed, 22 Jan 2014 10:35:41 +0100 Subject: [PATCH 29/35] Add filtering of already encoded content --- .../encoding/ContentEncodingHelperImpl.java | 42 +++++++++++----- .../ApplicationConfiguration.java | 6 +-- .../api/content/ContentEncodingHelper.java | 7 ++- .../wisdom/engine/server/WisdomHandler.java | 2 +- .../engine/server/WisdomServerTest.java | 49 +++++++++++++------ 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java index 3ced441b0..7611c1088 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java +++ b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; @@ -11,12 +12,14 @@ import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.annotations.encoder.AllowEncoding; import org.wisdom.api.annotations.encoder.DenyEncoding; -import org.wisdom.api.bodies.RenderableStream; import org.wisdom.api.bodies.RenderableURL; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.content.ContentEncodingHelper; +import org.wisdom.api.http.Context; import org.wisdom.api.http.EncodingNames; +import org.wisdom.api.http.HeaderNames; import org.wisdom.api.http.Renderable; +import org.wisdom.api.http.Result; import org.wisdom.api.router.Route; import org.wisdom.api.utils.KnownMimeTypes; @@ -30,7 +33,7 @@ public class ContentEncodingHelperImpl implements ContentEncodingHelper{ Boolean allowEncodingGlobalSetting = null; - Boolean allowStreamEncodingGlobalSetting = null; + Boolean allowUrlEncodingGlobalSetting = null; Long maxSizeGlobalSetting = null; @@ -42,10 +45,10 @@ public boolean getAllowEncodingGlobalSetting(){ return allowEncodingGlobalSetting; } - public boolean getAllowStreamEncodingGlobalSetting(){ - if(allowStreamEncodingGlobalSetting == null) - allowStreamEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_STREAM, ApplicationConfiguration.DEFAULT_ENCODING_STREAM); - return allowStreamEncodingGlobalSetting; + public boolean getAllowUrlEncodingGlobalSetting(){ + if(allowUrlEncodingGlobalSetting == null) + allowUrlEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_URL, ApplicationConfiguration.DEFAULT_ENCODING_URL); + return allowUrlEncodingGlobalSetting; } public long getMaxSizeGlobalSetting(){ @@ -61,8 +64,24 @@ public long getMinSizeGlobalSetting(){ } @Override - public boolean shouldEncode(Route route, Renderable renderable) { - return shouldEncodeWithRoute(route) && shouldEncodeWithSize(route, renderable) && shouldEncodeWithMimeType(renderable); + public boolean shouldEncode(Context context, Result result, Renderable renderable) { + return shouldEncodeWithHeaders(result.getHeaders()) && shouldEncodeWithRoute(context.getRoute()) && shouldEncodeWithSize(context.getRoute(), renderable) && shouldEncodeWithMimeType(renderable); + } + + @Override + public boolean shouldEncodeWithHeaders(Map headers){ + String contentEncoding = headers.get(HeaderNames.CONTENT_ENCODING); + + if(contentEncoding != null){// There is a content encoding already set + //if empty or identity, we can encode + if(contentEncoding.equals("") || contentEncoding.equals("\n") || contentEncoding.equals(EncodingNames.IDENTITY)){ + return true; + }else{ + return false; + } + } + + return true; } @Override @@ -70,7 +89,8 @@ public boolean shouldEncodeWithMimeType(Renderable renderable){ String mime = renderable.mimetype(); if(mime == null){ - //TODO What to do when we can't know the mime type ? drop or continue ? + //TODO What to do when we can't know the mime type ? drop or allow ? + //Drop on unknown mime type return false; } @@ -85,8 +105,8 @@ public boolean shouldEncodeWithMimeType(Renderable renderable){ public boolean shouldEncodeWithSize(Route route, Renderable renderable){ long renderableLength = renderable.length(); // Renderable is stream, return config value - if(renderable instanceof RenderableStream || renderable instanceof RenderableURL){ - return getAllowStreamEncodingGlobalSetting(); + if(renderable instanceof RenderableURL){ + return getAllowUrlEncodingGlobalSetting(); } // Not a stream and value is -1 or 0 if(renderableLength <= 0) diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java index fff0558dd..9eb523492 100755 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/ApplicationConfiguration.java @@ -52,11 +52,11 @@ public interface ApplicationConfiguration { long DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko /** - * The global encoding min activation size. + * The global url encoding activation key. */ - String ENCODING_STREAM = "encoding.stream"; + String ENCODING_URL = "encoding.url"; - boolean DEFAULT_ENCODING_STREAM = true; + boolean DEFAULT_ENCODING_URL = true; /** * Gets the base directory of the Wisdom application. diff --git a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java index 873d7eedc..c3dc04b9f 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java @@ -1,8 +1,11 @@ package org.wisdom.api.content; import java.util.List; +import java.util.Map; +import org.wisdom.api.http.Context; import org.wisdom.api.http.Renderable; +import org.wisdom.api.http.Result; import org.wisdom.api.router.Route; public interface ContentEncodingHelper { @@ -14,11 +17,13 @@ public interface ContentEncodingHelper { */ public List parseAcceptEncodingHeader(String headerContent); - public boolean shouldEncode(Route route, Renderable renderable); + public boolean shouldEncode(Context context, Result result, Renderable renderable); public boolean shouldEncodeWithRoute(Route route); public boolean shouldEncodeWithSize(Route route, Renderable renderable); public boolean shouldEncodeWithMimeType(Renderable renderable); + + public boolean shouldEncodeWithHeaders(Map headers); } diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index fb14123bd..a745d05bb 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -322,7 +322,7 @@ private InputStream processResult(Result result) throws Exception { InputStream processedResult = renderable.render(context, result); - if(accessor.content_engines.getContentEncodingHelper().shouldEncode(context.getRoute(), renderable)){ + if(accessor.content_engines.getContentEncodingHelper().shouldEncode(context, result, renderable)){ ContentCodec codec = null; for(String encoding : accessor.content_engines.getContentEncodingHelper().parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ diff --git a/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java b/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java index 0c68c6f2c..ba2379aec 100644 --- a/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java +++ b/wisdom-engine/src/test/java/org/wisdom/engine/server/WisdomServerTest.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.apache.commons.io.IOUtils; import org.junit.After; @@ -24,6 +25,7 @@ import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.wisdom.api.error.ErrorHandler; +import org.wisdom.api.http.Context; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Renderable; import org.wisdom.api.http.Result; @@ -59,11 +61,6 @@ public void testServerStartSequence() throws InterruptedException, IOException { ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { - @Override - public boolean shouldEncode(Route route, Renderable renderable) { - return true; - } - @Override public List parseAcceptEncodingHeader(String headerContent) { return new ArrayList(); @@ -84,6 +81,17 @@ public boolean shouldEncodeWithSize(Route route, public boolean shouldEncodeWithMimeType(Renderable renderable) { return true; } + + @Override + public boolean shouldEncode(Context context, Result result, + Renderable renderable) { + return false; + } + + @Override + public boolean shouldEncodeWithHeaders(Map headers) { + return false; + } }; ContentEngine contentEngine = mock(ContentEngine.class); when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); @@ -129,10 +137,6 @@ public Result index() { when(router.getRouteFor("GET", "/")).thenReturn(route); ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { - @Override - public boolean shouldEncode(Route route, Renderable renderable) { - return true; - } @Override public List parseAcceptEncodingHeader(String headerContent) { @@ -154,6 +158,17 @@ public boolean shouldEncodeWithSize(Route route, public boolean shouldEncodeWithMimeType(Renderable renderable) { return true; } + + @Override + public boolean shouldEncode(Context context, Result result, + Renderable renderable) { + return false; + } + + @Override + public boolean shouldEncodeWithHeaders(Map headers) { + return false; + } }; ContentEngine contentEngine = mock(ContentEngine.class); when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); @@ -212,11 +227,6 @@ public void serialize(Renderable renderable) { }; ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { - @Override - public boolean shouldEncode(Route route, Renderable renderable) { - return true; - } - @Override public List parseAcceptEncodingHeader(String headerContent) { return new ArrayList(); @@ -237,6 +247,17 @@ public boolean shouldEncodeWithSize(Route route, public boolean shouldEncodeWithMimeType(Renderable renderable) { return true; } + + @Override + public boolean shouldEncode(Context context, Result result, + Renderable renderable) { + return false; + } + + @Override + public boolean shouldEncodeWithHeaders(Map headers) { + return false; + } }; ContentEngine contentEngine = mock(ContentEngine.class); when(contentEngine.getContentEncodingHelper()).thenReturn(encodingHelper); From e9c02d84acb5fd6b34b38549f25fb5227f5e081a Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 10:04:28 +0100 Subject: [PATCH 30/35] Refactor @AllowEncoding annotation to use long instead of int --- .../org/wisdom/api/annotations/encoder/AllowEncoding.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java index a10ce0da7..190db54ba 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java +++ b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java @@ -8,6 +8,6 @@ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AllowEncoding { - int maxSize() default -1; - int minSize() default -1; + long maxSize() default -1; + long minSize() default -1; } From fb7193b5cc1a32850fb60dd1cc873788983f8c1c Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 10:05:51 +0100 Subject: [PATCH 31/35] Add ContentEncodingHelperImpl UT * Add UT and FakeControllers * Add mockito dependency * Modifications to ContentEncodingHelperImpl to satisfy tests --- content-manager/pom.xml | 5 + .../encoding/ContentEncodingHelperImpl.java | 30 ++- .../encoders/EncodingHelperImplTest.java | 250 ++++++++++++++++++ .../content/encoders/FakeAllowController.java | 22 ++ .../content/encoders/FakeDenyController.java | 22 ++ .../content/encoders/FakeSizeController.java | 17 ++ 6 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 content-manager/src/test/java/org/wisdom/content/encoders/EncodingHelperImplTest.java create mode 100644 content-manager/src/test/java/org/wisdom/content/encoders/FakeAllowController.java create mode 100644 content-manager/src/test/java/org/wisdom/content/encoders/FakeDenyController.java create mode 100644 content-manager/src/test/java/org/wisdom/content/encoders/FakeSizeController.java diff --git a/content-manager/pom.xml b/content-manager/pom.xml index 3594db7f1..7411d968a 100644 --- a/content-manager/pom.xml +++ b/content-manager/pom.xml @@ -21,6 +21,11 @@ wisdom-api ${project.version} + + + org.mockito + mockito-all + org.apache.felix diff --git a/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java index 7611c1088..2bacdf13a 100644 --- a/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java +++ b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java @@ -39,6 +39,10 @@ public class ContentEncodingHelperImpl implements ContentEncodingHelper{ Long minSizeGlobalSetting = null; + public void setConfiguration(ApplicationConfiguration configuration){ + this.configuration = configuration; + } + public boolean getAllowEncodingGlobalSetting(){ if(allowEncodingGlobalSetting == null) allowEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_GLOBAL, ApplicationConfiguration.DEFAULT_ENCODING_GLOBAL); @@ -65,11 +69,22 @@ public long getMinSizeGlobalSetting(){ @Override public boolean shouldEncode(Context context, Result result, Renderable renderable) { + //TODO We could do only renderable tests if nulls. Default behavior abort / allow ? + //If no result or context, abort + if(context == null || result == null){ + return false; + } + return shouldEncodeWithHeaders(result.getHeaders()) && shouldEncodeWithRoute(context.getRoute()) && shouldEncodeWithSize(context.getRoute(), renderable) && shouldEncodeWithMimeType(renderable); } @Override public boolean shouldEncodeWithHeaders(Map headers){ + //TODO What to do if no headers provided ? allow, abort ? + //No header provided, allow encoding + if(headers == null) + return true; + String contentEncoding = headers.get(HeaderNames.CONTENT_ENCODING); if(contentEncoding != null){// There is a content encoding already set @@ -86,6 +101,11 @@ public boolean shouldEncodeWithHeaders(Map headers){ @Override public boolean shouldEncodeWithMimeType(Renderable renderable){ + //TODO What to do if no renderable provided ? allow, abort ? + //No renderable provided, abort encoding + if(renderable == null) + return false; + String mime = renderable.mimetype(); if(mime == null){ @@ -103,12 +123,18 @@ public boolean shouldEncodeWithMimeType(Renderable renderable){ @Override public boolean shouldEncodeWithSize(Route route, Renderable renderable){ + //TODO What to do if no renderable provided ? allow, abort ? + //No renderable provided, abort encoding + if(renderable == null) + return false; + long renderableLength = renderable.length(); - // Renderable is stream, return config value + + // Renderable is url, return config value if(renderable instanceof RenderableURL){ return getAllowUrlEncodingGlobalSetting(); } - // Not a stream and value is -1 or 0 + // Not an URL and value is -1 or 0 if(renderableLength <= 0) return false; diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/EncodingHelperImplTest.java b/content-manager/src/test/java/org/wisdom/content/encoders/EncodingHelperImplTest.java new file mode 100644 index 000000000..d97ac8d18 --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/EncodingHelperImplTest.java @@ -0,0 +1,250 @@ +package org.wisdom.content.encoders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.wisdom.api.Controller; +import org.wisdom.api.bodies.RenderableString; +import org.wisdom.api.bodies.RenderableURL; +import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.http.EncodingNames; +import org.wisdom.api.http.HeaderNames; +import org.wisdom.api.http.HttpMethod; +import org.wisdom.api.router.Route; +import org.wisdom.api.router.RouteBuilder; +import org.wisdom.api.utils.KnownMimeTypes; +import org.wisdom.content.encoding.ContentEncodingHelperImpl; + +public class EncodingHelperImplTest{ + + ContentEncodingHelperImpl encodingHelper = null; + + ApplicationConfiguration configuration = null; + + @Before + public void before(){ + encodingHelper = new ContentEncodingHelperImpl(); + configuration = Mockito.mock(ApplicationConfiguration.class); + when(configuration.getBooleanWithDefault(anyString(), anyBoolean())).then(new Answer() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return (boolean)invocation.getArguments()[1]; + } + }); + when(configuration.getLongWithDefault(anyString(), anyLong())).then(new Answer() { + + @Override + public Long answer(InvocationOnMock invocation) throws Throwable { + return (Long)invocation.getArguments()[1]; + } + }); + when(configuration.getIntegerWithDefault(anyString(), anyInt())).then(new Answer() { + + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + return (Integer)invocation.getArguments()[1]; + } + }); + encodingHelper.setConfiguration(configuration); + } + + @Test + public void testShouldEncodeWithHeaders(){ + Map headers = new HashMap(); + //Already encoded + headers.put(HeaderNames.CONTENT_ENCODING, EncodingNames.GZIP); + assertThat(encodingHelper.shouldEncodeWithHeaders(headers)).isEqualTo(false); + + headers.clear(); + //Content_Encoding set but empty + headers.put(HeaderNames.CONTENT_ENCODING, ""); + assertThat(encodingHelper.shouldEncodeWithHeaders(headers)).isEqualTo(true); + + headers.clear(); + //Content_Encoding set but Identity + headers.put(HeaderNames.CONTENT_ENCODING, EncodingNames.IDENTITY); + assertThat(encodingHelper.shouldEncodeWithHeaders(headers)).isEqualTo(true); + + headers.clear(); + //Content_Encoding set but null + headers.put(HeaderNames.CONTENT_ENCODING, EncodingNames.IDENTITY); + assertThat(encodingHelper.shouldEncodeWithHeaders(headers)).isEqualTo(true); + + headers.clear(); + //Content_Encoding not set + assertThat(encodingHelper.shouldEncodeWithHeaders(headers)).isEqualTo(true); + + headers.clear(); + //Null parameter + assertThat(encodingHelper.shouldEncodeWithHeaders(null)).isEqualTo(true); + } + + @Test + public void testShouldEncodeWithMimeType(){ + RenderableURL renderable = Mockito.mock(RenderableURL.class); + + //Test all known Mime type + for(String mime : KnownMimeTypes.EXTENSIONS.values()){ + when(renderable.mimetype()).thenReturn(mime); + if(KnownMimeTypes.COMPRESSED_MIME.contains(mime)) + assertThat(encodingHelper.shouldEncodeWithMimeType(renderable)).isEqualTo(false); + else + assertThat(encodingHelper.shouldEncodeWithMimeType(renderable)).isEqualTo(true); + } + + //Null mimetype + when(renderable.mimetype()).thenReturn(null); + assertThat(encodingHelper.shouldEncodeWithMimeType(renderable)).isEqualTo(false); + + //Null renderable + assertThat(encodingHelper.shouldEncodeWithMimeType(null)).isEqualTo(false); + } + + @Test + public void testShouldEncodeWithSizeWithoutRoutes(){ + RenderableString renderable = Mockito.mock(RenderableString.class); + + //out of bounds + when(renderable.length()).thenReturn(Long.MIN_VALUE); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(Long.MAX_VALUE); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(0L); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(50L); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE + 1); + when(renderable.length()).thenReturn(Long.MIN_VALUE); + + when(renderable.length()).thenReturn(ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE - 1); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + //inside bounds + when(renderable.length()).thenReturn(1024 * 50L);// 50Ko + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(true); + + when(renderable.length()).thenReturn(ApplicationConfiguration.DEFAULT_ENCODING_MAX_SIZE); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(true); + + when(renderable.length()).thenReturn(ApplicationConfiguration.DEFAULT_ENCODING_MIN_SIZE); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(true); + + //Shouldn't encode -1 length + when(renderable.length()).thenReturn(-1L); + assertThat(encodingHelper.shouldEncodeWithSize(null, renderable)).isEqualTo(false); + + //Null renderable + assertThat(encodingHelper.shouldEncodeWithSize(null, null)).isEqualTo(false); + } + + @Test + public void testShouldEncodeWithSizeWithRoutes(){ + RenderableString renderable = Mockito.mock(RenderableString.class); + Route route = new RouteBuilder().route(HttpMethod.GET).on("/").to(new FakeSizeController(), "changeSize"); + + //out of bounds + when(renderable.length()).thenReturn(Long.MIN_VALUE); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(Long.MAX_VALUE); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(1L - 1); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(false); + + when(renderable.length()).thenReturn(1000000*1024L + 1); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(false); + + //in annotation bounds + when(renderable.length()).thenReturn(1L); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(true); + + when(renderable.length()).thenReturn(1000000*1024L); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(true); + + when(renderable.length()).thenReturn(1L + 1); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(true); + + when(renderable.length()).thenReturn(1000000*1024L - 1); + assertThat(encodingHelper.shouldEncodeWithSize(route, renderable)).isEqualTo(true); + } + + @Test + public void testShouldEncodeWithRouteDefaultTrue(){ + //Default is true ! + Controller deny = new FakeDenyController(); + Route routeDenyDeny = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "deny"); + Route routeDenyAllow = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "allow"); + Route routeDenyNo = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "noAnnotation"); + + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyDeny)).isEqualTo(false); + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyAllow)).isEqualTo(true); + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyNo)).isEqualTo(false); + + Controller allow = new FakeAllowController(); + Route routeAllowDeny = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "deny"); + Route routeAllowAllow = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "allow"); + Route routeAllowNo = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "noAnnotation"); + + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowDeny)).isEqualTo(false); + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowAllow)).isEqualTo(true); + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowNo)).isEqualTo(true); + + Route route = new RouteBuilder().route(HttpMethod.GET).on("/").to(new FakeSizeController(), "noAnnotation"); + + assertThat(encodingHelper.shouldEncodeWithRoute(route)).isEqualTo(true); + + assertThat(encodingHelper.shouldEncodeWithRoute(route)).isEqualTo(true); + } + + @Test + public void testShouldEncodeWithRouteDefaultFalse(){ + //Default is false + when(configuration.getBooleanWithDefault(anyString(), anyBoolean())).then(new Answer() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return false; + } + }); + Controller deny = new FakeDenyController(); + Route routeDenyDeny = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "deny"); + Route routeDenyAllow = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "allow"); + Route routeDenyNo = new RouteBuilder().route(HttpMethod.GET).on("/").to(deny, "noAnnotation"); + + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyDeny)).isEqualTo(false); + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyAllow)).isEqualTo(true); + assertThat(encodingHelper.shouldEncodeWithRoute(routeDenyNo)).isEqualTo(false); + + Controller allow = new FakeAllowController(); + Route routeAllowDeny = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "deny"); + Route routeAllowAllow = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "allow"); + Route routeAllowNo = new RouteBuilder().route(HttpMethod.GET).on("/").to(allow, "noAnnotation"); + + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowDeny)).isEqualTo(false); + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowAllow)).isEqualTo(true); + assertThat(encodingHelper.shouldEncodeWithRoute(routeAllowNo)).isEqualTo(true); + + Route route = new RouteBuilder().route(HttpMethod.GET).on("/").to(new FakeSizeController(), "noAnnotation"); + assertThat(encodingHelper.shouldEncodeWithRoute(route)).isEqualTo(false); + + assertThat(encodingHelper.shouldEncodeWithRoute(null)).isEqualTo(false); + } +} diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/FakeAllowController.java b/content-manager/src/test/java/org/wisdom/content/encoders/FakeAllowController.java new file mode 100644 index 000000000..54c561ec7 --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/FakeAllowController.java @@ -0,0 +1,22 @@ +package org.wisdom.content.encoders; + +import org.wisdom.api.DefaultController; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.annotations.encoder.DenyEncoding; +import org.wisdom.api.http.Result; + +@AllowEncoding +public class FakeAllowController extends DefaultController{ + @DenyEncoding + public Result deny(){ + return ok(); + } + @AllowEncoding + public Result allow(){ + return ok(); + } + + public Result noAnnotation(){ + return ok(); + } +} diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/FakeDenyController.java b/content-manager/src/test/java/org/wisdom/content/encoders/FakeDenyController.java new file mode 100644 index 000000000..ff3dab733 --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/FakeDenyController.java @@ -0,0 +1,22 @@ +package org.wisdom.content.encoders; + +import org.wisdom.api.DefaultController; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.annotations.encoder.DenyEncoding; +import org.wisdom.api.http.Result; + +@DenyEncoding +public class FakeDenyController extends DefaultController { + @DenyEncoding + public Result deny(){ + return ok(); + } + @AllowEncoding + public Result allow(){ + return ok(); + } + + public Result noAnnotation(){ + return ok(); + } +} diff --git a/content-manager/src/test/java/org/wisdom/content/encoders/FakeSizeController.java b/content-manager/src/test/java/org/wisdom/content/encoders/FakeSizeController.java new file mode 100644 index 000000000..62180789e --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/FakeSizeController.java @@ -0,0 +1,17 @@ +package org.wisdom.content.encoders; + +import org.wisdom.api.DefaultController; +import org.wisdom.api.annotations.encoder.AllowEncoding; +import org.wisdom.api.http.Result; + +public class FakeSizeController extends DefaultController{ + + @AllowEncoding(maxSize=1000000*1024, minSize=1) + public Result changeSize(){ + return ok(); + } + + public Result noAnnotation(){ + return ok(); + } +} From a817c1c2d0f85156edf92aa13128f5ae5085760c Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 10:33:06 +0100 Subject: [PATCH 32/35] Update documentation to match latest changes * Configuration keys * URL encoding * MimeType filtering * Cosmetic changes --- .../src/main/resources/assets/http.ad | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/documentation/src/main/resources/assets/http.ad b/documentation/src/main/resources/assets/http.ad index 23a8c8d94..0d1af7ef7 100644 --- a/documentation/src/main/resources/assets/http.ad +++ b/documentation/src/main/resources/assets/http.ad @@ -82,24 +82,41 @@ include::{sourcedir}/controllers/Redirect.java[tags=temporary-redirect] === Response encoding Wisdom can encode automatically responses content according to the client +Accept_Encoding+ header. The default behavior -is to automatically encode all traffic which size is between 1KB and 10MB. Encoders available are +gzip+ and +deflate+ +is to automatically encode all traffic which size is between 1KB and 10MB. Encoders currently available are +gzip+ and +deflate+ ==== Activate / Deactivate encoding -You can disable encoding globally by setting the key +global.encoding+ to false in the configuration file. +You can disable encoding globally by setting the key +encoding.global+ to false in the configuration file. Wisdom also provides 2 annotations allowing you to finely tune encoding behaviors. * +@DenyEncoding+ can be used to disable encoding on +Controllers+ and +Routes+ ; * +@AllowEncoding+ can be used to enable encoding on +Controllers+ and +Routes+. -In case of multiple instructions, priority is as follow : Route > Method > Configuration. +In case of multiple instructions, priority is as follow : +Route+ > +Method+ > +Configuration+. -=== Control activation threshold +==== Control encoding activation size By default, Wisdom encode contents which size are between 1KB and 10MB. If you want a different behavior, you can tune theses values with : -* configuration keys : +encoding.max.size+ and +encoding.min.size+ (in bytes) -* +@AllowEncoding+ parameters : +maxSize+ and +minSize+ (in bytes) +* +encoding.max.size+ and +encoding.min.size+ configuration keys (long value in bytes) +* +@AllowEncoding+ +maxSize+ and +minSize+ parameters (long value in bytes) + +==== MimeType filtering + +By default already encoded MimeTypes are not encoded by Wisdom. This include +audio/+, +video/+, +image/+ MimeType groups +as well as all common compressed MimeType (+7z+, +rar+, ...) + +==== URL encoding + +Content which size can't be found by Wisdom are not encoded, for performance reasons. Thus +Stream+ contents +are not encoded. + +We have an exception for +URL+, as we are able to determine MimeType. By default, URL content are encoded +but you can deactivate it by the global configuration key +encoding.url+. Setting this key to +false+ will +disable +URL+ content encoding (this include +assets+ and +resources+). + +If you want to deactivate URL content encoding on a particular +Route+ or +Controller+, you can still use @DenyEncoding to not encode +URL returned by theses routes. From d04764468ec5ce49d07699362ddc71fd60eac29b Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 16:14:21 +0100 Subject: [PATCH 33/35] Refactor AkkaBootstrap to handle multiple Callable types --- .../java/org/wisdom/akka/AkkaSystemService.java | 14 +++++++++----- .../java/org/wisdom/akka/impl/AkkaBootstrap.java | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/akka-system/src/main/java/org/wisdom/akka/AkkaSystemService.java b/akka-system/src/main/java/org/wisdom/akka/AkkaSystemService.java index 2aa1a8be3..b5fd4cea7 100644 --- a/akka-system/src/main/java/org/wisdom/akka/AkkaSystemService.java +++ b/akka-system/src/main/java/org/wisdom/akka/AkkaSystemService.java @@ -1,12 +1,14 @@ package org.wisdom.akka; -import akka.actor.ActorSystem; +import java.io.InputStream; +import java.util.concurrent.Callable; + import org.wisdom.api.http.Context; import org.wisdom.api.http.Result; + import scala.concurrent.ExecutionContext; import scala.concurrent.Future; - -import java.util.concurrent.Callable; +import akka.actor.ActorSystem; /** * A service to access the wisdom actor system and ease the dispatching of task. @@ -24,7 +26,7 @@ public interface AkkaSystemService { * @param context the context * @return the future */ - Future dispatch(Callable callable, Context context); + Future dispatchResultWithContext(Callable callable, Context context); /** * Dispatches the given task using an execution context preserving the current HTTP Context and the thread context @@ -32,7 +34,9 @@ public interface AkkaSystemService { * @param callable the classloader * @return the future */ - Future dispatch(Callable callable); + Future dispatchResult(Callable callable); + + Future dispatchInputStream(Callable callable); /** * Gets an Akka execution context preserving the HTTP Context and thread context classloader of the caller thread. diff --git a/akka-system/src/main/java/org/wisdom/akka/impl/AkkaBootstrap.java b/akka-system/src/main/java/org/wisdom/akka/impl/AkkaBootstrap.java index 947974ac3..14946daae 100644 --- a/akka-system/src/main/java/org/wisdom/akka/impl/AkkaBootstrap.java +++ b/akka-system/src/main/java/org/wisdom/akka/impl/AkkaBootstrap.java @@ -2,16 +2,20 @@ import akka.actor.ActorSystem; import akka.osgi.OsgiActorSystemFactory; + import com.typesafe.config.ConfigFactory; + import org.apache.felix.ipojo.annotations.*; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import org.wisdom.akka.AkkaSystemService; import org.wisdom.api.http.Context; import org.wisdom.api.http.Result; + import scala.concurrent.ExecutionContext; import scala.concurrent.Future; +import java.io.InputStream; import java.util.concurrent.Callable; @Component @@ -56,13 +60,21 @@ public ActorSystem system() { } @Override - public Future dispatch(Callable callable, Context context) { + public Future dispatchResultWithContext(Callable callable, Context context) { return akka.dispatch.Futures.future(callable, new HttpExecutionContext(system.dispatcher(), context, Thread.currentThread().getContextClassLoader())); } @Override - public Future dispatch(Callable callable) { + public Future dispatchResult(Callable callable) { + return akka.dispatch.Futures.future(callable, + new HttpExecutionContext(system.dispatcher(), Context.context.get(), + Thread.currentThread().getContextClassLoader + ())); + } + + @Override + public Future dispatchInputStream(Callable callable) { return akka.dispatch.Futures.future(callable, new HttpExecutionContext(system.dispatcher(), Context.context.get(), Thread.currentThread().getContextClassLoader From 4d916aeb7623c10ff1c5daab33cdaec97c336660 Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 16:17:00 +0100 Subject: [PATCH 34/35] return writeResponse results as it can holds Async informations --- .../main/java/org/wisdom/engine/server/WisdomHandler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index a745d05bb..7ed0609c0 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -251,19 +251,20 @@ private boolean dispatch(ChannelHandlerContext ctx) { } try { - writeResponse(ctx, request, context, result, true); + return writeResponse(ctx, request, context, result, true, false); } catch (Exception e) { LOGGER.error("Cannot write response", e); result = Results.internalServerError(e); try { - writeResponse(ctx, request, context, result, false); + return writeResponse(ctx, request, context, result, false, false); } catch (Exception e1) { LOGGER.error("Cannot even write the error response...", e1); // Ignore. } } finally { // Cleanup thread local - Context.context.remove(); + //TODO we can't remove as it can still be asynchronous (Content encoding) + //Context.context.remove(); } return false; } From 96ab1d611138baba2cf1757683ad7c787792770d Mon Sep 17 00:00:00 2001 From: Nicolas Rempulski Date: Thu, 23 Jan 2014 16:23:09 +0100 Subject: [PATCH 35/35] Refactor WisdomHandler to handle asynchronous content encoding * add fromAsync parameter to write / finalizeWrite to know is their call is from an async result and need cleanup at the end * Remove cleanup after AsyncResult handling as there could be an async content encoding running * Extract content encoding from process result to handle it in writeResponse * Add a proceedAsyncEncoding function to encode content InputStream asynchronously * FinalizeWrite now check if the call is from an Async function and if keepAlive is not set, cleanup this handler --- .../wisdom/engine/server/WisdomHandler.java | 119 +++++++++++++----- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java index 7ed0609c0..b06f5d558 100644 --- a/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java +++ b/wisdom-engine/src/main/java/org/wisdom/engine/server/WisdomHandler.java @@ -44,6 +44,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Map; +import java.util.concurrent.Callable; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; @@ -232,6 +233,7 @@ private boolean dispatch(ChannelHandlerContext ctx) { if (route == null) { // 3.1 : no route to destination + //TODO Something wrong here, we have to do renderable null checks on write and finalizeWrite functions after passing here LOGGER.info("No route to " + context.path()); result = Results.notFound(); for (ErrorHandler handler : accessor.handlers) { @@ -280,19 +282,21 @@ private boolean dispatch(ChannelHandlerContext ctx) { * @param context the context * @param result the async result */ - private void handleAsyncResult(final ChannelHandlerContext ctx, final HttpRequest request, final Context context, - AsyncResult result) { - Future future = accessor.system.dispatch(result.callable(), context); + private void handleAsyncResult( + final ChannelHandlerContext ctx, + final HttpRequest request, + final Context context, + AsyncResult result) { + Future future = accessor.system.dispatchResultWithContext(result.callable(), context); future.onComplete(new OnComplete() { public void onComplete(Throwable failure, Result result) { if (failure != null) { //We got a failure, handle it here - writeResponse(ctx, request, context, Results.internalServerError(failure), false); + writeResponse(ctx, request, context, Results.internalServerError(failure), false, true); } else { // We got a result, write it here. - writeResponse(ctx, request, context, result, true); + writeResponse(ctx, request, context, result, true, true); } - cleanup(); } }, accessor.system.fromThread()); } @@ -320,8 +324,31 @@ private InputStream processResult(Result result) throws Exception { serializer.serialize(renderable); } } - - InputStream processedResult = renderable.render(context, result); + return renderable.render(context, result); + } + + private boolean writeResponse( + final ChannelHandlerContext ctx, + final HttpRequest request, Context context, + Result result, + boolean handleFlashAndSessionCookie, + boolean fromAsync) { + //TODO Refactor this method. + + // Render the result. + InputStream stream; + boolean success = true; + Renderable renderable = result.getRenderable(); + if (renderable == null) { + renderable = new NoHttpBody(); + } + try { + stream = processResult(result); + } catch (Exception e) { + LOGGER.error("Cannot render the response to " + request.getUri(), e); + stream = new ByteArrayInputStream(NoHttpBody.EMPTY); + success = false; + } if(accessor.content_engines.getContentEncodingHelper().shouldEncode(context, result, renderable)){ ContentCodec codec = null; @@ -332,42 +359,66 @@ private InputStream processResult(Result result) throws Exception { break; } - if(codec != null){ - processedResult = codec.encode(processedResult); + if(codec != null){ // Encode Async + proceedAsyncEncoding(codec, stream, ctx, result, success, handleFlashAndSessionCookie, fromAsync); result.with(CONTENT_ENCODING, codec.getEncodingType()); + return true; } + //No encoding possible, do the finalize } - return processedResult; + + return finalizeWriteReponse(ctx, result, stream, success, handleFlashAndSessionCookie, fromAsync); } - private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest request, Context context, - Result result, - boolean handleFlashAndSessionCookie) { - //TODO Refactor this method. - - // Decide whether to close the connection or not. - boolean keepAlive = isKeepAlive(request); + private void proceedAsyncEncoding( + final ContentCodec codec, + final InputStream stream, + final ChannelHandlerContext ctx, + final Result result, + final boolean success, + final boolean handleFlashAndSessionCookie, + final boolean fromAsync){ + + + Future future = accessor.system.dispatchInputStream(new Callable() { + @Override + public InputStream call() throws Exception { + return codec.encode(stream); + } + }); + future.onComplete(new OnComplete(){ + + @Override + public void onComplete(Throwable arg0, InputStream encodedStream) + throws Throwable { + finalizeWriteReponse(ctx, result, encodedStream, success, handleFlashAndSessionCookie, true); + } + + }, accessor.system.fromThread()); + } - // Render the result. - InputStream stream; - boolean success = true; - Renderable renderable = result.getRenderable(); + private boolean finalizeWriteReponse( + final ChannelHandlerContext ctx, + Result result, + InputStream stream, + boolean success, + boolean handleFlashAndSessionCookie, + boolean fromAsync){ + + Renderable renderable = result.getRenderable(); if (renderable == null) { renderable = new NoHttpBody(); } - try { - stream = processResult(result); - } catch (Exception e) { - LOGGER.error("Cannot render the response to " + request.getUri(), e); - stream = new ByteArrayInputStream(NoHttpBody.EMPTY); - success = false; - } final InputStream content = stream; - + // Decide whether to close the connection or not. + boolean keepAlive = isKeepAlive(request); + // Build the response object. HttpResponse response; Object res; - final boolean isChunked = renderable.mustBeChunked(); + + boolean isChunked = renderable.mustBeChunked(); + if (isChunked) { response = new DefaultHttpResponse(request.getProtocolVersion(), getStatusFromResult(result, success)); if (renderable.length() > 0) { @@ -451,9 +502,11 @@ public void operationComplete(ChannelFuture channelFuture) throws Exception { writeFuture.addListener(ChannelFutureListener.CLOSE); } } - + if(fromAsync && !keepAlive){ + cleanup(); + return true;// No matter, no one handle it + } return keepAlive; - } private HttpResponseStatus getStatusFromResult(Result result, boolean success) {