diff --git a/incubator/brotli/pom.xml b/incubator/brotli/pom.xml new file mode 100644 index 00000000000..a2a448b4e10 --- /dev/null +++ b/incubator/brotli/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + 2.38-SNAPSHOT + + project + org.glassfish.jersey + 2.38-SNAPSHOT + ../../pom.xml + + + org.glassfish.jersey.message + jersey-brotli + jar + brotli + + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + com.oracle.brotli + brotli + 1.0.0-SNAPSHOT + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-external + ${project.version} + test + + + org.junit.jupiter + junit-jupiter + test + + + org.hamcrest + hamcrest + test + + + + \ No newline at end of file diff --git a/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java b/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java new file mode 100644 index 00000000000..92083a60887 --- /dev/null +++ b/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java @@ -0,0 +1,37 @@ +package org.glassfish.jersey.message; + +import com.oracle.brotli.decoder.BrotliInputStream; +import com.oracle.brotli.encoder.BrotliOutputStream; +import org.glassfish.jersey.spi.ContentEncoder; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.HttpHeaders; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Brotli encoding support. Interceptor that encodes the output or decodes the input if + * {@link HttpHeaders#CONTENT_ENCODING Content-Encoding header} value equals to {@code br}. + */ +@Priority(Priorities.ENTITY_CODER) +public class BrotliEncoder extends ContentEncoder { + + /** + * Initialize BrotliEncoder. + */ + public BrotliEncoder() { + super("br"); + } + + @Override + public InputStream decode(String contentEncoding, InputStream encodedStream) throws IOException { + return BrotliInputStream.builder().inputStream(encodedStream).build(); + } + + @Override + public OutputStream encode(String contentEncoding, OutputStream entityStream) throws IOException { + return BrotliOutputStream.builder().outputStream(entityStream).build(); + } +} diff --git a/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java new file mode 100644 index 00000000000..3974a21f555 --- /dev/null +++ b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java @@ -0,0 +1,98 @@ +package org.glassfish.jersey.message; + +import com.oracle.brotli.decoder.BrotliInputStream; +import com.oracle.brotli.encoder.BrotliOutputStream; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BrotliEncoderTest { + + @Test + public void testEncode() throws IOException { + test(new TestSpec() { + @Override + public OutputStream getEncoded(OutputStream stream) throws IOException { + return new BrotliEncoder().encode("br", stream); + } + + @Override + public InputStream getDecoded(InputStream stream) throws IOException { + return BrotliInputStream.builder().inputStream(stream).build(); + } + }); + } + + @Test + public void testDecode() throws IOException { + test(new TestSpec() { + @Override + public OutputStream getEncoded(OutputStream stream) throws IOException { + return BrotliOutputStream.builder().outputStream(stream).build(); + } + + @Override + public InputStream getDecoded(InputStream stream) throws IOException { + return new BrotliEncoder().decode("br", stream); + } + }); + } + + @Test + public void testEncodeDecode() throws IOException { + test(new TestSpec() { + @Override + public OutputStream getEncoded(OutputStream stream) throws IOException { + return new BrotliEncoder().encode("br", stream); + } + + @Override + public InputStream getDecoded(InputStream stream) throws IOException { + return new BrotliEncoder().decode("br", stream); + } + }); + } + + void test(TestSpec testSpec) throws IOException { + byte[] entity = "Hello world!".getBytes(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStream encoded = testSpec.getEncoded(baos); + encoded.write(entity); + encoded.close(); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + byte[] result = new byte[entity.length]; + InputStream decoded = testSpec.getDecoded(bais); + int len = decoded.read(result); + assertEquals(-1, decoded.read()); + decoded.close(); + assertEquals(entity.length, len); + assertArrayEquals(entity, result); + } + + interface TestSpec { + /** + * Returns encoded stream. + * + * @param stream Original stream. + * @return Encoded stream. + * @throws IOException I/O exception. + */ + OutputStream getEncoded(OutputStream stream) throws IOException; + + /** + * Returns decoded stream. + * + * @param stream Original stream. + * @return Decoded stream. + * @throws IOException I/O exception. + */ + InputStream getDecoded(InputStream stream) throws IOException; + } +} diff --git a/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java new file mode 100644 index 00000000000..15731c028d0 --- /dev/null +++ b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java @@ -0,0 +1,95 @@ +package org.glassfish.jersey.message; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.filter.EncodingFilter; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.external.ExternalTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BrotliITTest extends JerseyTest { + + private static final String CONTENT_ENCODING = "Content-Encoding"; + private static final String BR = "br"; + + @Override + protected Application configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + return new MyApplication(); + } + + @Override + protected TestContainerFactory getTestContainerFactory() throws TestContainerException { + return new ExternalTestContainerFactory(); + } + + protected void assertHtmlResponse(final String response) { + assertNotNull(response, "No text returned!"); + + assertResponseContains(response, ""); + assertResponseContains(response, ""); + } + + protected void assertResponseContains(final String response, final String text) { + assertTrue(response.contains(text), "Response should contain " + text + " but was: " + response); + } + + @Test + public void testString() throws Exception { + Response response = target("/client/string") + .register(BrotliEncoder.class) + .request("text/html") + .acceptEncoding(BR) + .get(); + String resp = response.readEntity(String.class); + assertResponseContains(resp, "string string string string string string"); + assertEquals(BR, response.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testJsp() throws Exception { + Response response = target("/client/html") + .register(BrotliEncoder.class) + .request("text/html", "application/xhtml+xml", "application/xml;q=0.9", "*/*;q=0.8") + .acceptEncoding(BR) + .get(); + String resp = response.readEntity(String.class); + assertHtmlResponse(resp); + assertResponseContains(resp, "find this string"); + assertEquals(BR, response.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testJspNotDecoded() throws Exception { + Response response = target("/client/html") + .request("text/html", "application/xhtml+xml", "application/xml;q=0.9", "*/*;q=0.8") + .acceptEncoding(BR) + .get(); + String resp = response.readEntity(String.class); + assertFalse(resp.contains("find this string")); + assertEquals(BR, response.getHeaderString(CONTENT_ENCODING)); + } + + class MyApplication extends ResourceConfig { + + public MyApplication() { + property("jersey.config.server.mvc.templateBasePath.jsp", "/WEB-INF/jsp"); + property("jersey.config.servlet.filter.forwardOn404", "true"); + property("jersey.config.servlet.filter.staticContentRegex", "/WEB-INF/.*\\.jsp"); + packages(MyApplication.class.getPackage().getName()); + EncodingFilter.enableFor(this, new Class[] {BrotliEncoder.class}); + } + } + +} \ No newline at end of file diff --git a/incubator/pom.xml b/incubator/pom.xml index 1a362714fac..98566634fed 100644 --- a/incubator/pom.xml +++ b/incubator/pom.xml @@ -42,6 +42,7 @@ html-json kryo open-tracing + brotli