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 091106b5b..50ba0d2e0 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); /** * Dispatches the given task. The task is executed using the given execution context. 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 bb8320ea1..ea6428a24 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 @@ -1,18 +1,25 @@ package org.wisdom.akka.impl; -import akka.actor.ActorSystem; -import akka.osgi.OsgiActorSystemFactory; -import com.typesafe.config.ConfigFactory; -import org.apache.felix.ipojo.annotations.*; +import java.io.InputStream; +import java.util.concurrent.Callable; + +import org.apache.felix.ipojo.annotations.Component; +import org.apache.felix.ipojo.annotations.Instantiate; +import org.apache.felix.ipojo.annotations.Invalidate; +import org.apache.felix.ipojo.annotations.Provides; +import org.apache.felix.ipojo.annotations.Validate; 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 akka.actor.ActorSystem; +import akka.osgi.OsgiActorSystemFactory; -import java.util.concurrent.Callable; +import com.typesafe.config.ConfigFactory; @Component @Provides @@ -56,13 +63,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 diff --git a/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfigurationImpl.java b/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfigurationImpl.java index 2d6fa8ac2..97cf6006e 100644 --- a/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfigurationImpl.java +++ b/application-configuration/src/main/java/org/wisdom/configuration/ApplicationConfigurationImpl.java @@ -1,18 +1,14 @@ package org.wisdom.configuration; -import org.apache.commons.configuration.ConfigurationConverter; +import java.io.File; + import org.apache.commons.configuration.ConfigurationException; -import org.apache.commons.configuration.MapConfiguration; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Instantiate; import org.apache.felix.ipojo.annotations.Provides; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.wisdom.api.configuration.Configuration; - -import java.io.File; -import java.util.*; /** * Implementation of the configuration service reading application/conf and an external (optional) property. @@ -24,9 +20,6 @@ public class ApplicationConfigurationImpl extends ConfigurationImpl implements o .ApplicationConfiguration { public static final String APPLICATION_CONFIGURATION = "application.configuration"; - static final String ERROR_KEYNOTFOUND = "Key %s does not exist. Please include it in your application.conf. " + - "Otherwise this application will not work"; - static final String ERROR_NOSUCHKEY = "No such key \""; private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationConfigurationImpl.class); private final Mode mode; private final File baseDirectory; @@ -145,6 +138,20 @@ public Boolean getBoolean(String key) { } return r; } + + /** + * @param key the key + * @return the property or null if not there or property no Long + */ + @Override + public Long getLong(String key) { + Long r = super.getLong(key); + if (r == null) { + LOGGER.error(ERROR_NOSUCHKEY + key + "\""); + return null; + } + return r; + } /** * Whether we are in dev mode diff --git a/application-configuration/src/main/java/org/wisdom/configuration/ConfigurationImpl.java b/application-configuration/src/main/java/org/wisdom/configuration/ConfigurationImpl.java index 769734198..9a156f51b 100644 --- a/application-configuration/src/main/java/org/wisdom/configuration/ConfigurationImpl.java +++ b/application-configuration/src/main/java/org/wisdom/configuration/ConfigurationImpl.java @@ -11,6 +11,10 @@ * Unlike the main application configuration, this implementation does not used a logger. */ public class ConfigurationImpl implements Configuration { + + private static final String ERROR_KEYNOTFOUND = "Key %s does not exist. Please include it in your application.conf. " + + "Otherwise this application will not work"; + protected static final String ERROR_NOSUCHKEY = "No such key \""; private org.apache.commons.configuration.Configuration configuration; @@ -137,6 +141,39 @@ public Boolean getBooleanWithDefault(String key, Boolean defaultValue) { return configuration.getBoolean(key, defaultValue); } } + + @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) { + throw new IllegalArgumentException(String.format(ERROR_KEYNOTFOUND, key)); + } else { + return value; + } + } /** * The "die" method forces this key to be set. Otherwise a runtime exception @@ -150,7 +187,7 @@ public Boolean getBooleanOrDie(String key) { Boolean value = getBoolean(key); if (value == null) { - throw new IllegalArgumentException(String.format(ApplicationConfigurationImpl.ERROR_KEYNOTFOUND, key)); + throw new IllegalArgumentException(String.format(ERROR_KEYNOTFOUND, key)); } else { return value; } @@ -168,7 +205,7 @@ public Integer getIntegerOrDie(String key) { Integer value = getInteger(key); if (value == null) { - throw new IllegalArgumentException(String.format(ApplicationConfigurationImpl.ERROR_KEYNOTFOUND, key)); + throw new IllegalArgumentException(String.format(ERROR_KEYNOTFOUND, key)); } else { return value; } @@ -186,7 +223,7 @@ public String getOrDie(String key) { String value = get(key); if (value == null) { - throw new IllegalArgumentException(String.format(ApplicationConfigurationImpl.ERROR_KEYNOTFOUND, key)); + throw new IllegalArgumentException(String.format(ERROR_KEYNOTFOUND, 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 de918f5bc..73f95dac7 100644 --- a/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java +++ b/application-configuration/src/test/java/org/wisdom/configuration/ApplicationConfigurationTest.java @@ -1,13 +1,14 @@ package org.wisdom.configuration; -import org.junit.After; -import org.junit.Test; -import org.wisdom.api.configuration.Configuration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import java.io.File; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import org.junit.After; +import org.junit.Test; +import org.wisdom.api.configuration.ApplicationConfiguration; +import org.wisdom.api.configuration.Configuration; /** * Check the configuration management behavior. @@ -84,6 +85,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(ApplicationConfigurationImpl.APPLICATION_CONFIGURATION, "target/test-classes/conf/regular.conf"); + ApplicationConfiguration configuration = new ApplicationConfigurationImpl(); + 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() { @@ -198,7 +209,7 @@ public void testEmptySubConfigurations() { @Test public void testAllAndProperties() { - final int numberOfPropertiesStartingWithKey = 8; + final int numberOfPropertiesStartingWithKey = 9; System.setProperty(ApplicationConfigurationImpl.APPLICATION_CONFIGURATION, "target/test-classes/conf/regular.conf"); ApplicationConfigurationImpl configuration = new ApplicationConfigurationImpl(); assertThat(configuration).isNotNull(); diff --git a/application-configuration/src/test/resources/conf/regular.conf b/application-configuration/src/test/resources/conf/regular.conf index c82df263c..df454cb66 100644 --- a/application-configuration/src/test/resources/conf/regular.conf +++ b/application-configuration/src/test/resources/conf/regular.conf @@ -29,3 +29,5 @@ key.array = a,b,c other.conf = a_file.txt +key.long = 9999999999999 + diff --git a/content-manager/pom.xml b/content-manager/pom.xml index d7a8f52c7..0ffe8bb03 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/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 new file mode 100644 index 000000000..cc2b25fe4 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/codecs/DeflateCodec.java @@ -0,0 +1,35 @@ +package org.wisdom.content.codecs; + +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +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.http.EncodingNames; + +@Component +@Instantiate +@Provides +public class DeflateCodec extends AbstractDefInfCodec { + + @Override + public String getEncodingType() { + return EncodingNames.DEFLATE; + } + + @Override + 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 new file mode 100644 index 000000000..36ba7e212 --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/codecs/GzipCodec.java @@ -0,0 +1,37 @@ +package org.wisdom.content.codecs; + +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.InflaterInputStream; + +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.http.EncodingNames; + +@Component +@Instantiate +@Provides +public class GzipCodec extends AbstractDefInfCodec { + + @Override + public String getEncodingType(){ + return EncodingNames.GZIP; + } + + @Override + public String getContentEncodingHeaderValue() { + return EncodingNames.GZIP; + } + + @Override + public Class getEncoderClass() { + return GZIPOutputStream.class; + } + + @Override + public Class getDecoderClass() { + return GZIPInputStream.class; + } +} diff --git a/content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java b/content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java new file mode 100644 index 000000000..a4e2aabbd --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/codecs/IdentityCodec.java @@ -0,0 +1,36 @@ +package org.wisdom.content.codecs; + +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.ContentCodec; +import org.wisdom.api.http.EncodingNames; + +@Component +@Instantiate +@Provides +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() { + return EncodingNames.IDENTITY; + } + + @Override + public String getContentEncodingHeaderValue() { + return null; + } +} 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..2bacdf13a --- /dev/null +++ b/content-manager/src/main/java/org/wisdom/content/encoding/ContentEncodingHelperImpl.java @@ -0,0 +1,294 @@ +package org.wisdom.content.encoding; + +import java.util.ArrayList; +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; +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.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; + +@Component +@Instantiate +@Provides +public class ContentEncodingHelperImpl implements ContentEncodingHelper{ + + @Requires(specification=ApplicationConfiguration.class, optional=false) + ApplicationConfiguration configuration; + + Boolean allowEncodingGlobalSetting = null; + + Boolean allowUrlEncodingGlobalSetting = null; + + Long maxSizeGlobalSetting = null; + + 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); + return allowEncodingGlobalSetting; + } + + public boolean getAllowUrlEncodingGlobalSetting(){ + if(allowUrlEncodingGlobalSetting == null) + allowUrlEncodingGlobalSetting = configuration.getBooleanWithDefault(ApplicationConfiguration.ENCODING_URL, ApplicationConfiguration.DEFAULT_ENCODING_URL); + return allowUrlEncodingGlobalSetting; + } + + 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(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 + //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 + 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){ + //TODO What to do when we can't know the mime type ? drop or allow ? + //Drop on unknown mime type + return false; + } + + if(KnownMimeTypes.COMPRESSED_MIME.contains(mime)){ + return false; + } + + return true; + } + + @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 url, return config value + if(renderable instanceof RenderableURL){ + return getAllowUrlEncodingGlobalSetting(); + } + // Not an URL 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 aa41d6bb0..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 @@ -5,6 +5,8 @@ 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.ContentCodec; +import org.wisdom.api.content.ContentEncodingHelper; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ContentSerializer; import org.slf4j.LoggerFactory; @@ -23,6 +25,10 @@ public class Engine implements ContentEngine { List parsers; @Requires(specification = ContentSerializer.class, optional = true) List serializers; + @Requires(specification = ContentCodec.class, optional = true) + List encoders; + @Requires(specification = ContentEncodingHelper.class, optional = true) + ContentEncodingHelper encodingHelper; @Override public BodyParser getBodyParserEngineForContentType(String contentType) { @@ -45,4 +51,19 @@ public ContentSerializer getContentSerializerForContentType(String contentType) LoggerFactory.getLogger(this.getClass()).info("Cannot find a content renderer handling " + contentType); return null; } + + @Override + public ContentCodec getContentCodecForEncodingType(String encoding) { + for (ContentCodec codec : encoders) { + if (codec.getEncodingType().equals(encoding)) { + return codec; + } + } + return null; + } + + @Override + public ContentEncodingHelper getContentEncodingHelper() { + return encodingHelper; + } } 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..c469d2fbc --- /dev/null +++ b/content-manager/src/test/java/org/wisdom/content/encoders/CodecsTest.java @@ -0,0 +1,79 @@ +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; +import org.wisdom.content.codecs.DeflateCodec; +import org.wisdom.content.codecs.GzipCodec; +import org.wisdom.content.codecs.IdentityCodec; + +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; + } +} 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(); + } +} diff --git a/documentation/src/main/resources/assets/http.ad b/documentation/src/main/resources/assets/http.ad index f0e2e1c77..b2b7152b4 100644 --- a/documentation/src/main/resources/assets/http.ad +++ b/documentation/src/main/resources/assets/http.ad @@ -78,3 +78,45 @@ 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 currently available are +gzip+ and +deflate+ + +==== Activate / Deactivate encoding + +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+. + +==== 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 : + +* +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. + 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..190db54ba --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/annotations/encoder/AllowEncoding.java @@ -0,0 +1,13 @@ +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 { + long maxSize() default -1; + long minSize() default -1; +} 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 { + +} 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 119d79d31..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 @@ -3,6 +3,7 @@ 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; @@ -50,7 +51,19 @@ public RenderableString(String object, String type) { @Override public InputStream render(Context context, Result result) throws Exception { - return new ByteArrayInputStream(rendered.getBytes()); + 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(bytes); } @Override 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 915853064..2acdf06b8 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 @@ -2,7 +2,6 @@ package org.wisdom.api.configuration; import java.io.File; -import java.util.Properties; /** * Service interface to access application configuration. @@ -29,7 +28,35 @@ public interface ApplicationConfiguration extends Configuration { * The HTTPS port key. */ String HTTPS_PORT = "https.port"; + + /** + * The global encoding activation key. + */ + String ENCODING_GLOBAL = "encoding.global"; + + boolean DEFAULT_ENCODING_GLOBAL = true; + + /** + * 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"; + + long DEFAULT_ENCODING_MAX_SIZE = 10000 * 1024; // 10Mo + + long DEFAULT_ENCODING_MIN_SIZE = 10 * 1024; //10Ko + + /** + * The global url encoding activation key. + */ + String ENCODING_URL = "encoding.url"; + + boolean DEFAULT_ENCODING_URL = true; + /** * Gets the base directory of the Wisdom application. * @return the base directory diff --git a/wisdom-api/src/main/java/org/wisdom/api/configuration/Configuration.java b/wisdom-api/src/main/java/org/wisdom/api/configuration/Configuration.java index 997b90bb6..8335a50c4 100644 --- a/wisdom-api/src/main/java/org/wisdom/api/configuration/Configuration.java +++ b/wisdom-api/src/main/java/org/wisdom/api/configuration/Configuration.java @@ -72,6 +72,36 @@ public interface Configuration { * @return the value of the key or the default value. */ Boolean getBooleanWithDefault(String key, Boolean 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); + + /** + * 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 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 new file mode 100644 index 000000000..2c05110bb --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentCodec.java @@ -0,0 +1,36 @@ +package org.wisdom.api.content; + +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(); + +} 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..c3dc04b9f --- /dev/null +++ b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEncodingHelper.java @@ -0,0 +1,29 @@ +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 { + + /** + * 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(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-api/src/main/java/org/wisdom/api/content/ContentEngine.java b/wisdom-api/src/main/java/org/wisdom/api/content/ContentEngine.java index c1db52772..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 @@ -5,5 +5,6 @@ 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/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}; +} 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 09b0dc884..0dd3ea386 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; @@ -561,6 +563,58 @@ private KnownMimeTypes(){ 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); + } + } + } + } + public static String getMimeTypeByExtension(String extension) { return EXTENSIONS.get(extension); } 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 ae6cc4fef..d27840d8c 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; @@ -29,8 +47,25 @@ import java.util.Map; import java.util.concurrent.Callable; -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.content.ContentCodec; +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.engine.wrapper.ContextFromNetty; +import org.wisdom.engine.wrapper.cookies.CookieHelper; + +import scala.concurrent.Future; +import akka.dispatch.OnComplete; /** * The Wisdom Channel Handler. @@ -260,12 +295,16 @@ 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()); + // If we open a websocket in the same request, just ignore it. if (handshaker != null) { return false; } LOGGER.info("No route to serve {} {}", context.request().method(), context.path()); + result = Results.notFound(); for (ErrorHandler handler : accessor.getHandlers()) { result = handler.onNoRoute( @@ -284,19 +323,21 @@ 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; } @@ -312,25 +353,30 @@ 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.getSystem().dispatch(result.callable(), context); + + private void handleAsyncResult( + final ChannelHandlerContext ctx, + final HttpRequest request, + final Context context, + AsyncResult result) { + Future future = accessor.getSystem().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.getSystem().fromThread()); } private InputStream processResult(Result result) throws Exception { Renderable renderable = result.getRenderable(); + if (renderable == null) { renderable = new NoHttpBody(); } @@ -353,15 +399,15 @@ private InputStream processResult(Result result) throws Exception { } return renderable.render(context, result); } - - private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest request, Context context, - Result result, - boolean handleFlashAndSessionCookie) { + + private boolean writeResponse( + final ChannelHandlerContext ctx, + final HttpRequest request, Context context, + Result result, + boolean handleFlashAndSessionCookie, + boolean fromAsync) { //TODO Refactor this method. - // Decide whether to close the connection or not. - boolean keepAlive = isKeepAlive(request); - // Render the result. InputStream stream; boolean success = true; @@ -376,12 +422,76 @@ private boolean writeResponse(final ChannelHandlerContext ctx, final HttpRequest stream = new ByteArrayInputStream(NoHttpBody.EMPTY); success = false; } - final InputStream content = stream; + + if(accessor.getContentEngines().getContentEncodingHelper().shouldEncode(context, result, renderable)){ + ContentCodec codec = null; + + for(String encoding : accessor.getContentEngines().getContentEncodingHelper().parseAcceptEncodingHeader(context.request().getHeader(HeaderNames.ACCEPT_ENCODING))){ + codec = accessor.getContentEngines().getContentCodecForEncodingType(encoding); + if(codec != null) + break; + } + + 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 finalizeWriteReponse(ctx, result, stream, success, handleFlashAndSessionCookie, fromAsync); + } + + 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.getSystem().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.getSystem().fromThread()); + } + 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(); + } + 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) { @@ -465,9 +575,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) { 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..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 @@ -1,14 +1,31 @@ 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 java.util.Map; + 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; +import org.wisdom.api.http.Context; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Renderable; import org.wisdom.api.http.Result; @@ -16,16 +33,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 +58,50 @@ public void testServerStartSequence() throws InterruptedException, IOException { // Prepare an empty router. Router router = mock(Router.class); + + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + + @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; + } + + @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); // Configure the server. server = new WisdomServer(new ServiceAccessor( null, configuration, router, - null, + contentEngine, null, Collections.emptyList(), null @@ -91,13 +135,50 @@ public Result index() { .on("/") .to(controller, "index"); when(router.getRouteFor("GET", "/")).thenReturn(route); + + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + + @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; + } + + @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); // Configure the server. server = new WisdomServer(new ServiceAccessor( null, configuration, router, - null, + contentEngine, null, Collections.emptyList(), null @@ -144,7 +225,42 @@ public void serialize(Renderable renderable) { } } }; + ContentEncodingHelper encodingHelper = new ContentEncodingHelper() { + + @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; + } + + @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); when(contentEngine.getContentSerializerForContentType(anyString())).thenReturn(serializer); // Configure the server.