From f53dd6371022338073c9f3ea525f294ca3c6e18f Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Thu, 5 Dec 2024 13:37:21 +0100
Subject: [PATCH 01/20] Fixes #12612 - Use Compression classes for client
decoding.
Now using Compression classes for client response decoding.
Removed oej.client.GZIPContentDecoder, now using only oeh.http.GZIPContentDecoder where necessary.
Signed-off-by: Simone Bordet
---
jetty-core/jetty-client/pom.xml | 8 +
.../src/main/java/module-info.java | 2 +
.../eclipse/jetty/client/ContentDecoder.java | 100 ++++----
.../jetty/client/GZIPContentDecoder.java | 135 -----------
.../org/eclipse/jetty/client/HttpClient.java | 25 +-
.../jetty/client/transport/HttpReceiver.java | 218 +++++++++---------
.../internal/HttpReceiverOverHTTP.java | 2 +-
...HttpClientContentDecoderFactoriesTest.java | 48 ++--
.../brotli/internal/BrotliDecoderSource.java | 13 +-
.../jetty/compression/DecoderSource.java | 116 +---------
.../compression/gzip/GzipCompression.java | 2 +-
.../compression/gzip/GzipDecoderConfig.java | 10 +-
.../gzip/internal/GzipDecoderSource.java | 90 ++++----
.../zstandard/ZstandardCompression.java | 2 +-
.../internal/ZstandardDecoderSource.java | 16 +-
.../io/content/ContentSourceTransformer.java | 153 +++++++-----
.../io/ContentSourceTransformerTest.java | 14 +-
.../ee10/proxy/AsyncMiddleManServlet.java | 23 +-
.../ee11/proxy/AsyncMiddleManServlet.java | 23 +-
.../ee9/proxy/AsyncMiddleManServlet.java | 23 +-
20 files changed, 454 insertions(+), 569 deletions(-)
delete mode 100644 jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/GZIPContentDecoder.java
diff --git a/jetty-core/jetty-client/pom.xml b/jetty-core/jetty-client/pom.xml
index a04f1b5cf9b7..2143de36defa 100644
--- a/jetty-core/jetty-client/pom.xml
+++ b/jetty-core/jetty-client/pom.xml
@@ -33,6 +33,14 @@
jetty-jmx
true
+
+ org.eclipse.jetty.compression
+ jetty-compression-common
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-gzip
+
org.slf4j
slf4j-api
diff --git a/jetty-core/jetty-client/src/main/java/module-info.java b/jetty-core/jetty-client/src/main/java/module-info.java
index ab16064533c2..69e26d46e12f 100644
--- a/jetty-core/jetty-client/src/main/java/module-info.java
+++ b/jetty-core/jetty-client/src/main/java/module-info.java
@@ -16,6 +16,8 @@
requires org.eclipse.jetty.alpn.client;
requires org.slf4j;
+ requires transitive org.eclipse.jetty.compression;
+ requires transitive org.eclipse.jetty.compression.gzip;
requires transitive org.eclipse.jetty.http;
// Only required if using JMX.
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
index 474d3515fd1c..2e94348f70fb 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
@@ -13,68 +13,50 @@
package org.eclipse.jetty.client;
-import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Objects;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.io.RetainableByteBuffer;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
/**
- * {@link ContentDecoder} decodes content bytes of a response.
+ * Groups abstractions related to response content decoding.
*
* @see Factory
+ * @see Factories
+ * @see HttpClient#getContentDecoderFactories()
*/
public interface ContentDecoder
{
/**
- * Processes the response just before the decoding of the response content.
- * Typical processing may involve modifying the response headers, for example
- * by temporarily removing the {@code Content-Length} header, or modifying the
- * {@code Content-Encoding} header.
+ * A factory for {@link Content.Source} that decode response content.
+ * A {@code Factory} has an {@link #getEncoding() encoding} and a
+ * {@link #getWeight() weight} that are used in the {@code Accept-Encoding}
+ * request header and in the {@code Content-Encoding} response headers.
+ * {@code Factory} instances are configured in {@link HttpClient} via
+ * {@link HttpClient#getContentDecoderFactories()}.
*/
- public default void beforeDecoding(Response response)
- {
- }
-
- /**
- * Decodes the bytes in the given {@code buffer} and returns the decoded bytes.
- * The returned {@link RetainableByteBuffer} will eventually be released via
- * {@link RetainableByteBuffer#release()} by the code that called this method.
- *
- * @param buffer the buffer containing encoded bytes
- * @return a buffer containing decoded bytes
- */
- public abstract RetainableByteBuffer decode(ByteBuffer buffer);
-
- /**
- * Processes the exchange after the response content has been decoded.
- * Typical processing may involve modifying the response headers, for example
- * updating the {@code Content-Length} header to the length of the decoded
- * response content.
- */
- public default void afterDecoding(Response response)
- {
- }
-
- /**
- * Factory for {@link ContentDecoder}s; subclasses must implement {@link #newContentDecoder()}.
- *
- * {@link Factory} have an {@link #getEncoding() encoding}, which is the string used in
- * {@code Accept-Encoding} request header and in {@code Content-Encoding} response headers.
- *
- * {@link Factory} instances are configured in {@link HttpClient} via
- * {@link HttpClient#getContentDecoderFactories()}.
- */
- public abstract static class Factory
+ abstract class Factory extends ContainerLifeCycle
{
private final String encoding;
+ private final float weight;
protected Factory(String encoding)
{
- this.encoding = encoding;
+ this(encoding, -1F);
+ }
+
+ protected Factory(String encoding, float weight)
+ {
+ this.encoding = Objects.requireNonNull(encoding);
+ if (weight != -1F && !(weight >= 0F && weight <= 1F))
+ throw new IllegalArgumentException("Invalid weight: " + weight);
+ this.weight = weight;
}
/**
@@ -85,6 +67,14 @@ public String getEncoding()
return encoding;
}
+ /**
+ * @return the weight (between 0 and 1, at most 3 decimal digits) to use for the {@code Accept-Encoding} request header
+ */
+ public float getWeight()
+ {
+ return weight;
+ }
+
@Override
public boolean equals(Object obj)
{
@@ -102,14 +92,16 @@ public int hashCode()
}
/**
- * Factory method for {@link ContentDecoder}s
+ *
Creates a {@link Content.Source} that decodes the
+ * chunks of the given {@link Content.Source} parameter.
*
- * @return a new instance of a {@link ContentDecoder}
+ * @param contentSource the encoded {@link Content.Source}
+ * @return the decoded {@link Content.Source}
*/
- public abstract ContentDecoder newContentDecoder();
+ public abstract Content.Source newDecoderContentSource(Content.Source contentSource);
}
- public static class Factories implements Iterable
+ class Factories extends ContainerLifeCycle implements Iterable
{
private final Map factories = new LinkedHashMap<>();
private HttpField acceptEncodingField;
@@ -134,8 +126,20 @@ public void clear()
public Factory put(Factory factory)
{
Factory result = factories.put(factory.getEncoding(), factory);
- String value = String.join(",", factories.keySet());
- acceptEncodingField = new HttpField(HttpHeader.ACCEPT_ENCODING, value);
+ updateBean(result, factory, true);
+
+ StringBuilder header = new StringBuilder();
+ factories.forEach((encoding, value) ->
+ {
+ if (!header.isEmpty())
+ header.append(", ");
+ header.append(encoding);
+ float weight = value.getWeight();
+ if (weight != -1F)
+ header.append(";q=").append(new DecimalFormat("#.###").format(weight));
+ });
+ acceptEncodingField = new HttpField(HttpHeader.ACCEPT_ENCODING, header.toString());
+
return result;
}
}
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/GZIPContentDecoder.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/GZIPContentDecoder.java
deleted file mode 100644
index 95ad573cfec8..000000000000
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/GZIPContentDecoder.java
+++ /dev/null
@@ -1,135 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under the
-// terms of the Eclipse Public License v. 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
-// which is available at https://www.apache.org/licenses/LICENSE-2.0.
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package org.eclipse.jetty.client;
-
-import java.util.ListIterator;
-
-import org.eclipse.jetty.client.transport.HttpResponse;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.io.ByteBufferPool;
-import org.eclipse.jetty.io.RetainableByteBuffer;
-import org.eclipse.jetty.util.IO;
-
-/**
- * {@link ContentDecoder} for the "gzip" encoding.
- */
-public class GZIPContentDecoder extends org.eclipse.jetty.http.GZIPContentDecoder implements ContentDecoder
-{
- public static final int DEFAULT_BUFFER_SIZE = IO.DEFAULT_BUFFER_SIZE;
-
- private long decodedLength;
-
- public GZIPContentDecoder()
- {
- this(DEFAULT_BUFFER_SIZE);
- }
-
- public GZIPContentDecoder(int bufferSize)
- {
- this(null, bufferSize);
- }
-
- public GZIPContentDecoder(ByteBufferPool byteBufferPool, int bufferSize)
- {
- super(byteBufferPool, bufferSize);
- }
-
- @Override
- public void beforeDecoding(Response response)
- {
- HttpResponse httpResponse = (HttpResponse)response;
- httpResponse.headers(headers ->
- {
- boolean seenContentEncoding = false;
- for (ListIterator iterator = headers.listIterator(headers.size()); iterator.hasPrevious();)
- {
- HttpField field = iterator.previous();
- HttpHeader header = field.getHeader();
- if (header == HttpHeader.CONTENT_LENGTH)
- {
- // Content-Length is not valid anymore while we are decoding.
- iterator.remove();
- }
- else if (header == HttpHeader.CONTENT_ENCODING && !seenContentEncoding)
- {
- // Last Content-Encoding should be removed/modified as the content will be decoded.
- seenContentEncoding = true;
- String value = field.getValue();
- int comma = value.lastIndexOf(",");
- if (comma < 0)
- iterator.remove();
- else
- iterator.set(new HttpField(HttpHeader.CONTENT_ENCODING, value.substring(0, comma)));
- }
- }
- });
- }
-
- @Override
- protected boolean decodedChunk(RetainableByteBuffer chunk)
- {
- decodedLength += chunk.remaining();
- super.decodedChunk(chunk);
- return true;
- }
-
- @Override
- public void afterDecoding(Response response)
- {
- HttpResponse httpResponse = (HttpResponse)response;
- httpResponse.headers(headers ->
- {
- headers.remove(HttpHeader.TRANSFER_ENCODING);
- headers.put(HttpHeader.CONTENT_LENGTH, decodedLength);
- });
- }
-
- /**
- * Specialized {@link ContentDecoder.Factory} for the "gzip" encoding.
- */
- public static class Factory extends ContentDecoder.Factory
- {
- private final ByteBufferPool byteBufferPool;
- private final int bufferSize;
-
- public Factory()
- {
- this(DEFAULT_BUFFER_SIZE);
- }
-
- public Factory(int bufferSize)
- {
- this(null, bufferSize);
- }
-
- public Factory(ByteBufferPool byteBufferPool)
- {
- this(byteBufferPool, DEFAULT_BUFFER_SIZE);
- }
-
- public Factory(ByteBufferPool byteBufferPool, int bufferSize)
- {
- super("gzip");
- this.byteBufferPool = byteBufferPool;
- this.bufferSize = bufferSize;
- }
-
- @Override
- public ContentDecoder newContentDecoder()
- {
- return new GZIPContentDecoder(byteBufferPool, bufferSize);
- }
- }
-}
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
index 1d5426e7f588..3415c21ad528 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
@@ -37,6 +37,8 @@
import org.eclipse.jetty.client.transport.HttpConversation;
import org.eclipse.jetty.client.transport.HttpDestination;
import org.eclipse.jetty.client.transport.HttpRequest;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookieStore;
@@ -50,6 +52,7 @@
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.Transport;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
import org.eclipse.jetty.util.Fields;
@@ -226,7 +229,9 @@ protected void doStart() throws Exception
handlers.put(new ProxyAuthenticationProtocolHandler(this));
handlers.put(new UpgradeProtocolHandler());
- decoderFactories.put(new GZIPContentDecoder.Factory(byteBufferPool));
+// TypeUtil.serviceStream(ServiceLoader.load(Compression.class))
+// .forEach(c -> decoderFactories.put(c));
+ decoderFactories.put(new CompressionContentDecoderFactory(new GzipCompression()));
if (cookieStore == null)
cookieStore = new HttpCookieStore.Default();
@@ -1181,4 +1186,22 @@ public interface Aware
{
void setHttpClient(HttpClient httpClient);
}
+
+ private static class CompressionContentDecoderFactory extends ContentDecoder.Factory
+ {
+ private final Compression compression;
+
+ protected CompressionContentDecoderFactory(Compression compression)
+ {
+ super(compression.getEncodingName());
+ this.compression = Objects.requireNonNull(compression);
+ installBean(compression);
+ }
+
+ @Override
+ public Content.Source newDecoderContentSource(Content.Source contentSource)
+ {
+ return compression.newDecoderSource(contentSource);
+ }
+ }
}
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
index 6dd4666b52cc..e0184631c611 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
@@ -15,6 +15,7 @@
import java.net.URI;
import java.util.List;
+import java.util.ListIterator;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
@@ -30,12 +31,10 @@
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.io.Content;
-import org.eclipse.jetty.io.RetainableByteBuffer;
-import org.eclipse.jetty.io.content.ContentSourceTransformer;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.Promise;
-import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.thread.AutoLock;
+import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.SerializedInvoker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,7 +70,8 @@ public abstract class HttpReceiver
private final HttpChannel channel;
private final SerializedInvoker invoker;
private ResponseState responseState = ResponseState.IDLE;
- private NotifiableContentSource contentSource;
+ private ContentSource rawContentSource;
+ private Content.Source contentSource;
private Throwable failure;
protected HttpReceiver(HttpChannel channel)
@@ -259,7 +259,7 @@ protected void responseHeaders(HttpExchange exchange)
// HEAD responses may have Content-Encoding
// and Content-Length, but have no content.
- ContentDecoder decoder = null;
+ ContentDecoder.Factory decoderFactory = null;
if (!HttpMethod.HEAD.is(exchange.getRequest().getMethod()))
{
// Content-Encoding may have multiple values in the order they
@@ -279,8 +279,8 @@ protected void responseHeaders(HttpExchange exchange)
{
if (factory.getEncoding().equalsIgnoreCase(contentEncoding))
{
- decoder = factory.newContentDecoder();
- decoder.beforeDecoding(response);
+ decoderFactory = factory;
+ beforeDecoding(response, contentEncoding);
break;
}
}
@@ -301,12 +301,17 @@ protected void responseHeaders(HttpExchange exchange)
}
responseState = ResponseState.CONTENT;
- if (contentSource != null)
+ if (rawContentSource != null)
throw new IllegalStateException();
- contentSource = new ContentSource();
+ rawContentSource = new ContentSource();
+ contentSource = rawContentSource;
- if (decoder != null)
- contentSource = new DecodingContentSource(contentSource, invoker, decoder, response);
+ if (decoderFactory != null)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Decoding {} response content", decoderFactory.getEncoding());
+ contentSource = new DecodedContentSource(decoderFactory.newDecoderContentSource(rawContentSource), response);
+ }
if (LOG.isDebugEnabled())
LOG.debug("Response content {} {}", response, contentSource);
@@ -335,7 +340,7 @@ protected void responseContentAvailable(HttpExchange exchange)
if (exchange.isResponseCompleteOrTerminated())
return;
- contentSource.onDataAvailable();
+ rawContentSource.onDataAvailable();
});
}
@@ -472,8 +477,7 @@ protected void dispose()
private void cleanup()
{
- if (contentSource != null)
- contentSource.destroy();
+ rawContentSource = null;
contentSource = null;
}
@@ -499,7 +503,7 @@ public void abort(HttpExchange exchange, Throwable failure, Promise pro
responseState = ResponseState.FAILURE;
this.failure = failure;
if (contentSource != null)
- contentSource.error(failure);
+ contentSource.fail(failure);
dispose();
HttpResponse response = exchange.getResponse();
@@ -514,6 +518,48 @@ public void abort(HttpExchange exchange, Throwable failure, Promise pro
});
}
+ private void beforeDecoding(Response response, String contentEncoding)
+ {
+ HttpResponse httpResponse = (HttpResponse)response;
+ httpResponse.headers(headers ->
+ {
+ boolean seenContentEncoding = false;
+ for (ListIterator iterator = headers.listIterator(headers.size()); iterator.hasPrevious();)
+ {
+ HttpField field = iterator.previous();
+ HttpHeader header = field.getHeader();
+ if (header == HttpHeader.CONTENT_LENGTH)
+ {
+ // Content-Length is not valid anymore while we are decoding.
+ iterator.remove();
+ }
+ else if (header == HttpHeader.CONTENT_ENCODING && !seenContentEncoding)
+ {
+ // Last Content-Encoding should be removed/modified as the content will be decoded.
+ seenContentEncoding = true;
+ // TODO: introduce HttpFields.removeLast() or similar.
+ // Use the contentEncoding parameter.
+ String value = field.getValue();
+ int comma = value.lastIndexOf(",");
+ if (comma < 0)
+ iterator.remove();
+ else
+ iterator.set(new HttpField(HttpHeader.CONTENT_ENCODING, value.substring(0, comma)));
+ }
+ }
+ });
+ }
+
+ private void afterDecoding(Response response, long decodedLength)
+ {
+ HttpResponse httpResponse = (HttpResponse)response;
+ httpResponse.headers(headers ->
+ {
+ headers.remove(HttpHeader.TRANSFER_ENCODING);
+ headers.put(HttpHeader.CONTENT_LENGTH, decodedLength);
+ });
+ }
+
@Override
public String toString()
{
@@ -556,129 +602,77 @@ private enum ResponseState
FAILURE
}
- private interface NotifiableContentSource extends Content.Source, Destroyable
- {
- boolean error(Throwable failure);
-
- void onDataAvailable();
-
- @Override
- default void destroy()
- {
- }
- }
-
- private static class DecodingContentSource extends ContentSourceTransformer implements NotifiableContentSource
+ private class DecodedContentSource implements Content.Source
{
- private static final Logger LOG = LoggerFactory.getLogger(DecodingContentSource.class);
+ private static final Logger LOG = LoggerFactory.getLogger(DecodedContentSource.class);
- private final ContentDecoder _decoder;
- private final Response _response;
- private volatile Content.Chunk _chunk;
+ private final Content.Source source;
+ private final Response response;
+ private long decodedLength;
- private DecodingContentSource(NotifiableContentSource rawSource, SerializedInvoker invoker, ContentDecoder decoder, Response response)
- {
- super(rawSource, invoker);
- _decoder = decoder;
- _response = response;
- }
-
- @Override
- protected NotifiableContentSource getContentSource()
+ private DecodedContentSource(Content.Source source, Response response)
{
- return (NotifiableContentSource)super.getContentSource();
+ this.source = source;
+ this.response = response;
}
@Override
- public void onDataAvailable()
+ public long getLength()
{
- getContentSource().onDataAvailable();
+ return source.getLength();
}
@Override
- protected Content.Chunk transform(Content.Chunk inputChunk)
+ public Content.Chunk read()
{
while (true)
{
- boolean retain = _chunk == null;
+ Content.Chunk chunk = source.read();
+
if (LOG.isDebugEnabled())
- LOG.debug("input: {}, chunk: {}, retain? {}", inputChunk, _chunk, retain);
- if (_chunk == null)
- _chunk = inputChunk;
- if (_chunk == null)
+ LOG.debug("Decoded chunk {}", chunk);
+
+ if (chunk == null)
return null;
- if (Content.Chunk.isFailure(_chunk))
+
+ if (chunk.isEmpty() && !chunk.isLast())
{
- Content.Chunk failure = _chunk;
- _chunk = Content.Chunk.next(failure);
- return failure;
+ chunk.release();
+ continue;
}
- // Retain the input chunk because its ByteBuffer will be referenced by the Inflater.
- if (retain)
- _chunk.retain();
- if (LOG.isDebugEnabled())
- LOG.debug("decoding: {}", _chunk);
- RetainableByteBuffer decodedBuffer = _decoder.decode(_chunk.getByteBuffer());
- if (LOG.isDebugEnabled())
- LOG.debug("decoded: {}", decodedBuffer);
+ decodedLength += chunk.remaining();
- if (decodedBuffer != null && decodedBuffer.hasRemaining())
- {
- // The decoded ByteBuffer is a transformed "copy" of the
- // compressed one, so it has its own reference counter.
- if (decodedBuffer.canRetain())
- {
- if (LOG.isDebugEnabled())
- LOG.debug("returning decoded content");
- return Content.Chunk.asChunk(decodedBuffer.getByteBuffer(), false, decodedBuffer);
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("returning non-retainable decoded content");
- return Content.Chunk.from(decodedBuffer.getByteBuffer(), false);
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("decoding produced no content");
- if (decodedBuffer != null)
- decodedBuffer.release();
+ if (chunk.isLast())
+ afterDecoding(response, decodedLength);
- if (!_chunk.hasRemaining())
- {
- Content.Chunk result = _chunk.isLast() ? Content.Chunk.EOF : null;
- if (LOG.isDebugEnabled())
- LOG.debug("Could not decode more from this chunk, releasing it, r={}", result);
- _chunk.release();
- _chunk = null;
- return result;
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("retrying transformation");
- }
- }
+ return chunk;
}
}
@Override
- public boolean error(Throwable failure)
+ public void demand(Runnable demandCallback)
+ {
+ Runnable demand = new Invocable.ReadyTask(Invocable.getInvocationType(demandCallback), () -> invoker.run(demandCallback));
+ source.demand(demand);
+ }
+
+ @Override
+ public void fail(Throwable failure)
+ {
+ source.fail(failure);
+ }
+
+ @Override
+ public void fail(Throwable failure, boolean last)
{
- if (_chunk != null)
- _chunk.release();
- _chunk = null;
- return getContentSource().error(failure);
+ source.fail(failure, last);
}
@Override
- public void destroy()
+ public boolean rewind()
{
- _decoder.afterDecoding(_response);
- getContentSource().destroy();
+ return source.rewind();
}
}
@@ -686,7 +680,7 @@ public void destroy()
* This Content.Source implementation guarantees that all {@link #read(boolean)} calls
* happening from a {@link #demand(Runnable)} callback must be serialized.
*/
- private class ContentSource implements NotifiableContentSource
+ private class ContentSource implements Content.Source
{
private static final Logger LOG = LoggerFactory.getLogger(ContentSource.class);
@@ -729,8 +723,7 @@ public Content.Chunk read()
}
}
- @Override
- public void onDataAvailable()
+ private void onDataAvailable()
{
if (LOG.isDebugEnabled())
LOG.debug("onDataAvailable on {}", this);
@@ -826,8 +819,7 @@ public void fail(Throwable failure)
invokeDemandCallback(true);
}
- @Override
- public boolean error(Throwable failure)
+ private boolean error(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("Erroring {}", this);
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpReceiverOverHTTP.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpReceiverOverHTTP.java
index da1f86f047e8..29ea530759e8 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpReceiverOverHTTP.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/internal/HttpReceiverOverHTTP.java
@@ -524,7 +524,7 @@ private void receiveNext()
throw new IllegalStateException();
if (LOG.isDebugEnabled())
- LOG.debug("Receiving next request in {}", this);
+ LOG.debug("Receiving next response in {}", this);
boolean setFillInterest = parseAndFill(true);
if (!hasContent() && setFillInterest)
fillInterested();
diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
index fbc58bc298e8..361696865e82 100644
--- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
+++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
@@ -18,11 +18,11 @@
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ArrayByteBufferPool;
-import org.eclipse.jetty.io.RetainableByteBuffer;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.io.content.ContentSourceTransformer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
-import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.junit.jupiter.params.ParameterizedTest;
@@ -53,19 +53,24 @@ public boolean handle(Request request, Response response, Callback callback)
client.getContentDecoderFactories().put(new ContentDecoder.Factory("UPPERCASE")
{
@Override
- public ContentDecoder newContentDecoder()
+ public Content.Source newDecoderContentSource(Content.Source source)
{
- return byteBuffer ->
+ return new ContentSourceTransformer(source)
{
- byte b = byteBuffer.get();
- if (b == '*')
- return bufferPool.acquire(0, true);
+ @Override
+ protected Content.Chunk transform(Content.Chunk chunk)
+ {
+ if (chunk.isEmpty())
+ return chunk.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
- RetainableByteBuffer buffer = bufferPool.acquire(1, true);
- int pos = BufferUtil.flipToFill(buffer.getByteBuffer());
- buffer.getByteBuffer().put(StringUtil.asciiToLowerCase(b));
- BufferUtil.flipToFlush(buffer.getByteBuffer(), pos);
- return buffer;
+ ByteBuffer byteBuffer = chunk.getByteBuffer();
+ byte b = byteBuffer.get();
+ if (b == '*')
+ return Content.Chunk.EMPTY;
+
+ byte lower = StringUtil.asciiToLowerCase(b);
+ return Content.Chunk.from(ByteBuffer.wrap(new byte[]{lower}), false);
+ }
};
}
});
@@ -97,13 +102,22 @@ public boolean handle(Request request, Response response, Callback callback)
client.getContentDecoderFactories().put(new ContentDecoder.Factory("UPPERCASE")
{
@Override
- public ContentDecoder newContentDecoder()
+ public Content.Source newDecoderContentSource(Content.Source source)
{
- return byteBuffer ->
+ return new ContentSourceTransformer(source)
{
- String uppercase = US_ASCII.decode(byteBuffer).toString();
- String lowercase = StringUtil.asciiToLowerCase(uppercase);
- return RetainableByteBuffer.wrap(ByteBuffer.wrap(lowercase.getBytes(US_ASCII)));
+ @Override
+ protected Content.Chunk transform(Content.Chunk chunk)
+ {
+ if (chunk.isEmpty())
+ return chunk.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
+
+ ByteBuffer byteBuffer = chunk.getByteBuffer();
+ String upperCase = US_ASCII.decode(byteBuffer).toString();
+ String lowerCase = StringUtil.asciiToLowerCase(upperCase);
+
+ return Content.Chunk.from(US_ASCII.encode(lowerCase), false);
+ }
};
}
});
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliDecoderSource.java b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliDecoderSource.java
index 27099bfbb40f..12f44887f1fc 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliDecoderSource.java
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliDecoderSource.java
@@ -40,13 +40,13 @@ public BrotliDecoderSource(Content.Source source, BrotliDecoderConfig config)
}
@Override
- protected Content.Chunk nextChunk(Content.Chunk readChunk) throws IOException
+ protected Content.Chunk transform(Content.Chunk inputChunk)
{
- ByteBuffer compressed = readChunk.getByteBuffer();
- if (readChunk.isLast() && !readChunk.hasRemaining())
+ ByteBuffer compressed = inputChunk.getByteBuffer();
+ if (inputChunk.isLast() && !inputChunk.hasRemaining())
return Content.Chunk.EOF;
- boolean last = readChunk.isLast();
+ boolean last = inputChunk.isLast();
while (true)
{
@@ -76,7 +76,10 @@ protected Content.Chunk nextChunk(Content.Chunk readChunk) throws IOException
// rely on status.OK to go to EOF
return Content.Chunk.from(output, false);
}
- default -> throw new IOException("Decoder failure: Corrupted input buffer");
+ default ->
+ {
+ return Content.Chunk.from(new IOException("Decoder failure: Corrupted input buffer"));
+ }
}
}
}
diff --git a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/DecoderSource.java b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/DecoderSource.java
index 642f55b08c8d..a36021fcb32e 100644
--- a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/DecoderSource.java
+++ b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/DecoderSource.java
@@ -13,121 +13,13 @@
package org.eclipse.jetty.compression;
-import java.io.IOException;
-
import org.eclipse.jetty.io.Content;
-import org.eclipse.jetty.util.ExceptionUtil;
+import org.eclipse.jetty.io.content.ContentSourceTransformer;
-public abstract class DecoderSource implements Content.Source
+public abstract class DecoderSource extends ContentSourceTransformer
{
- private final Content.Source source;
- private Content.Chunk activeChunk;
- private Throwable failed;
- private boolean terminated = false;
-
- protected DecoderSource(Content.Source source)
- {
- this.source = source;
- }
-
- @Override
- public void demand(Runnable demandCallback)
- {
- if (activeChunk != null && activeChunk.hasRemaining())
- demandCallback.run();
- else
- source.demand(demandCallback);
- }
-
- @Override
- public void fail(Throwable failure)
- {
- failed = ExceptionUtil.combine(failed, failure);
- source.fail(failure);
- }
-
- @Override
- public Content.Chunk read()
- {
- if (failed != null)
- return Content.Chunk.from(failed, true);
-
- if (terminated)
- return Content.Chunk.EOF;
-
- Content.Chunk readChunk = readChunk();
- if (readChunk == null)
- return null;
-
- if (Content.Chunk.isFailure(readChunk))
- {
- failed = ExceptionUtil.combine(failed, readChunk.getFailure());
- return readChunk;
- }
-
- try
- {
- Content.Chunk chunk = nextChunk(readChunk);
- if (chunk != null && chunk.isLast())
- {
- terminate();
- }
- return chunk;
- }
- catch (Throwable x)
- {
- fail(x);
- return Content.Chunk.from(failed, true);
- }
- }
-
- /**
- * Process the readChunk and produce a response Chunk.
- *
- * @param readChunk the active Read Chunk (never null, never a failure)
- * @throws IOException if decoder failure occurs.
- */
- protected abstract Content.Chunk nextChunk(Content.Chunk readChunk) throws IOException;
-
- /**
- * Place to cleanup and release any resources
- * being held by this DecoderSource.
- */
- protected void release()
- {
- }
-
- private void freeActiveChunk()
- {
- if (activeChunk != null)
- activeChunk.release();
- activeChunk = null;
- }
-
- private Content.Chunk readChunk()
- {
- if (activeChunk != null)
- {
- if (activeChunk.hasRemaining())
- return activeChunk;
- else
- {
- activeChunk.release();
- activeChunk = null;
- }
- }
-
- activeChunk = source.read();
- return activeChunk;
- }
-
- private void terminate()
+ public DecoderSource(Content.Source rawSource)
{
- if (!terminated)
- {
- terminated = true;
- freeActiveChunk();
- release();
- }
+ super(rawSource);
}
}
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
index 8e62827a3c0f..c671fa9f0c87 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
@@ -163,7 +163,7 @@ public InputStream newDecoderInputStream(InputStream in, DecoderConfig config) t
public DecoderSource newDecoderSource(Content.Source source, DecoderConfig config)
{
GzipDecoderConfig gzipDecoderConfig = (GzipDecoderConfig)config;
- return new GzipDecoderSource(this, source, gzipDecoderConfig);
+ return new GzipDecoderSource(source, this, gzipDecoderConfig);
}
@Override
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipDecoderConfig.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipDecoderConfig.java
index 70c8a4826cc2..a48a92045ce9 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipDecoderConfig.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipDecoderConfig.java
@@ -14,16 +14,12 @@
package org.eclipse.jetty.compression.gzip;
import org.eclipse.jetty.compression.DecoderConfig;
+import org.eclipse.jetty.util.IO;
public class GzipDecoderConfig implements DecoderConfig
{
- /**
- * Default Buffer Size as found in {@link java.util.zip.GZIPInputStream}.
- */
- private static final int DEFAULT_BUFFER_SIZE = 512;
- /**
- * Minimum buffer size to avoid issues with JDK-8133170
- */
+ private static final int DEFAULT_BUFFER_SIZE = IO.DEFAULT_BUFFER_SIZE;
+ // Minimum buffer size to avoid issues with JDK-8133170.
private static final int MIN_BUFFER_SIZE = 32;
private int bufferSize = DEFAULT_BUFFER_SIZE;
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
index 516616baa0cf..e6e6c75fc177 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
@@ -47,7 +47,7 @@ private enum State
private long value;
private byte flags;
- public GzipDecoderSource(GzipCompression compression, Content.Source source, GzipDecoderConfig config)
+ public GzipDecoderSource(Content.Source source, GzipCompression compression, GzipDecoderConfig config)
{
super(source);
this.compression = compression;
@@ -59,24 +59,21 @@ public GzipDecoderSource(GzipCompression compression, Content.Source source, Gzi
}
@Override
- protected Content.Chunk nextChunk(Content.Chunk readChunk)
+ protected Content.Chunk transform(Content.Chunk inputChunk)
{
- ByteBuffer compressed = readChunk.getByteBuffer();
- // parse
+ ByteBuffer compressed = inputChunk.getByteBuffer();
try
{
- while (compressed.hasRemaining())
+ while (true)
{
switch (state)
{
- case INITIAL:
+ case INITIAL ->
{
inflater.reset();
state = State.ID;
- break;
}
-
- case FLAGS:
+ case FLAGS ->
{
if ((flags & 0x04) == 0x04)
{
@@ -85,9 +82,13 @@ protected Content.Chunk nextChunk(Content.Chunk readChunk)
value = 0;
}
else if ((flags & 0x08) == 0x08)
+ {
state = State.NAME;
+ }
else if ((flags & 0x10) == 0x10)
+ {
state = State.COMMENT;
+ }
else if ((flags & 0x2) == 0x2)
{
state = State.HCRC;
@@ -99,54 +100,51 @@ else if ((flags & 0x2) == 0x2)
state = State.DATA;
continue;
}
- break;
}
-
- case DATA:
+ case DATA ->
{
- try
+ while (true)
{
RetainableByteBuffer buffer = compression.acquireByteBuffer(bufferSize);
- ByteBuffer decoded = buffer.getByteBuffer();
- int pos = BufferUtil.flipToFill(decoded);
- inflater.inflate(decoded);
- BufferUtil.flipToFlush(decoded, pos);
- if (buffer.hasRemaining())
+ try
{
- return Content.Chunk.asChunk(decoded, false, buffer);
+ ByteBuffer decoded = buffer.getByteBuffer();
+ int pos = BufferUtil.flipToFill(decoded);
+ inflater.inflate(decoded);
+ BufferUtil.flipToFlush(decoded, pos);
+ if (buffer.hasRemaining())
+ return Content.Chunk.asChunk(decoded, false, buffer);
+ buffer.release();
}
- else
+ catch (DataFormatException x)
{
buffer.release();
+ ZipException failure = new ZipException();
+ failure.initCause(x);
+ throw failure;
}
- }
- catch (DataFormatException x)
- {
- throw new ZipException(x.getMessage());
- }
- if (inflater.needsInput())
- {
- if (!compressed.hasRemaining())
+ if (inflater.needsInput())
{
- return Content.Chunk.EMPTY;
+ if (!compressed.hasRemaining())
+ return Content.Chunk.EMPTY;
+ inflater.setInput(compressed);
+ // Loop around and try again to inflate.
+ }
+ else if (inflater.finished())
+ {
+ state = State.CRC;
+ size = 0;
+ value = 0;
+ break;
}
- inflater.setInput(compressed);
- }
- else if (inflater.finished())
- {
- state = State.CRC;
- size = 0;
- value = 0;
- break;
}
}
- continue;
-
- default:
- break;
}
+ if (inputChunk.isEmpty())
+ return inputChunk.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
+
byte currByte = compressed.get();
switch (state)
{
@@ -163,14 +161,7 @@ else if (inflater.finished())
if (size == 2)
{
if (value != 0x8B1F)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Skipping rest of input, no gzip magic number detected");
- state = State.INITIAL;
- compressed.position(compressed.limit());
- // TODO: need to consumeAll super source?
- return Content.Chunk.EOF;
- }
+ throw new ZipException("Invalid gzip bytes");
state = State.CM;
}
}
@@ -281,7 +272,6 @@ else if (inflater.finished())
state = State.ERROR;
return Content.Chunk.from(x, true);
}
- return readChunk.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
}
@Override
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
index 89471af9c443..852833559d07 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
@@ -164,7 +164,7 @@ public InputStream newDecoderInputStream(InputStream in, DecoderConfig config) t
public DecoderSource newDecoderSource(Content.Source source, DecoderConfig config)
{
ZstandardDecoderConfig zstandardDecoderConfig = (ZstandardDecoderConfig)config;
- return new ZstandardDecoderSource(this, source, zstandardDecoderConfig);
+ return new ZstandardDecoderSource(source, this, zstandardDecoderConfig);
}
@Override
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardDecoderSource.java b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardDecoderSource.java
index 170d031994b8..045338ab97dc 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardDecoderSource.java
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardDecoderSource.java
@@ -27,30 +27,28 @@ public class ZstandardDecoderSource extends DecoderSource
private final ZstandardCompression compression;
private final ZstdDecompressCtx decompressCtx;
- public ZstandardDecoderSource(ZstandardCompression compression, Content.Source src, ZstandardDecoderConfig config)
+ public ZstandardDecoderSource(Content.Source source, ZstandardCompression compression, ZstandardDecoderConfig config)
{
- super(src);
+ super(source);
this.compression = compression;
this.decompressCtx = new ZstdDecompressCtx();
this.decompressCtx.setMagicless(config.isMagicless());
}
@Override
- protected Content.Chunk nextChunk(Content.Chunk readChunk)
+ protected Content.Chunk transform(Content.Chunk inputChunk)
{
- ByteBuffer input = readChunk.getByteBuffer();
- if (!readChunk.hasRemaining())
- return readChunk;
+ ByteBuffer input = inputChunk.getByteBuffer();
+ if (!inputChunk.hasRemaining())
+ return inputChunk;
if (!input.isDirect())
throw new IllegalArgumentException("Read Chunk is not a Direct ByteBuffer");
RetainableByteBuffer dst = compression.acquireByteBuffer();
- boolean last = readChunk.isLast();
+ boolean last = inputChunk.isLast();
dst.getByteBuffer().clear();
boolean fullyFlushed = decompressCtx.decompressDirectByteBufferStream(dst.getByteBuffer(), input);
if (!fullyFlushed)
- {
last = false;
- }
dst.getByteBuffer().flip();
return Content.Chunk.asChunk(dst.getByteBuffer(), last, dst);
}
diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
index bb2e513c4586..de1fb6ff4f00 100644
--- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
+++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
@@ -13,12 +13,10 @@
package org.eclipse.jetty.io.content;
-import java.util.Objects;
-
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.ExceptionUtil;
-import org.eclipse.jetty.util.thread.Invocable;
-import org.eclipse.jetty.util.thread.SerializedInvoker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* This abstract {@link Content.Source} wraps another {@link Content.Source} and implementers need only
@@ -26,27 +24,23 @@
* read from the wrapped source.
* The {@link #demand(Runnable)} conversation is passed directly to the wrapped {@link Content.Source},
* which means that transformations that may fully consume bytes read can result in a null return from
- * {@link Content.Source#read()} even after a callback to the demand {@link Runnable} (as per spurious
+ * {@link Content.Source#read()} even after a callback to the demand {@link Runnable}, as per spurious
* invocation in {@link Content.Source#demand(Runnable)}.
*/
public abstract class ContentSourceTransformer implements Content.Source
{
- private final SerializedInvoker invoker;
+ private static final Logger LOG = LoggerFactory.getLogger(ContentSourceTransformer.class);
+
private final Content.Source rawSource;
private Content.Chunk rawChunk;
private Content.Chunk transformedChunk;
private volatile boolean needsRawRead;
- private volatile Runnable demandCallback;
+ private boolean finished;
protected ContentSourceTransformer(Content.Source rawSource)
- {
- this(rawSource, new SerializedInvoker(ContentSourceTransformer.class));
- }
-
- protected ContentSourceTransformer(Content.Source rawSource, SerializedInvoker invoker)
{
this.rawSource = rawSource;
- this.invoker = invoker;
+ this.needsRawRead = true;
}
protected Content.Source getContentSource()
@@ -57,11 +51,16 @@ protected Content.Source getContentSource()
@Override
public Content.Chunk read()
{
+ if (LOG.isDebugEnabled())
+ LOG.debug("Reading {}", this);
+
while (true)
{
if (needsRawRead)
{
rawChunk = rawSource.read();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Raw chunk {} {}", rawChunk, this);
needsRawRead = rawChunk == null;
if (rawChunk == null)
return null;
@@ -72,54 +71,96 @@ public Content.Chunk read()
Content.Chunk failure = rawChunk;
rawChunk = Content.Chunk.next(rawChunk);
needsRawRead = rawChunk == null;
+ if (rawChunk != null)
+ {
+ finished = true;
+ release();
+ }
return failure;
}
if (Content.Chunk.isFailure(transformedChunk))
return transformedChunk;
- transformedChunk = process(rawChunk);
+ if (finished)
+ return Content.Chunk.EOF;
+
+ boolean rawLast = rawChunk != null && rawChunk.isLast();
- if (rawChunk != null && rawChunk != transformedChunk)
+ transformedChunk = process(rawChunk != null ? rawChunk : Content.Chunk.EMPTY);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Transformed chunk {} {}", transformedChunk, this);
+
+ if (rawChunk == null && (transformedChunk == null || transformedChunk == Content.Chunk.EMPTY))
+ {
+ needsRawRead = true;
+ continue;
+ }
+
+ // Prevent double release.
+ if (transformedChunk == rawChunk)
+ rawChunk = null;
+
+ if (rawChunk != null && rawChunk.isEmpty())
+ {
rawChunk.release();
- rawChunk = null;
+ rawChunk = Content.Chunk.next(rawChunk);
+ }
if (transformedChunk != null)
{
+ boolean transformedLast = transformedChunk.isLast();
+ boolean transformedFailure = Content.Chunk.isFailure(transformedChunk);
+
+ // Transformation may be complete, but rawSource is not read until EOF,
+ // return a non-last transformed chunk to force more read() and transform().
+ if (transformedLast && !rawLast)
+ {
+ if (transformedChunk == Content.Chunk.EOF)
+ transformedChunk = Content.Chunk.EMPTY;
+ else if (!transformedFailure)
+ transformedChunk = Content.Chunk.asChunk(transformedChunk.getByteBuffer(), false, transformedChunk);
+ }
+
+ boolean terminated = rawLast && transformedLast;
+ boolean terminalFailure = transformedFailure && transformedLast;
+
Content.Chunk result = transformedChunk;
transformedChunk = Content.Chunk.next(result);
+
+ if (terminated || terminalFailure)
+ {
+ finished = true;
+ release();
+ }
+
return result;
}
- needsRawRead = true;
+ needsRawRead = rawChunk == null;
}
}
@Override
public void demand(Runnable demandCallback)
{
- this.demandCallback = Objects.requireNonNull(demandCallback);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Demanding {} {}", demandCallback, this);
+
if (needsRawRead)
- // Inner class used instead of lambda for clarity in stack traces.
- rawSource.demand(new DemandTask(Invocable.getInvocationType(demandCallback), this::invokeDemandCallback));
+ rawSource.demand(demandCallback);
else
- invoker.run(this::invokeDemandCallback);
+ ExceptionUtil.run(demandCallback, this::fail);
}
@Override
public void fail(Throwable failure)
{
+ if (LOG.isDebugEnabled())
+ LOG.debug("Failing {}", this, failure);
rawSource.fail(failure);
}
- private void invokeDemandCallback()
- {
- Runnable demandCallback = this.demandCallback;
- this.demandCallback = null;
- if (demandCallback != null)
- ExceptionUtil.run(demandCallback, this::fail);
- }
-
private Content.Chunk process(Content.Chunk rawChunk)
{
try
@@ -136,44 +177,50 @@ private Content.Chunk process(Content.Chunk rawChunk)
/**
* Transforms the input chunk parameter into an output chunk.
* When this method produces a non-{@code null}, non-last chunk,
- * it is subsequently invoked with a {@code null} input chunk to try to
+ * it is subsequently invoked with either the input chunk (if it has
+ * remaining bytes), or with {@link Content.Chunk#EMPTY} to try to
* produce more output chunks from the previous input chunk.
* For example, a single compressed input chunk may be transformed into
* multiple uncompressed output chunks.
- * The input chunk is released as soon as this method returns, so
- * implementations that must hold onto the input chunk must arrange to call
- * {@link Content.Chunk#retain()} and its correspondent {@link Content.Chunk#release()}.
- * Implementations should return an {@link Content.Chunk} with non-null
- * {@link Content.Chunk#getFailure()} in case
- * of transformation errors.
- * Exceptions thrown by this method are equivalent to returning an error chunk.
+ * The input chunk is released as soon as this method returns if it
+ * is fully consumed, so implementations that must hold onto the input
+ * chunk must arrange to call {@link Content.Chunk#retain()} and its
+ * correspondent {@link Content.Chunk#release()}.
+ * Implementations should return a {@link Content.Chunk} with non-null
+ * {@link Content.Chunk#getFailure()} in case of transformation errors.
+ * Exceptions thrown by this method are equivalent to returning an
+ * error chunk.
* Implementations of this method may return:
*
- * {@code null}, if more input chunks are necessary to produce an output chunk
- * the {@code inputChunk} itself, typically in case of non-null {@link Content.Chunk#getFailure()},
- * or when no transformation is required
+ * {@code null} or {@link Content.Chunk#EMPTY}, if more input chunks
+ * are necessary to produce an output chunk
+ * the {@code inputChunk} itself, typically in case of non-null
+ * {@link Content.Chunk#getFailure()}, or when no transformation is required
* a new {@link Content.Chunk} derived from {@code inputChunk}.
*
+ * The input chunk should be consumed (its position updated) as the
+ * transformation proceeds.
*
* @param inputChunk a chunk read from the wrapped {@link Content.Source}
* @return a transformed chunk or {@code null}
*/
protected abstract Content.Chunk transform(Content.Chunk inputChunk);
- private class DemandTask extends Invocable.Task.Abstract
+ /**
+ * Invoked when the transformation is complete to release any resource.
+ */
+ protected void release()
{
- private final Runnable invokeDemandCallback;
-
- private DemandTask(InvocationType invocationType, Runnable invokeDemandCallback)
- {
- super(invocationType);
- this.invokeDemandCallback = invokeDemandCallback;
- }
+ }
- @Override
- public void run()
- {
- invoker.run(invokeDemandCallback);
- }
+ @Override
+ public String toString()
+ {
+ return "%s@%x[finished=%b,source=%s]".formatted(
+ getClass().getSimpleName(),
+ hashCode(),
+ finished,
+ rawSource
+ );
}
}
diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java
index 2a71e9a494b1..c397c0e77728 100644
--- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java
+++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/ContentSourceTransformerTest.java
@@ -27,6 +27,7 @@
import org.eclipse.jetty.io.content.ContentSourceTransformer;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FutureCallback;
+import org.eclipse.jetty.util.thread.SerializedInvoker;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -367,14 +368,12 @@ public void testTransientFailuresFromTransformationAreReturned()
@Override
protected Content.Chunk transform(Content.Chunk rawChunk)
{
- if (rawChunk == null)
- return null;
- String decoded = UTF_8.decode(rawChunk.getByteBuffer().duplicate()).toString();
+ String decoded = UTF_8.decode(rawChunk.getByteBuffer()).toString();
return switch (decoded)
{
case "B" -> Content.Chunk.from(originalFailure1, false);
case "D" -> Content.Chunk.from(originalFailure2, false);
- default -> Content.Chunk.from(rawChunk.getByteBuffer(), rawChunk.isLast());
+ default -> Content.Chunk.from(UTF_8.encode(decoded), rawChunk.isLast());
};
}
};
@@ -399,6 +398,7 @@ protected Content.Chunk transform(Content.Chunk rawChunk)
private static class WordSplitLowCaseTransformer extends ContentSourceTransformer
{
+ private final SerializedInvoker invoker = new SerializedInvoker();
private final Queue chunks = new ArrayDeque<>();
private WordSplitLowCaseTransformer(Content.Source rawSource)
@@ -406,6 +406,12 @@ private WordSplitLowCaseTransformer(Content.Source rawSource)
super(rawSource);
}
+ @Override
+ public void demand(Runnable demandCallback)
+ {
+ super.demand(() -> invoker.run(demandCallback));
+ }
+
@Override
protected Content.Chunk transform(Content.Chunk rawChunk)
{
diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java
index 014652a027e4..c54154dd05f4 100644
--- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java
+++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java
@@ -35,12 +35,11 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.AsyncRequestContent;
-import org.eclipse.jetty.client.ContentDecoder;
-import org.eclipse.jetty.client.GZIPContentDecoder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result;
+import org.eclipse.jetty.http.GZIPContentDecoder;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
@@ -50,6 +49,7 @@
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.thread.AutoLock;
@@ -777,7 +777,7 @@ public static class GZIPContentTransformer implements ContentTransformer
private final List buffers = new ArrayList<>(2);
private final ContentTransformer transformer;
- private final ContentDecoder decoder;
+ private final GZIPContentDecoder decoder;
private final ByteArrayOutputStream out;
private final GZIPOutputStream gzipOut;
@@ -792,7 +792,7 @@ public GZIPContentTransformer(HttpClient httpClient, ContentTransformer transfor
{
this.transformer = transformer;
ByteBufferPool bufferPool = httpClient == null ? null : httpClient.getByteBufferPool();
- this.decoder = new GZIPContentDecoder(bufferPool, GZIPContentDecoder.DEFAULT_BUFFER_SIZE);
+ this.decoder = new GZIPDecoder(bufferPool);
this.out = new ByteArrayOutputStream();
this.gzipOut = new GZIPOutputStream(out);
}
@@ -854,6 +854,21 @@ private ByteBuffer gzip(List buffers, boolean finished) throws IOExc
out.reset();
return ByteBuffer.wrap(gzipBytes);
}
+
+ private static class GZIPDecoder extends GZIPContentDecoder
+ {
+ public GZIPDecoder(ByteBufferPool bufferPool)
+ {
+ super(bufferPool, IO.DEFAULT_BUFFER_SIZE);
+ }
+
+ @Override
+ protected boolean decodedChunk(RetainableByteBuffer chunk)
+ {
+ super.decodedChunk(chunk);
+ return true;
+ }
+ }
}
private class ProxyAsyncRequestContent extends AsyncRequestContent
diff --git a/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java b/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
index e74a3bc18119..9988107888ea 100644
--- a/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
+++ b/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
@@ -35,12 +35,11 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.AsyncRequestContent;
-import org.eclipse.jetty.client.ContentDecoder;
-import org.eclipse.jetty.client.GZIPContentDecoder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result;
+import org.eclipse.jetty.http.GZIPContentDecoder;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
@@ -50,6 +49,7 @@
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.thread.AutoLock;
@@ -777,7 +777,7 @@ public static class GZIPContentTransformer implements ContentTransformer
private final List buffers = new ArrayList<>(2);
private final ContentTransformer transformer;
- private final ContentDecoder decoder;
+ private final GZIPContentDecoder decoder;
private final ByteArrayOutputStream out;
private final GZIPOutputStream gzipOut;
@@ -792,7 +792,7 @@ public GZIPContentTransformer(HttpClient httpClient, ContentTransformer transfor
{
this.transformer = transformer;
ByteBufferPool bufferPool = httpClient == null ? null : httpClient.getByteBufferPool();
- this.decoder = new GZIPContentDecoder(bufferPool, GZIPContentDecoder.DEFAULT_BUFFER_SIZE);
+ this.decoder = new GZIPDecoder(bufferPool);
this.out = new ByteArrayOutputStream();
this.gzipOut = new GZIPOutputStream(out);
}
@@ -854,6 +854,21 @@ private ByteBuffer gzip(List buffers, boolean finished) throws IOExc
out.reset();
return ByteBuffer.wrap(gzipBytes);
}
+
+ private static class GZIPDecoder extends GZIPContentDecoder
+ {
+ public GZIPDecoder(ByteBufferPool bufferPool)
+ {
+ super(bufferPool, IO.DEFAULT_BUFFER_SIZE);
+ }
+
+ @Override
+ protected boolean decodedChunk(RetainableByteBuffer chunk)
+ {
+ super.decodedChunk(chunk);
+ return true;
+ }
+ }
}
private class ProxyAsyncRequestContent extends AsyncRequestContent
diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java
index 7ec105a2aa59..288bc115974d 100644
--- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java
+++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java
@@ -35,12 +35,11 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.AsyncRequestContent;
-import org.eclipse.jetty.client.ContentDecoder;
-import org.eclipse.jetty.client.GZIPContentDecoder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result;
+import org.eclipse.jetty.http.GZIPContentDecoder;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
@@ -50,6 +49,7 @@
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.thread.AutoLock;
@@ -777,7 +777,7 @@ public static class GZIPContentTransformer implements ContentTransformer
private final List buffers = new ArrayList<>(2);
private final ContentTransformer transformer;
- private final ContentDecoder decoder;
+ private final GZIPContentDecoder decoder;
private final ByteArrayOutputStream out;
private final GZIPOutputStream gzipOut;
@@ -792,7 +792,7 @@ public GZIPContentTransformer(HttpClient httpClient, ContentTransformer transfor
{
this.transformer = transformer;
ByteBufferPool bufferPool = httpClient == null ? null : httpClient.getByteBufferPool();
- this.decoder = new GZIPContentDecoder(bufferPool, GZIPContentDecoder.DEFAULT_BUFFER_SIZE);
+ this.decoder = new GZIPDecoder(bufferPool);
this.out = new ByteArrayOutputStream();
this.gzipOut = new GZIPOutputStream(out);
}
@@ -854,6 +854,21 @@ private ByteBuffer gzip(List buffers, boolean finished) throws IOExc
out.reset();
return ByteBuffer.wrap(gzipBytes);
}
+
+ private static class GZIPDecoder extends GZIPContentDecoder
+ {
+ public GZIPDecoder(ByteBufferPool bufferPool)
+ {
+ super(bufferPool, IO.DEFAULT_BUFFER_SIZE);
+ }
+
+ @Override
+ protected boolean decodedChunk(RetainableByteBuffer chunk)
+ {
+ super.decodedChunk(chunk);
+ return true;
+ }
+ }
}
private class ProxyAsyncRequestContent extends AsyncRequestContent
From 7a061035bf4765ed6aaa604fbd4986a30f70c45b Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Thu, 5 Dec 2024 15:00:52 +0100
Subject: [PATCH 02/20] Updated OSGi dependencies, now that jetty-client
depends on jetty-compression-common.
Signed-off-by: Simone Bordet
---
.../java/org/eclipse/jetty/ee11/osgi/test/TestOSGiUtil.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/jetty-ee11/jetty-ee11-osgi/test-jetty-ee11-osgi/src/test/java/org/eclipse/jetty/ee11/osgi/test/TestOSGiUtil.java b/jetty-ee11/jetty-ee11-osgi/test-jetty-ee11-osgi/src/test/java/org/eclipse/jetty/ee11/osgi/test/TestOSGiUtil.java
index f0aefe4aeeea..fbbb1763f139 100644
--- a/jetty-ee11/jetty-ee11-osgi/test-jetty-ee11-osgi/src/test/java/org/eclipse/jetty/ee11/osgi/test/TestOSGiUtil.java
+++ b/jetty-ee11/jetty-ee11-osgi/test-jetty-ee11-osgi/src/test/java/org/eclipse/jetty/ee11/osgi/test/TestOSGiUtil.java
@@ -214,6 +214,8 @@ public static void coreJettyDependencies(List res)
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-osgi").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-ee").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-common").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-gzip").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee11").artifactId("jetty-ee11-servlet").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee11").artifactId("jetty-ee11-webapp").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee11").artifactId("jetty-ee11-servlets").versionAsInProject().start());
From f6ce76343f2de6532e3a042b3441744fe6094f4d Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Thu, 5 Dec 2024 19:21:26 +0100
Subject: [PATCH 03/20] Now using ServiceLoader to load the Compression
implementations.
Updated tests that required client jars in a web application.
Signed-off-by: Simone Bordet
---
.../src/main/config/modules/client.mod | 2 ++
.../jetty-client/src/main/java/module-info.java | 6 ++++--
.../org/eclipse/jetty/client/ContentDecoder.java | 5 +++++
.../java/org/eclipse/jetty/client/HttpClient.java | 15 +++++++++------
.../tests/JakartaClientClassLoaderTest.java | 6 +++++-
...JakartaClientShutdownWithServerWebAppTest.java | 6 +++++-
.../tests/JettyClientClassLoaderTest.java | 6 +++++-
.../tests/JakartaClientClassLoaderTest.java | 6 +++++-
...JakartaClientShutdownWithServerWebAppTest.java | 6 +++++-
.../tests/JettyClientClassLoaderTest.java | 6 +++++-
.../tests/JakartaClientClassLoaderTest.java | 6 +++++-
...JakartaClientShutdownWithServerWebAppTest.java | 6 +++++-
.../tests/JettyClientClassLoaderTest.java | 6 +++++-
13 files changed, 65 insertions(+), 17 deletions(-)
diff --git a/jetty-core/jetty-client/src/main/config/modules/client.mod b/jetty-core/jetty-client/src/main/config/modules/client.mod
index c1dfd8bb6856..8a541ca6ae79 100644
--- a/jetty-core/jetty-client/src/main/config/modules/client.mod
+++ b/jetty-core/jetty-client/src/main/config/modules/client.mod
@@ -10,3 +10,5 @@ client
lib/jetty-alpn-client-${jetty.version}.jar
lib/jetty-alpn-java-client-${jetty.version}.jar
lib/jetty-client-${jetty.version}.jar
+lib/jetty-compression-common-${jetty.version}.jar
+lib/jetty-compression-gzip-${jetty.version}.jar
diff --git a/jetty-core/jetty-client/src/main/java/module-info.java b/jetty-core/jetty-client/src/main/java/module-info.java
index 69e26d46e12f..5813b7e8a2e6 100644
--- a/jetty-core/jetty-client/src/main/java/module-info.java
+++ b/jetty-core/jetty-client/src/main/java/module-info.java
@@ -16,8 +16,8 @@
requires org.eclipse.jetty.alpn.client;
requires org.slf4j;
- requires transitive org.eclipse.jetty.compression;
- requires transitive org.eclipse.jetty.compression.gzip;
+ requires org.eclipse.jetty.compression;
+ requires org.eclipse.jetty.compression.gzip;
requires transitive org.eclipse.jetty.http;
// Only required if using JMX.
@@ -32,4 +32,6 @@
exports org.eclipse.jetty.client.jmx to
org.eclipse.jetty.jmx;
+
+ uses org.eclipse.jetty.compression.Compression;
}
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
index 2e94348f70fb..2bcb4a6c8c6d 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentDecoder.java
@@ -117,6 +117,11 @@ public Iterator iterator()
return factories.values().iterator();
}
+ public boolean isEmpty()
+ {
+ return factories.isEmpty();
+ }
+
public void clear()
{
factories.clear();
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
index 3415c21ad528..4a795bc9cea1 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
@@ -25,6 +25,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
@@ -38,7 +39,6 @@
import org.eclipse.jetty.client.transport.HttpDestination;
import org.eclipse.jetty.client.transport.HttpRequest;
import org.eclipse.jetty.compression.Compression;
-import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookieStore;
@@ -60,6 +60,7 @@
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.SocketAddressResolver;
+import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
@@ -229,9 +230,11 @@ protected void doStart() throws Exception
handlers.put(new ProxyAuthenticationProtocolHandler(this));
handlers.put(new UpgradeProtocolHandler());
-// TypeUtil.serviceStream(ServiceLoader.load(Compression.class))
-// .forEach(c -> decoderFactories.put(c));
- decoderFactories.put(new CompressionContentDecoderFactory(new GzipCompression()));
+ if (decoderFactories.isEmpty())
+ {
+ TypeUtil.serviceStream(ServiceLoader.load(Compression.class))
+ .forEach(c -> decoderFactories.put(new CompressionContentDecoderFactory(c)));
+ }
if (cookieStore == null)
cookieStore = new HttpCookieStore.Default();
@@ -587,7 +590,7 @@ public void failed(Throwable x)
connect(socketAddresses, nextIndex, context);
}
});
- HttpClient.this.transport.connect((SocketAddress)socketAddresses.get(index), context);
+ HttpClient.this.transport.connect(socketAddresses.get(index), context);
}
});
}
@@ -1191,7 +1194,7 @@ private static class CompressionContentDecoderFactory extends ContentDecoder.Fac
{
private final Compression compression;
- protected CompressionContentDecoderFactory(Compression compression)
+ private CompressionContentDecoderFactory(Compression compression)
{
super(compression.getEncodingName());
this.compression = Objects.requireNonNull(compression);
diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientClassLoaderTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
index 14d13d6a853c..28e7721a68fe 100644
--- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
+++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
@@ -31,6 +31,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.webapp.Configurations;
import org.eclipse.jetty.ee10.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
@@ -57,7 +59,7 @@ public class JakartaClientClassLoaderTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -150,6 +152,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
index 499de09000e6..1d0e73216fc6 100644
--- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
+++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee10/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
@@ -25,6 +25,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee10.webapp.Configuration;
import org.eclipse.jetty.ee10.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
import org.eclipse.jetty.ee10.websocket.jakarta.client.webapp.JakartaWebSocketShutdownContainer;
@@ -51,7 +53,7 @@ public class JakartaClientShutdownWithServerWebAppTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -101,6 +103,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/JettyClientClassLoaderTest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/JettyClientClassLoaderTest.java
index 3e493d91b05b..a9e2443122a2 100644
--- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/JettyClientClassLoaderTest.java
+++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/JettyClientClassLoaderTest.java
@@ -26,6 +26,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee10.websocket.client.config.JettyWebSocketClientConfiguration;
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory;
@@ -61,7 +63,7 @@ public class JettyClientClassLoaderTest
private final HttpClient httpClient = new HttpClient();
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -170,6 +172,8 @@ public WebAppTester.WebApp createWebSocketWebapp(String contextName) throws Exce
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientClassLoaderTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
index 84ed3a324be4..081d3406fa1a 100644
--- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
+++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
@@ -31,6 +31,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee11.webapp.Configuration;
import org.eclipse.jetty.ee11.webapp.Configurations;
import org.eclipse.jetty.ee11.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
@@ -57,7 +59,7 @@ public class JakartaClientClassLoaderTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -150,6 +152,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
index 041aaa2a30e7..56f0f9729945 100644
--- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
+++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
@@ -25,6 +25,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee11.webapp.Configuration;
import org.eclipse.jetty.ee11.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
import org.eclipse.jetty.ee11.websocket.jakarta.client.webapp.JakartaWebSocketShutdownContainer;
@@ -51,7 +53,7 @@ public class JakartaClientShutdownWithServerWebAppTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -101,6 +103,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee11/websocket/tests/JettyClientClassLoaderTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee11/websocket/tests/JettyClientClassLoaderTest.java
index 227da13bca85..57995fa97fcd 100644
--- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee11/websocket/tests/JettyClientClassLoaderTest.java
+++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee11/websocket/tests/JettyClientClassLoaderTest.java
@@ -26,6 +26,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee11.websocket.client.config.JettyWebSocketClientConfiguration;
import org.eclipse.jetty.ee11.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.ee11.websocket.server.JettyWebSocketServletFactory;
@@ -61,7 +63,7 @@ public class JettyClientClassLoaderTest
private final HttpClient httpClient = new HttpClient();
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -170,6 +172,8 @@ public WebAppTester.WebApp createWebSocketWebapp(String contextName) throws Exce
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientClassLoaderTest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
index ccc71df660eb..df781edc22f6 100644
--- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
+++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientClassLoaderTest.java
@@ -31,6 +31,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee9.webapp.Configuration;
import org.eclipse.jetty.ee9.webapp.Configurations;
import org.eclipse.jetty.ee9.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
@@ -57,7 +59,7 @@ public class JakartaClientClassLoaderTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -150,6 +152,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
index 1295bc3b7958..c22e18bf8ed9 100644
--- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
+++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee9/websocket/jakarta/tests/JakartaClientShutdownWithServerWebAppTest.java
@@ -25,6 +25,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee9.webapp.Configuration;
import org.eclipse.jetty.ee9.webapp.Configurations;
import org.eclipse.jetty.ee9.websocket.jakarta.client.JakartaWebSocketClientContainerProvider;
@@ -50,7 +52,7 @@ public class JakartaClientShutdownWithServerWebAppTest
private HttpClient httpClient;
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -100,6 +102,8 @@ public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exceptio
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/JettyClientClassLoaderTest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/JettyClientClassLoaderTest.java
index 876add899168..9846bde7b516 100644
--- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/JettyClientClassLoaderTest.java
+++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee9/websocket/tests/JettyClientClassLoaderTest.java
@@ -26,6 +26,8 @@
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
import org.eclipse.jetty.ee9.websocket.api.Session;
import org.eclipse.jetty.ee9.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.ee9.websocket.api.annotations.OnWebSocketConnect;
@@ -60,7 +62,7 @@ public class JettyClientClassLoaderTest
private final HttpClient httpClient = new HttpClient();
@FunctionalInterface
- interface ThrowingRunnable
+ public interface ThrowingRunnable
{
void run() throws Exception;
}
@@ -169,6 +171,8 @@ public WebAppTester.WebApp createWebSocketWebapp(String contextName) throws Exce
app.copyLib(CoreClientUpgradeRequest.class, "jetty-websocket-core-client.jar");
app.copyLib(WebSocketComponents.class, "jetty-websocket-core-common.jar");
app.copyLib(Response.class, "jetty-client.jar");
+ app.copyLib(Compression.class, "jetty-compression-common.jar");
+ app.copyLib(GzipCompression.class, "jetty-compression-gzip.jar");
app.copyLib(EndPoint.class, "jetty-io.jar");
app.copyLib(BadMessageException.class, "jetty-http.jar");
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
From 8abb27d9a2a662392d25003f2661b1b2b1bb4436 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Fri, 6 Dec 2024 00:13:21 +0100
Subject: [PATCH 04/20] Reorganized the Jetty modules so that the client can
use them without installing CompressionHandler.
Deprecated gzip.mod in favor of compression-gzip.mod.
Renamed `CompressionHandler.registerCompression()` because there are many other methods called `put*` in the class.
Fixed `jetty-home/pom.xml` to generate the correct jetty-home.
Added distribution tests.
Signed-off-by: Simone Bordet
---
.../config/modules/brotli-compression.mod | 30 ++++++
.../config/modules/compression-brotli.mod | 56 ----------
.../config/etc/jetty-compression-gzip.xml | 46 --------
.../main/config/modules/compression-gzip.mod | 60 -----------
.../main/config/modules/gzip-compression.mod | 12 +++
.../config/etc/jetty-compression-brotli.xml | 18 ++--
.../config/etc/jetty-compression-gzip.xml | 42 ++++++++
.../etc/jetty-compression-zstandard.xml | 31 ++++++
.../src/main/config/etc/jetty-compression.xml | 8 --
.../config/modules/compression-brotli.mod | 41 +++++++
.../main/config/modules/compression-gzip.mod | 58 ++++++++++
.../config/modules/compression-zstandard.mod | 54 ++++++++++
.../src/main/config/modules/compression.mod | 9 +-
.../server/CompressionHandler.java | 102 +++++++++---------
.../compression/CompressionHandlerTest.java | 18 ++--
.../etc/jetty-compression-zstandard.xml | 35 ------
.../config/modules/compression-zstandard.mod | 87 ---------------
.../config/modules/zstandard-compression.mod | 46 ++++++++
.../src/main/config/modules/gzip.mod | 5 +-
jetty-home/pom.xml | 16 ++-
.../test-distribution-common/pom.xml | 15 +++
.../tests/distribution/DistributionTests.java | 49 +++++++++
22 files changed, 467 insertions(+), 371 deletions(-)
create mode 100644 jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/brotli-compression.mod
delete mode 100644 jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/compression-brotli.mod
delete mode 100644 jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/etc/jetty-compression-gzip.xml
delete mode 100644 jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/compression-gzip.mod
create mode 100644 jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/gzip-compression.mod
rename jetty-core/jetty-compression/{jetty-compression-brotli => jetty-compression-server}/src/main/config/etc/jetty-compression-brotli.xml (50%)
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
delete mode 100644 jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/etc/jetty-compression-zstandard.xml
delete mode 100644 jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/compression-zstandard.mod
create mode 100644 jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/zstandard-compression.mod
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/brotli-compression.mod b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/brotli-compression.mod
new file mode 100644
index 000000000000..6ca203c755ba
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/brotli-compression.mod
@@ -0,0 +1,30 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables support for the Brotli compression algorithm.
+
+[tags]
+compression
+brotli
+
+[files]
+maven://com.aayushatharva.brotli4j/brotli4j/${brotli4j.version}|lib/compression/brotli4j-${brotli4j.version}.jar
+maven://com.aayushatharva.brotli4j/service/${brotli4j.version}|lib/compression/brotli4j-service-${brotli4j.version}.jar
+maven://com.aayushatharva.brotli4j/native-windows-x86_64/${brotli4j.version}|lib/compression/brotli4j-native-windows-x86_64-${brotli4j.version}.jar
+maven://com.aayushatharva.brotli4j/native-linux-x86_64/${brotli4j.version}|lib/compression/brotli4j-native-linux-x86_64-${brotli4j.version}.jar
+
+[lib]
+lib/compression/jetty-compression-common-${jetty.version}.jar
+lib/compression/jetty-compression-brotli-${jetty.version}.jar
+lib/compression/brotli4j-${brotli4j.version}.jar
+lib/compression/brotli4j-service-${brotli4j.version}.jar
+lib/compression/brotli4j-native-windows-x86_64-${brotli4j.version}.jar
+lib/compression/brotli4j-native-linux-x86_64-${brotli4j.version}.jar
+
+[ini]
+brotli4j.version?=@brotli4j.version@
+
+[license]
+Brotli4j is distributed under Apache License 2.0
+Copyright 2021, Aayush Atharva
+https://github.com/hyperxpro/Brotli4j/blob/main/LICENSE
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/compression-brotli.mod b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/compression-brotli.mod
deleted file mode 100644
index 1e6f74b2a693..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/modules/compression-brotli.mod
+++ /dev/null
@@ -1,56 +0,0 @@
-# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
-
-[description]
-Enables Experimental Brotli algorithm for CompressionHandler server wide compression.
-
-[tags]
-server
-handler
-compression
-experimental
-
-[depend]
-compression
-
-[files]
-maven://com.aayushatharva.brotli4j/brotli4j/${brotli4j.version}|lib/compression/brotli4j-${brotli4j.version}.jar
-
-[lib]
-lib/compression/jetty-compression-brotli-${jetty.version}.jar
-lib/compression/brotli4j-${brotli4j.version}.jar
-
-[xml]
-etc/jetty-compression-brotli.xml
-
-[ini]
-brotli4j.version?=@brotli4j.version@
-
-[ini-template]
-## Minimum content length after which brotli is enabled
-# jetty.brotli.minCompressSize=32
-
-## Buffer Size for Decoder
-# jetty.brotli.decoder.bufferSize=16384
-
-## Buffer Size for Encoder
-# jetty.brotli.encoder.bufferSize=4096
-
-## Compression Level for Encoder
-# valid values from 0 to 11
-# jetty.brotli.encoder.compressionLevel=11
-
-## Strategy for Encoder
-# 0 = Generic
-# 1 = Text
-# 2 = Font
-# jetty.brotli.encoder.strategy=0
-
-## Brotli log2(LZ window size) for Encoder
-# valid values from 10 to 24
-# jetty.brotli.encoder.lgWindow=22
-
-[license]
-Brotli4j is distributed under Apache License 2.0
-Copyright 2021, Aayush Atharva
-https://github.com/hyperxpro/Brotli4j/blob/main/LICENSE
-
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/etc/jetty-compression-gzip.xml b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/etc/jetty-compression-gzip.xml
deleted file mode 100644
index d97dd9d2e8d4..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/etc/jetty-compression-gzip.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
- [
- ]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/compression-gzip.mod b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/compression-gzip.mod
deleted file mode 100644
index 43e23d02ee40..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/compression-gzip.mod
+++ /dev/null
@@ -1,60 +0,0 @@
-# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
-
-[description]
-Enables Experimental GZIP algorithm for CompressionHandler server wide compression.
-
-[tags]
-server
-handler
-compression
-experimental
-
-[depend]
-compression
-
-[lib]
-lib/compression/jetty-compression-gzip-${jetty.version}.jar
-
-[xml]
-etc/jetty-compression-gzip.xml
-
-[ini-template]
-## Minimum content length after which gzip is enabled
-# jetty.gzip.minCompressSize=32
-
-## Inflater pool max size (-1 for unlimited, 0 for no pooling)
-# jetty.gzip.inflaterPool.capacity=1024
-
-## Inflater pool use GZIP compatible compression
-#jetty.gzip.inflaterPool.noWrap=true
-
-## Deflater pool max size (-1 for unlimited, 0 for no pooling)
-# jetty.gzip.deflaterPool.capacity=1024
-
-## Deflater pool default compression level (-1 for default)
-# jetty.gzip.deflaterPool.compressionLevel=-1
-
-## Deflater pool use GZIP compatible compression
-# jetty.gzip.deflaterPool.noWrap=true
-
-## Buffer Size for Decoder
-# jetty.gzip.decoder.bufferSize=512
-
-## Buffer Size for Encoder
-# jetty.gzip.encoder.bufferSize=512
-
-## Compression Level for Encoder
-# valid values from 1 to 9
-# Leave at -1 for default compression level from java.util.zip.Deflater.
-# jetty.gzip.encoder.compressionLevel=-1
-
-## Strategy for Encoder
-# 0 for Default Strategy
-# 1 for Filtered
-# 2 for Huffman Only
-# see https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/Deflater.html
-# jetty.gzip.encoder.strategy=0
-
-## syncFlush for Encoder
-# true for SYNC_FLUSH, false for NO_FLUSH (default)
-# jetty.gzip.encoder.syncFlush=false
\ No newline at end of file
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/gzip-compression.mod b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/gzip-compression.mod
new file mode 100644
index 000000000000..e5e3ccc40143
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/config/modules/gzip-compression.mod
@@ -0,0 +1,12 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables support for the GZIP compression algorithm.
+
+[tags]
+compression
+gzip
+
+[lib]
+lib/compression/jetty-compression-common-${jetty.version}.jar
+lib/compression/jetty-compression-gzip-${jetty.version}.jar
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/etc/jetty-compression-brotli.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
similarity index 50%
rename from jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/etc/jetty-compression-brotli.xml
rename to jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
index f0685b57a07f..31c55130fe3d 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/config/etc/jetty-compression-brotli.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
@@ -1,27 +1,23 @@
-
-
-
-
[
- ]
+
-
+
-
+
-
-
-
-
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
new file mode 100644
index 000000000000..9075c2ddc8a2
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ [
+ ]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
new file mode 100644
index 000000000000..e07484875d6f
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ [
+ ]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression.xml
index 1a4b7a03f617..d6a3c6cc8582 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression.xml
@@ -1,12 +1,6 @@
-
-
-
-
-
-
@@ -15,5 +9,3 @@
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
new file mode 100644
index 000000000000..0b2661152cc2
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
@@ -0,0 +1,41 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables the Brotli compression algorithm in CompressionHandler.
+
+[tags]
+server
+handler
+compression
+brotli
+
+[depend]
+brotli-compression
+compression
+
+[xml]
+etc/jetty-compression-brotli.xml
+
+[ini-template]
+## Minimum content length after which brotli is enabled
+# jetty.compression.brotli.minCompressSize=32
+
+## Buffer Size for Decoder
+# jetty.compression.brotli.decoder.bufferSize=16384
+
+## Buffer Size for Encoder
+# jetty.compression.brotli.encoder.bufferSize=4096
+
+## Compression Level for Encoder
+# valid values from 0 to 11
+# jetty.compression.brotli.encoder.compressionLevel=11
+
+## Strategy for Encoder
+# 0 = Generic
+# 1 = Text
+# 2 = Font
+# jetty.compression.brotli.encoder.strategy=0
+
+## Brotli log2(LZ window size) for Encoder
+# valid values from 10 to 24
+# jetty.compression.brotli.encoder.lgWindow=22
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
new file mode 100644
index 000000000000..28c72d8687f5
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
@@ -0,0 +1,58 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables the GZIP compression algorithm in CompressionHandler.
+
+[tags]
+server
+handler
+compression
+gzip
+
+[depend]
+gzip-compression
+compression
+
+[xml]
+etc/jetty-compression-gzip.xml
+
+[ini-template]
+## Minimum content length after which gzip is enabled
+# jetty.compression.gzip.minCompressSize=32
+
+## Inflater pool max size (-1 for unlimited, 0 for no pooling)
+# jetty.compression.gzip.inflaterPool.capacity=1024
+
+## Inflater pool use GZIP compatible compression
+#jetty.compression.gzip.inflaterPool.noWrap=true
+
+## Deflater pool max size (-1 for unlimited, 0 for no pooling)
+# jetty.compression.gzip.deflaterPool.capacity=1024
+
+## Deflater pool default compression level (-1 for default)
+# jetty.compression.gzip.deflaterPool.compressionLevel=-1
+
+## Deflater pool use GZIP compatible compression
+# jetty.compression.gzip.deflaterPool.noWrap=true
+
+## Buffer Size for Decoder
+# jetty.compression.gzip.decoder.bufferSize=512
+
+## Buffer Size for Encoder
+# jetty.compression.gzip.encoder.bufferSize=512
+
+## Compression Level for Encoder
+## valid values from 1 to 9
+## Leave at -1 for default compression level from java.util.zip.Deflater.
+# jetty.compression.gzip.encoder.compressionLevel=-1
+
+## Strategy for Encoder
+## 0 for Default Strategy
+## 1 for Filtered
+## 2 for Huffman Only
+## See https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/zip/Deflater.html
+# jetty.compression.gzip.encoder.strategy=0
+
+## syncFlush for Encoder
+## true for SYNC_FLUSH, false for NO_FLUSH (default)
+# jetty.compression.gzip.encoder.syncFlush=false
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
new file mode 100644
index 000000000000..648d907db93a
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
@@ -0,0 +1,54 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables the Zstandard compression algorithm in CompressionHandler.
+
+[tags]
+server
+handler
+compression
+zstandard
+
+[depend]
+zstandard-compression
+compression
+
+[xml]
+etc/jetty-compression-zstandard.xml
+
+[ini-template]
+## Minimum content length after which brotli is enabled
+# jetty.compression.zstandard.minCompressSize=32
+
+## Buffer Size for Decoder
+# If unspecified, this default comes from zstd-jni's integration with the zstd libs.
+# jetty.compression.zstandard.decoder.bufferSize=128000
+
+## Enable/Disable Magicless frames for Decoder
+# Note: No browser zstandard implementations should be generating Magicless frames.
+# Leave at false for maximum compatibility with known browsers.
+# jetty.compression.zstandard.decoder.magicless=false
+
+## Buffer Size for Encoder
+# If unspecified, this default comes from zstd-jni's integration with the zstd libs.
+# jetty.compression.zstandard.encoder.bufferSize=128000
+
+## Compression Level for Encoder
+# If unspecified, this default comes from the zstd-jni's integration with the zstd libs.
+# valid values from 1 to 19
+# jetty.compression.zstandard.encoder.compressionLevel=3
+
+## Strategy for Encoder
+# If unspecified, this default comes from the zstd-jni's integration with the zstd libs.
+# See https://facebook.github.io/zstd/zstd_manual.html#Chapter5
+# valid values from 1 to 9
+# Leave at -1 for maximum compatibility with known browsers.
+# jetty.compression.zstandard.encoder.strategy=-1
+
+## Enable/Disable Magicless frames for Encoder
+# Note: browser zstandard implementations require this to be false.
+# jetty.compression.zstandard.encoder.magicless=false
+
+# Enable/Disable compression checksums
+# Note: browser zstandard implementations requires this to be false.
+# jetty.compression.zstandard.encoder.checksum=false
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
index 7198ffe1c662..e22d32d40cbd 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
@@ -1,14 +1,16 @@
# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
[description]
-Enables Experimental CompressionHandler for dynamic compression for the entire server.
-Enable a one or more compression libs too. (compression-gzip, compression-brotli, or compression-zstandard)
+Installs CompressionHandler at the root of the Handler tree,
+to support decompression of requests and compression of responses.
+Compression specific modules must be enabled to support specific
+compressoin algorithms, see module "compression-gzip",
+"compression-brotli" and "compression-zstandard".
[tags]
server
handler
compression
-experimental
[depend]
server
@@ -19,4 +21,3 @@ lib/compression/jetty-compression-server-${jetty.version}.jar
[xml]
etc/jetty-compression.xml
-
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
index 2a993dc94657..271d78675172 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
@@ -48,7 +48,7 @@
* Supports any arbitrary content-encoding via {@link org.eclipse.jetty.compression.Compression} implementations
* such as {@code gzip}, {@code zstd}, and {@code brotli}.
* By default, there are no {@link Compression} implementations that will be automatically added.
- * It is up to the user to call {@link #registerCompression(Compression)} to add which implementations that they want to use.
+ * It is up to the user to call {@link #putCompression(Compression)} to add which implementations that they want to use.
*
*
*
@@ -82,7 +82,7 @@ public CompressionHandler()
* @param compression the compression implementation.
* @return the previously registered compression with the same encoding name, can be null.
*/
- public Compression registerCompression(Compression compression)
+ public Compression putCompression(Compression compression)
{
Compression previous = supportedEncodings.put(compression.getEncodingName(), compression);
compression.setContainer(this);
@@ -96,7 +96,7 @@ public Compression registerCompression(Compression compression)
* @param encodingName the encoding name of the compression to remove.
* @return the Compression that was removed, can be null if no Compression exists on that encoding name.
*/
- public Compression unregisterCompression(String encodingName)
+ public Compression removeCompression(String encodingName)
{
Compression compression = supportedEncodings.remove(encodingName);
removeBean(compression);
@@ -186,6 +186,48 @@ public CompressionConfig putConfiguration(String pathSpecString, CompressionConf
return putConfiguration(pathSpec, config);
}
+ @Override
+ protected void doStart() throws Exception
+ {
+ // If the supported encodings is empty, that means this handler wasn't manually configured with encodings.
+ // Fallback to discovered encodings via the service loader instead.
+ if (supportedEncodings.isEmpty())
+ {
+ TypeUtil.serviceStream(ServiceLoader.load(Compression.class)).forEach(this::putCompression);
+ }
+
+ if (pathConfigs.isEmpty())
+ {
+ // add default configuration if no paths have been configured.
+ pathConfigs.put("/",
+ CompressionConfig.builder()
+ .from(MimeTypes.DEFAULTS)
+ .build());
+ }
+
+ // ensure that the preferred encoder order is sane for the configuration.
+ for (MappedResource pathConfig : pathConfigs)
+ {
+ List preferredEncoders = pathConfig.getResource().getCompressPreferredEncoderOrder();
+ if (preferredEncoders.isEmpty())
+ continue;
+ ListIterator preferredIter = preferredEncoders.listIterator();
+ while (preferredIter.hasNext())
+ {
+ String listedEncoder = preferredIter.next();
+ if (!supportedEncodings.containsKey(listedEncoder))
+ {
+ LOG.warn("Unable to find compression encoder {} from configuration for pathspec {} in registered compression encoders [{}]",
+ listedEncoder, pathConfig.getPathSpec(),
+ String.join(", ", supportedEncodings.keySet()));
+ preferredIter.remove(); // remove bad encoding
+ }
+ }
+ }
+
+ super.doStart();
+ }
+
@Override
public boolean handle(final Request request, final Response response, final Callback callback) throws Exception
{
@@ -314,54 +356,6 @@ public boolean handle(final Request request, final Response response, final Call
return false;
}
- @Override
- public String toString()
- {
- return String.format("%s@%x{%s,supported=%s}", getClass().getSimpleName(), hashCode(), getState(), String.join(",", supportedEncodings.keySet()));
- }
-
- @Override
- protected void doStart() throws Exception
- {
- // If the supported encodings is empty, that means this handler wasn't manually configured with encodings.
- // Fallback to discovered encodings via the service loader instead.
- if (supportedEncodings.isEmpty())
- {
- TypeUtil.serviceStream(ServiceLoader.load(Compression.class)).forEach(this::registerCompression);
- }
-
- if (pathConfigs.isEmpty())
- {
- // add default configuration if no paths have been configured.
- pathConfigs.put("/",
- CompressionConfig.builder()
- .from(MimeTypes.DEFAULTS)
- .build());
- }
-
- // ensure that the preferred encoder order is sane for the configuration.
- for (MappedResource pathConfig : pathConfigs)
- {
- List preferredEncoders = pathConfig.getResource().getCompressPreferredEncoderOrder();
- if (preferredEncoders.isEmpty())
- continue;
- ListIterator preferredIter = preferredEncoders.listIterator();
- while (preferredIter.hasNext())
- {
- String listedEncoder = preferredIter.next();
- if (!supportedEncodings.containsKey(listedEncoder))
- {
- LOG.warn("Unable to find compression encoder {} from configuration for pathspec {} in registered compression encoders [{}]",
- listedEncoder, pathConfig.getPathSpec(),
- String.join(", ", supportedEncodings.keySet()));
- preferredIter.remove(); // remove bad encoding
- }
- }
- }
-
- super.doStart();
- }
-
private Compression getCompression(String encoding)
{
Compression compression = supportedEncodings.get(encoding);
@@ -392,4 +386,10 @@ private Request newDecompressionRequest(Request request, String decompressEncodi
return new DecompressionRequest(compression, request);
}
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x{%s,supported=%s}", getClass().getSimpleName(), hashCode(), getState(), String.join(",", supportedEncodings.keySet()));
+ }
}
diff --git a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
index 46171ea4194c..09a265705456 100644
--- a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
+++ b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
@@ -104,7 +104,7 @@ public void testCompressEncodingsConfig(String compressionType,
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.compressIncludeEncoding("br")
.compressIncludeEncoding("gzip")
@@ -188,7 +188,7 @@ public void testCompressMimeTypesConfig(String compressionType,
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.compressIncludeMimeType("text/plain")
.compressIncludeMimeType("image/svg+xml")
@@ -249,7 +249,7 @@ public void testDefaultCompressionConfiguration(Class compressionCl
String message = "Hello Jetty!";
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
compressionHandler.setHandler(new Handler.Abstract()
{
@Override
@@ -295,7 +295,7 @@ public void testDefaultCompressionConfigurationText(Class compressi
String resourceBody = Files.readString(resourcePath, UTF_8);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
compressionHandler.setHandler(new Handler.Abstract()
{
@Override
@@ -396,7 +396,7 @@ public void testCompressPathConfig(String compressionType,
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.compressIncludePath("/path/*")
.compressExcludePath("*.png")
@@ -473,7 +473,7 @@ public void testDecompressMethodsConfig(String compressionType,
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(compression);
+ compressionHandler.putCompression(compression);
CompressionConfig config = CompressionConfig.builder()
.decompressIncludeMethod("GET")
.decompressIncludeMethod("POST")
@@ -611,9 +611,9 @@ public void testCompressPreferredEncoders(
byte[] resourceBody = Files.readAllBytes(resourcePath);
CompressionHandler compressionHandler = new CompressionHandler();
- compressionHandler.registerCompression(gzipCompression);
- compressionHandler.registerCompression(brotliCompression);
- compressionHandler.registerCompression(zstdCompression);
+ compressionHandler.putCompression(gzipCompression);
+ compressionHandler.putCompression(brotliCompression);
+ compressionHandler.putCompression(zstdCompression);
QuotedQualityCSV qcsv = new QuotedQualityCSV();
qcsv.addValue(preferredEncodingCsv);
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/etc/jetty-compression-zstandard.xml b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/etc/jetty-compression-zstandard.xml
deleted file mode 100644
index dba3cb3c205f..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/etc/jetty-compression-zstandard.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
- [
- ]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/compression-zstandard.mod b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/compression-zstandard.mod
deleted file mode 100644
index 4ecce18e384a..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/compression-zstandard.mod
+++ /dev/null
@@ -1,87 +0,0 @@
-# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
-
-[description]
-Enables Experimental Zstandard algorithm for CompressionHandler server wide compression.
-
-[tags]
-server
-handler
-compression
-experimental
-
-[depend]
-compression
-
-[files]
-maven://com.github.luben/zstd-jni/${zstd-jni.version}|lib/compression/zstd-jni-${zstd-jni.version}.jar
-
-[lib]
-lib/compression/jetty-compression-zstandard-${jetty.version}.jar
-lib/compression/zstd-jni-${zstd-jni.version}.jar
-
-[xml]
-etc/jetty-compression-zstandard.xml
-
-[ini-template]
-## Minimum content length after which brotli is enabled
-# jetty.zstandard.minCompressSize=32
-
-## Buffer Size for Decoder
-# If unspecified, this default comes from zstd-jni's integration with the zstd libs.
-# jetty.zstandard.decoder.bufferSize=128000
-
-## Enable/Disable Magicless frames for Decoder
-# Note: No browser zstandard implementations should be generating Magicless frames.
-# Leave at false for maximum compatibility with known browsers.
-# jetty.zstandard.decoder.magicless=false
-
-## Buffer Size for Encoder
-# If unspecified, this default comes from zstd-jni's integration with the zstd libs.
-# jetty.zstandard.encoder.bufferSize=128000
-
-## Compression Level for Encoder
-# If unspecified, this default comes from the zstd-jni's integration with the zstd libs.
-# valid values from 1 to 19
-# jetty.zstandard.encoder.compressionLevel=3
-
-## Strategy for Encoder
-# If unspecified, this default comes from the zstd-jni's integration with the zstd libs.
-# See https://facebook.github.io/zstd/zstd_manual.html#Chapter5
-# valid values from 1 to 9
-# Leave at -1 for maximum compatibility with known browsers.
-# jetty.zstandard.encoder.strategy=-1
-
-## Enable/Disable Magicless frames for Encoder
-# Note: browser zstandard implementations require this to be false.
-# jetty.zstandard.encoder.magicless=false
-
-# Enable/Disable compression checksums
-# Note: browser zstandard implementations requires this to be false.
-# jetty.zstandard.encoder.checksum=false
-
-[license]
-Zstd-jni: JNI bindings to Zstd Library
-Copyright (c) 2015-present, Luben Karavelov/ All rights reserved.
-BSD License
-https://github.com/luben/zstd-jni/blob/master/LICENSE
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice, this
- list of conditions and the following disclaimer in the documentation and/or
- other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/zstandard-compression.mod b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/zstandard-compression.mod
new file mode 100644
index 000000000000..2b1a52fd5f95
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/config/modules/zstandard-compression.mod
@@ -0,0 +1,46 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables support for the Zstandard compression algorithm.
+
+[tags]
+compression
+zstandard
+
+[files]
+maven://com.github.luben/zstd-jni/${zstd-jni.version}|lib/compression/zstd-jni-${zstd-jni.version}.jar
+
+[lib]
+lib/compression/jetty-compression-common-${jetty.version}.jar
+lib/compression/jetty-compression-zstandard-${jetty.version}.jar
+lib/compression/zstd-jni-${zstd-jni.version}.jar
+
+[ini]
+zstd-jni.version=@zstd-jni.version@
+
+[license]
+Zstd-jni: JNI bindings to Zstd Library
+Copyright (c) 2015-present, Luben Karavelov/ All rights reserved.
+BSD License
+https://github.com/luben/zstd-jni/blob/master/LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/jetty-core/jetty-server/src/main/config/modules/gzip.mod b/jetty-core/jetty-server/src/main/config/modules/gzip.mod
index 1fe563b62e7a..6f13b1ff30d1 100644
--- a/jetty-core/jetty-server/src/main/config/modules/gzip.mod
+++ b/jetty-core/jetty-server/src/main/config/modules/gzip.mod
@@ -2,11 +2,14 @@
[description]
Enables GzipHandler for dynamic gzip compression for the entire server.
-If MSIE prior to version 7 are to be handled, also enable the msie module.
[tags]
server
handler
+deprecated
+
+[deprecated]
+Use module compression-gzip instead.
[depend]
server
diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml
index a877bc756660..08a47d25f1e2 100644
--- a/jetty-home/pom.xml
+++ b/jetty-home/pom.xml
@@ -123,6 +123,11 @@
org.eclipse.jetty
jetty-util-ajax
+
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ ${project.version}
+
org.eclipse.jetty.compression
jetty-compression-gzip
@@ -133,6 +138,11 @@
jetty-compression-server
${project.version}
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
+ ${project.version}
+
org.eclipse.jetty.demos
jetty-core-demo-handler
@@ -484,7 +494,7 @@
generate-resources
org.eclipse.jetty
- org.eclipse.jetty.orbit,org.eclipse.jetty.http2,org.eclipse.jetty.http3,org.eclipse.jetty.quic,org.eclipse.jetty.websocket,org.eclipse.jetty.ee8.websocket,org.eclipse.jetty.ee9.websocket,org.eclipse.jetty.ee11.websocket,org.eclipse.jetty.fcgi,org.eclipse.jetty.toolchain,org.apache.taglibs
+ org.eclipse.jetty.orbit,org.eclipse.jetty.compression,org.eclipse.jetty.http2,org.eclipse.jetty.http3,org.eclipse.jetty.quic,org.eclipse.jetty.websocket,org.eclipse.jetty.ee8.websocket,org.eclipse.jetty.ee9.websocket,org.eclipse.jetty.ee11.websocket,org.eclipse.jetty.fcgi,org.eclipse.jetty.toolchain,org.apache.taglibs
jetty-ee8-apache-jsp,jetty-ee9-apache-jsp,jetty-ee10-apache-jsp,jetty-ee11-apache-jsp,jetty-ee8-glassfish-jstl,jetty-ee9-glassfish-jstl,jetty-ee10-glassfish-jstl,jetty-ee11-glassfish-jstl,jetty-start,jetty-slf4j-impl,javadoc
jar
${assembly-directory}/lib
@@ -498,7 +508,7 @@
generate-resources
org.eclipse.jetty
- org.eclipse.jetty.orbit,org.eclipse.jetty.http2,org.eclipse.jetty.http3,org.eclipse.jetty.quic,org.eclipse.jetty.websocket,org.eclipse.jetty.fcgi,org.eclipse.jetty.toolchain,org.apache.taglibs
+ org.eclipse.jetty.orbit,org.eclipse.jetty.compression,org.eclipse.jetty.http2,org.eclipse.jetty.http3,org.eclipse.jetty.quic,org.eclipse.jetty.websocket,org.eclipse.jetty.fcgi,org.eclipse.jetty.toolchain,org.apache.taglibs
jetty-ee8-apache-jsp,jetty-ee9-apache-jsp,jetty-ee10-apache-jsp,jetty-ee11-apache-jsp,jetty-ee8-glassfish-jstl,jetty-ee9-glassfish-jstl,jetty-ee10-glassfish-jstl,jetty-ee11-glassfish-jstl,jetty-start
jar
sources
@@ -513,7 +523,7 @@
generate-resources
org.eclipse.jetty.compression
- jetty-compression-api,jetty-compression-server,jetty-compression-gzip
+ jetty-compression-common,jetty-compression-brotli,jetty-compression-gzip,jetty-compression-server,jetty-compression-zstandard
jar
${assembly-directory}/lib/compression
diff --git a/tests/test-distribution/test-distribution-common/pom.xml b/tests/test-distribution/test-distribution-common/pom.xml
index 1899042a9d0c..1073be7a9f09 100644
--- a/tests/test-distribution/test-distribution-common/pom.xml
+++ b/tests/test-distribution/test-distribution-common/pom.xml
@@ -181,6 +181,21 @@
jetty-util-ajax
test
+
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-gzip
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
+ test
+
org.eclipse.jetty.ee10
jetty-ee10-test-log4j2-webapp
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
index 83863ad24cfb..1877e0a4eb69 100644
--- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
@@ -92,6 +92,7 @@
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
@@ -2206,6 +2207,51 @@ public void testLimitHandlers(String env) throws Exception
int port = Tester.freePort();
try (JettyHomeTester.Run run2 = distribution.start("jetty.http.selectors=1", "jetty.http.port=" + port))
+ {
+ assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
+
+ startHttpClient();
+ URI serverUri = URI.create("http://localhost:" + port + "/test/");
+ ContentResponse response = client.newRequest(serverUri)
+ .timeout(15, TimeUnit.SECONDS)
+ .send();
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"brotli", "gzip", "zstandard"})
+ public void testCompressionHandler(String encoding) throws Exception
+ {
+ String jettyVersion = System.getProperty("jettyVersion");
+ JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
+ .jettyVersion(jettyVersion)
+ .build();
+
+ String[] modules = {
+ "resources",
+ "http",
+ "compression-" + encoding,
+ "ee11-webapp",
+ "ee11-deploy"
+ };
+ try (JettyHomeTester.Run run1 = distribution.start("--approve-all-licenses", "--add-modules=" + String.join(",", modules)))
+ {
+ assertTrue(run1.awaitFor(10, TimeUnit.SECONDS));
+ assertEquals(0, run1.getExitValue());
+
+ Path jettyLogging = distribution.getJettyBase().resolve("resources/jetty-logging.properties");
+ String loggingConfig = """
+ org.eclipse.jetty.LEVEL=DEBUG
+ """;
+ Files.writeString(jettyLogging, loggingConfig, StandardOpenOption.TRUNCATE_EXISTING);
+
+ String coordinates = "org.eclipse.jetty.demos:jetty-servlet5-demo-simple-webapp:war:" + jettyVersion;
+ distribution.installWar(distribution.resolveArtifact(coordinates), "test");
+
+ int port = Tester.freePort();
+ try (JettyHomeTester.Run run2 = distribution.start("--approve-all-licenses", "jetty.http.selectors=1", "jetty.http.port=" + port))
{
try
{
@@ -2214,9 +2260,12 @@ public void testLimitHandlers(String env) throws Exception
startHttpClient();
URI serverUri = URI.create("http://localhost:" + port + "/test/");
ContentResponse response = client.newRequest(serverUri)
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, encoding))
.timeout(15, TimeUnit.SECONDS)
.send();
+
assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertThat(response.getContentAsString(), containsStringIgnoringCase("Hello World"));
}
finally
{
From 05d6b15af015209a2153a687173925a018acba1a Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Fri, 6 Dec 2024 18:00:39 +0100
Subject: [PATCH 05/20] * Removed EncoderSink.canEncode(), since it is too late
to skip encoding. * Fixed GzipEncoderSink, to finish deflating when
last=true. * Enhanced logic in CompressionResponse, to skip by status code,
content-type, content-encoding and empty content. * CompressionResponse does
not need to implement Callback. * Added tests for CompressionHandler in ee11.
* Code cleanups.
Signed-off-by: Simone Bordet
---
.../compression/brotli/BrotliCompression.java | 5 -
.../brotli/internal/BrotliEncoderSink.java | 5 +-
.../jetty/compression/EncoderSink.java | 73 ++---
.../compression/gzip/GzipCompression.java | 2 +
.../gzip/internal/GzipDecoderSource.java | 3 -
.../gzip/internal/GzipEncoderSink.java | 36 +--
.../server/CompressionHandler.java | 28 +-
.../server/internal/CompressionResponse.java | 121 ++++----
.../internal/ZstandardEncoderSink.java | 15 -
jetty-ee11/jetty-ee11-servlets/pom.xml | 24 +-
.../jetty/ee11/servlets/AbstractGzipTest.java | 2 +-
.../CompressionContentLengthTest.java | 271 ++++++++++++++++++
12 files changed, 411 insertions(+), 174 deletions(-)
create mode 100644 jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
index bd9f4bd37ccf..4813e62a9765 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
@@ -30,14 +30,11 @@
import org.eclipse.jetty.compression.EncoderSink;
import org.eclipse.jetty.compression.brotli.internal.BrotliDecoderSource;
import org.eclipse.jetty.compression.brotli.internal.BrotliEncoderSink;
-import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.RetainableByteBuffer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Brotli Compression.
@@ -47,8 +44,6 @@
public class BrotliCompression extends Compression
{
private static final List EXTENSIONS = List.of("br");
- private static final Logger LOG = LoggerFactory.getLogger(BrotliCompression.class);
- private static final CompressedContentFormat BR = new CompressedContentFormat("br", ".br");
private static final String ENCODING_NAME = "br";
private static final HttpField X_CONTENT_ENCODING = new PreEncodedHttpField("X-Content-Encoding", ENCODING_NAME);
private static final HttpField CONTENT_ENCODING = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, ENCODING_NAME);
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliEncoderSink.java b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliEncoderSink.java
index 9a9d9b94b06d..e66bdc0c1003 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliEncoderSink.java
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/internal/BrotliEncoderSink.java
@@ -34,7 +34,7 @@ enum State
*/
PROCESSING,
/**
- * Done taking input, flushing whats left in encoder.
+ * Done taking input, flushing what's left in encoder.
*/
FLUSHING,
/**
@@ -47,7 +47,6 @@ enum State
FINISHED
}
- private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
private final EncoderJNI.Wrapper encoder;
private final ByteBuffer inputBuffer;
private final AtomicReference state = new AtomicReference<>(State.PROCESSING);
@@ -118,7 +117,7 @@ protected WriteRecord encode(boolean last, ByteBuffer content)
inputBuffer.limit(inputBuffer.position());
ByteBuffer output = encode(EncoderJNI.Operation.FINISH);
state.compareAndSet(State.FINISHING, State.FINISHED);
- return new WriteRecord(true, output != null ? output : EMPTY_BUFFER, Callback.NOOP);
+ return new WriteRecord(true, output != null ? output : BufferUtil.EMPTY_BUFFER, Callback.NOOP);
}
case FINISHED ->
{
diff --git a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/EncoderSink.java b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/EncoderSink.java
index 7d3426dbf87d..09c665c509ef 100644
--- a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/EncoderSink.java
+++ b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/EncoderSink.java
@@ -35,38 +35,12 @@ protected EncoderSink(Content.Sink sink)
@Override
public void write(boolean last, ByteBuffer content, Callback callback)
{
- try
- {
- if (!canEncode(last, content))
- {
- callback.succeeded();
- return;
- }
- }
- catch (Throwable t)
- {
- callback.failed(t);
- return;
- }
-
if (content != null || last)
new EncodeBufferCallback(last, content, callback).iterate();
else
callback.succeeded();
}
- /**
- * Figure out if the encoding can be done with the provided content.
- *
- * @param last the last write.
- * @param content the content of the write event.
- * @return true if the {@link #encode(boolean, ByteBuffer)} should proceed.
- */
- protected boolean canEncode(boolean last, ByteBuffer content)
- {
- return true;
- }
-
protected abstract WriteRecord encode(boolean last, ByteBuffer content);
protected void release()
@@ -101,31 +75,6 @@ public EncodeBufferCallback(boolean last, ByteBuffer content, Callback callback)
this.last = last;
}
- @Override
- public String toString()
- {
- return String.format("%s[content=%s last=%b]",
- super.toString(),
- BufferUtil.toDetailString(content),
- last
- );
- }
-
- protected void finished()
- {
- state.set(State.FINISHED);
- release();
- }
-
- @Override
- protected void onCompleteFailure(Throwable x)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("On Complete Failure", x);
- release();
- super.onCompleteFailure(x);
- }
-
@Override
protected Action process()
{
@@ -161,5 +110,27 @@ private void write(WriteRecord writeRecord)
callback = Callback.combine(callback, writeRecord.callback);
sink.write(writeRecord.last, writeRecord.output, callback);
}
+
+ protected void finished()
+ {
+ state.set(State.FINISHED);
+ release();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable x)
+ {
+ release();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[content=%s,last=%b]",
+ super.toString(),
+ BufferUtil.toDetailString(content),
+ last
+ );
+ }
}
}
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
index c671fa9f0c87..247d1d41510b 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
@@ -43,6 +43,8 @@ public class GzipCompression extends Compression
private static final String ENCODING_NAME = "gzip";
private static final HttpField X_CONTENT_ENCODING = new PreEncodedHttpField("X-Content-Encoding", ENCODING_NAME);
private static final HttpField CONTENT_ENCODING = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, ENCODING_NAME);
+
+ // TODO: this field is never actually used.
private int minCompressSize = DEFAULT_MIN_GZIP_SIZE;
private DeflaterPool deflaterPool;
private InflaterPool inflaterPool;
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
index e6e6c75fc177..85371bd3556c 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipDecoderSource.java
@@ -25,8 +25,6 @@
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.compression.InflaterPool;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class GzipDecoderSource extends DecoderSource
{
@@ -35,7 +33,6 @@ private enum State
INITIAL, ID, CM, FLG, MTIME, XFL, OS, FLAGS, EXTRA_LENGTH, EXTRA, NAME, COMMENT, HCRC, DATA, CRC, ISIZE, FINISHED, ERROR
}
- private static final Logger LOG = LoggerFactory.getLogger(GzipDecoderSource.class);
// Unsigned Integer Max == 2^32
private static final long UINT_MAX = 0xFFFFFFFFL;
private final GzipCompression compression;
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipEncoderSink.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipEncoderSink.java
index 23fb15f582ff..91f533d091e7 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipEncoderSink.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/internal/GzipEncoderSink.java
@@ -84,12 +84,6 @@ enum State
private final int bufferSize;
private final CRC32 crc = new CRC32();
private final AtomicReference state = new AtomicReference<>(State.HEADERS);
- /**
- * Number of input bytes provided to the deflater.
- * This is different then {@link Deflater#getTotalIn()} as that only shows
- * the number of input bytes that have been read.
- */
- private long inputBytesProvided = 0;
public GzipEncoderSink(GzipCompression compression, Content.Sink sink, GzipEncoderConfig config)
{
@@ -114,7 +108,6 @@ protected void addInput(ByteBuffer content)
int space = Math.min(input.remaining(), content.remaining());
ByteBuffer slice = content.slice();
slice.limit(space);
- inputBytesProvided += slice.remaining();
// Update CRC based on what can be consumed right now.
// Any leftover content will be consumed on a later call.
crc.update(slice.slice());
@@ -151,25 +144,22 @@ protected WriteRecord encode(boolean last, ByteBuffer content)
output = compression.acquireByteBuffer(bufferSize);
if (encode(content, output.getByteBuffer()))
{
- if (output.hasRemaining())
- {
- WriteRecord writeRecord = new WriteRecord(false, output.getByteBuffer(), Callback.from(output::release));
- output = null;
- return writeRecord;
- }
+ WriteRecord writeRecord = new WriteRecord(false, output.getByteBuffer(), Callback.from(output::release));
+ output = null;
+ return writeRecord;
}
}
- else if (inputBytesProvided > 0)
- {
- // no remaining content (and input has been provided)
- return null;
- }
- if (!content.hasRemaining() && last)
+ else
{
- state.compareAndSet(State.BODY, State.FLUSHING);
- // Reset input, so that Gzip stops looking at ByteBuffer (that might be reused)
- // deflater.setInput(EMPTY_BUFFER);
- deflater.finish();
+ if (last)
+ {
+ state.compareAndSet(State.BODY, State.FLUSHING);
+ deflater.finish();
+ }
+ else
+ {
+ return null;
+ }
}
}
case FLUSHING ->
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
index 271d78675172..444c191db11e 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
@@ -232,7 +232,7 @@ protected void doStart() throws Exception
public boolean handle(final Request request, final Response response, final Callback callback) throws Exception
{
if (LOG.isDebugEnabled())
- LOG.debug("{} handle {}", this, request);
+ LOG.debug("handling {} {} {}", request, response, this);
Handler next = getHandler();
if (next == null)
@@ -249,7 +249,7 @@ public boolean handle(final Request request, final Response response, final Call
if (matchedConfig == null)
{
if (LOG.isDebugEnabled())
- LOG.debug("Skipping Compression: Path {} has no matching compression config", pathInContext);
+ LOG.debug("skipping compression: path {} has no matching compression config", pathInContext);
// No configuration, skip
return next.handle(request, response, callback);
}
@@ -306,7 +306,7 @@ public boolean handle(final Request request, final Response response, final Call
if (LOG.isDebugEnabled())
{
- LOG.debug("Request[{}] Content-Encoding={}, Accept-Encoding={}, decompressEncoding={}, compressEncoding={}",
+ LOG.debug("request[{}] Content-Encoding={}, Accept-Encoding={}, decompressEncoding={}, compressEncoding={}",
request, requestContentEncoding, requestAcceptEncoding, decompressEncoding, compressEncoding);
}
@@ -314,14 +314,13 @@ public boolean handle(final Request request, final Response response, final Call
if (decompressEncoding == null && compressEncoding == null)
{
if (LOG.isDebugEnabled())
- LOG.debug("Skipping Compression and Decompression: no request encoding matches");
+ LOG.debug("skipping compression and decompression: no request encoding matches");
// No need for a Vary header, as we will never deflate
return next.handle(request, response, callback);
}
Request decompressionRequest = request;
Response compressionResponse = response;
- Callback compressionCallback = callback;
// We need to wrap the request IFF we are inflating or have seen etags with compression separators
if (decompressEncoding != null || etagMatches)
@@ -338,21 +337,20 @@ public boolean handle(final Request request, final Response response, final Call
response.getHeaders().ensureField(config.getVary());
}
- Response compression = newCompressionResponse(request, response, callback, compressEncoding, config);
- compressionResponse = compression;
- if (compression instanceof Callback dynamicCallback)
- compressionCallback = dynamicCallback;
+ compressionResponse = newCompressionResponse(request, response, compressEncoding, config);
}
+ if (LOG.isDebugEnabled())
+ LOG.debug("handle {} {} {}", decompressionRequest, compressionResponse, this);
+
// Call handle() with the possibly wrapped request, response and callback
- if (next.handle(decompressionRequest, compressionResponse, compressionCallback))
+ if (next.handle(decompressionRequest, compressionResponse, callback))
return true;
// If the request was not accepted, destroy any compressRequest wrapper
if (request instanceof DecompressionRequest decompressRequest)
- {
decompressRequest.destroy();
- }
+
return false;
}
@@ -362,20 +360,20 @@ private Compression getCompression(String encoding)
if (compression == null)
{
if (LOG.isDebugEnabled())
- LOG.debug("No Compression found for encoding type {}", encoding);
+ LOG.debug("no compression found for encoding type {}", encoding);
return null;
}
return compression;
}
- private Response newCompressionResponse(Request request, Response response, Callback callback, String compressEncoding, CompressionConfig config)
+ private Response newCompressionResponse(Request request, Response response, String compressEncoding, CompressionConfig config)
{
Compression compression = getCompression(compressEncoding);
if (compression == null)
return response;
- return new CompressionResponse(compression, request, response, callback, config);
+ return new CompressionResponse(request, response, compression, config);
}
private Request newDecompressionRequest(Request request, String decompressEncoding)
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
index 6f0895074bbd..b354e7cc3af0 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
@@ -21,59 +21,32 @@
import org.eclipse.jetty.compression.server.CompressionConfig;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
-import org.eclipse.jetty.util.thread.Invocable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-public class CompressionResponse extends Response.Wrapper implements Callback, Invocable
+public class CompressionResponse extends Response.Wrapper
{
- enum State
- {
- MIGHT_COMPRESS,
- NOT_COMPRESSING,
- COMPRESSING
- }
+ private static final Logger LOG = LoggerFactory.getLogger(CompressionResponse.class);
- private final Callback callback;
private final CompressionConfig config;
private final Compression compression;
private final AtomicReference state = new AtomicReference<>(State.MIGHT_COMPRESS);
private EncoderSink encoderSink;
- private boolean last;
- public CompressionResponse(Compression compression, Request request, Response wrapped, Callback callback, CompressionConfig config)
+ public CompressionResponse(Request request, Response wrapped, Compression compression, CompressionConfig config)
{
super(request, wrapped);
- this.callback = callback;
this.config = config;
this.compression = compression;
}
- @Override
- public void failed(Throwable x)
- {
- this.callback.failed(x);
- }
-
- @Override
- public InvocationType getInvocationType()
- {
- return this.callback.getInvocationType();
- }
-
- @Override
- public void succeeded()
- {
- // We need to write nothing here to intercept the committing of the
- // response and possibly change headers in case write is never called.
- if (last)
- this.callback.succeeded();
- else
- write(true, null, this.callback);
- }
-
@Override
public void write(boolean last, ByteBuffer content, Callback callback)
{
@@ -81,41 +54,79 @@ public void write(boolean last, ByteBuffer content, Callback callback)
{
case MIGHT_COMPRESS ->
{
- boolean compressing = false;
-
- HttpField contentTypeField = getHeaders().getField(HttpHeader.CONTENT_TYPE);
- if (contentTypeField == null)
+ int status = getStatus();
+ if (status > 0 && (
+ HttpStatus.isInformational(status) ||
+ status == HttpStatus.NO_CONTENT_204 ||
+ status == HttpStatus.RESET_CONTENT_205) &&
+ !HttpMethod.HEAD.is(getRequest().getMethod()))
{
- compressing = state.compareAndSet(State.MIGHT_COMPRESS, State.COMPRESSING);
+ if (LOG.isDebugEnabled())
+ LOG.debug("no compression for status {} {}", status, this);
+ state.compareAndSet(State.MIGHT_COMPRESS, State.NOT_COMPRESSING);
+ super.write(last, content, callback);
+ return;
}
- else
+
+ // TODO: handle 304's etag.
+
+ HttpField contentTypeField = getHeaders().getField(HttpHeader.CONTENT_TYPE);
+ if (contentTypeField != null)
{
String mimeType = MimeTypes.getContentTypeWithoutCharset(contentTypeField.getValue());
- if (config.isCompressMimeTypeSupported(mimeType))
- {
- compressing = state.compareAndSet(State.MIGHT_COMPRESS, State.COMPRESSING);
- }
- else
+ if (!config.isCompressMimeTypeSupported(mimeType))
{
+ if (LOG.isDebugEnabled())
+ LOG.debug("no compression for unsupported content type {} {}", mimeType, this);
state.compareAndSet(State.MIGHT_COMPRESS, State.NOT_COMPRESSING);
+ super.write(last, content, callback);
+ return;
}
}
- if (compressing)
+ // Did the application explicitly set the Content-Encoding?
+ String contentEncoding = getHeaders().get(HttpHeader.CONTENT_ENCODING);
+ if (contentEncoding != null)
{
- this.encoderSink = compression.newEncoderSink(getWrapped());
- getHeaders().put(compression.getContentEncodingField());
+ if (LOG.isDebugEnabled())
+ LOG.debug("no compression for explicit content encoding {} {}", contentEncoding, this);
+ state.compareAndSet(State.MIGHT_COMPRESS, State.NOT_COMPRESSING);
+ super.write(last, content, callback);
+ return;
}
+ // If there is nothing to write, don't compress.
+ if (last && BufferUtil.isEmpty(content))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("no compression, nothing to write {}", this);
+ state.compareAndSet(State.MIGHT_COMPRESS, State.NOT_COMPRESSING);
+ super.write(last, content, callback);
+ return;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("compressing {} {}", compression.getEncodingName(), this);
+
+ state.compareAndSet(State.MIGHT_COMPRESS, State.COMPRESSING);
+ this.encoderSink = compression.newEncoderSink(getWrapped());
+
+ // Adjust the headers.
+ getHeaders().put(compression.getContentEncodingField());
+ getHeaders().remove(HttpHeader.CONTENT_LENGTH);
+ // TODO: etag.
+
this.write(last, content, callback);
}
- case COMPRESSING ->
- {
- encoderSink.write(last, content, callback);
- if (last)
- this.last = true;
- }
+ case COMPRESSING -> encoderSink.write(last, content, callback);
case NOT_COMPRESSING -> super.write(last, content, callback);
}
}
+
+ enum State
+ {
+ MIGHT_COMPRESS,
+ NOT_COMPRESSING,
+ COMPRESSING
+ }
}
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardEncoderSink.java b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardEncoderSink.java
index 9557018eba37..9a2356c9d873 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardEncoderSink.java
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/internal/ZstandardEncoderSink.java
@@ -19,7 +19,6 @@
import com.github.luben.zstd.EndDirective;
import com.github.luben.zstd.ZstdCompressCtx;
-import com.github.luben.zstd.ZstdFrameProgression;
import org.eclipse.jetty.compression.EncoderSink;
import org.eclipse.jetty.compression.zstandard.ZstandardCompression;
import org.eclipse.jetty.compression.zstandard.ZstandardEncoderConfig;
@@ -63,20 +62,6 @@ public ZstandardEncoderSink(ZstandardCompression compression, Content.Sink sink,
this.compressCtx.setChecksum(config.isChecksum());
}
- @Override
- protected boolean canEncode(boolean last, ByteBuffer content)
- {
- if (!content.hasRemaining())
- {
- // skip if progress not yet started.
- // this allows for empty body contents to not cause errors.
- ZstdFrameProgression frameProgression = compressCtx.getFrameProgression();
- return frameProgression.getConsumed() > 0;
- }
-
- return true;
- }
-
@Override
protected WriteRecord encode(boolean last, ByteBuffer content)
{
diff --git a/jetty-ee11/jetty-ee11-servlets/pom.xml b/jetty-ee11/jetty-ee11-servlets/pom.xml
index 4ab4d2b5a0e0..67737aba3beb 100644
--- a/jetty-ee11/jetty-ee11-servlets/pom.xml
+++ b/jetty-ee11/jetty-ee11-servlets/pom.xml
@@ -17,6 +17,10 @@
+
+ org.eclipse.jetty
+ jetty-client
+
org.eclipse.jetty
jetty-http
@@ -53,15 +57,29 @@
jetty-jmx
test
-
org.eclipse.jetty
jetty-slf4j-impl
test
- org.eclipse.jetty.toolchain
- jetty-test-helper
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-gzip
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-server
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
test
diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/AbstractGzipTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/AbstractGzipTest.java
index 70932f06b3a4..b2a637877e30 100644
--- a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/AbstractGzipTest.java
+++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/AbstractGzipTest.java
@@ -45,7 +45,7 @@ public abstract class AbstractGzipTest
{
protected static final int DEFAULT_OUTPUT_BUFFER_SIZE = new HttpConfiguration().getOutputBufferSize();
- protected WorkDir workDir;
+ public WorkDir workDir;
protected FilterInputStream newContentEncodingFilterInputStream(String contentEncoding, InputStream inputStream) throws IOException
{
diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
new file mode 100644
index 000000000000..38f7419434ab
--- /dev/null
+++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
@@ -0,0 +1,271 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee11.servlets;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import jakarta.servlet.Servlet;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.compression.Compression;
+import org.eclipse.jetty.compression.brotli.BrotliCompression;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
+import org.eclipse.jetty.compression.server.CompressionHandler;
+import org.eclipse.jetty.compression.zstandard.ZstandardCompression;
+import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.Sha1Sum;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Test the {@code CompressionHandler} support for the various ways that a webapp can set {@code Content-Length}.
+ */
+@ExtendWith(WorkDirExtension.class)
+public class CompressionContentLengthTest
+{
+ public static Stream scenarios()
+ {
+ // The list of servlets that implement various content sending behaviors
+ // some behaviors are saner than others, but they are all real world scenarios
+ // that we have seen or had issues reported against Jetty.
+ List> servlets = new ArrayList<>();
+
+ // AsyncContext create -> timeout -> onTimeout -> write-response -> complete
+ servlets.add(AsyncTimeoutCompleteWrite.Default.class);
+ servlets.add(AsyncTimeoutCompleteWrite.Passed.class);
+ // AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response
+ servlets.add(AsyncTimeoutDispatchWrite.Default.class);
+ servlets.add(AsyncTimeoutDispatchWrite.Passed.class);
+ // AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response
+ servlets.add(AsyncScheduledDispatchWrite.Default.class);
+ servlets.add(AsyncScheduledDispatchWrite.Passed.class);
+
+ // HttpOutput usage scenario from http://bugs.eclipse.org/450873
+ // 1. getOutputStream()
+ // 2. setHeader(content-type)
+ // 3. setHeader(content-length)
+ // 4. (unwrapped) HttpOutput.write(ByteBuffer)
+ servlets.add(HttpOutputWriteFileContentServlet.class);
+
+ // The following blocking scenarios are from http://bugs.eclipse.org/354014
+ // Blocking
+ // 1. setHeader(content-length)
+ // 2. getOutputStream()
+ // 3. setHeader(content-type)
+ // 4. outputStream.write()
+ servlets.add(BlockingServletLengthStreamTypeWrite.class);
+ // Blocking
+ // 1. setHeader(content-length)
+ // 2. setHeader(content-type)
+ // 3. getOutputStream()
+ // 4. outputStream.write()
+ servlets.add(BlockingServletLengthTypeStreamWrite.class);
+ // Blocking
+ // 1. getOutputStream()
+ // 2. setHeader(content-length)
+ // 3. setHeader(content-type)
+ // 4. outputStream.write()
+ servlets.add(BlockingServletStreamLengthTypeWrite.class);
+ // Blocking
+ // 1. getOutputStream()
+ // 2. setHeader(content-length)
+ // 3. setHeader(content-type)
+ // 4. outputStream.write() (with frequent response flush)
+ servlets.add(BlockingServletStreamLengthTypeWriteWithFlush.class);
+ // Blocking
+ // 1. getOutputStream()
+ // 2. setHeader(content-type)
+ // 3. setHeader(content-length)
+ // 4. outputStream.write()
+ servlets.add(BlockingServletStreamTypeLengthWrite.class);
+ // Blocking
+ // 1. setHeader(content-type)
+ // 2. setHeader(content-length)
+ // 3. getOutputStream()
+ // 4. outputStream.write()
+ servlets.add(BlockingServletTypeLengthStreamWrite.class);
+ // Blocking
+ // 1. setHeader(content-type)
+ // 2. getOutputStream()
+ // 3. setHeader(content-length)
+ // 4. outputStream.write()
+ servlets.add(BlockingServletTypeStreamLengthWrite.class);
+
+ int defaultSize = 32 * 1024;
+ List scenarios = new ArrayList<>();
+ for (Compression compression : List.of(new BrotliCompression(), new GzipCompression(), new ZstandardCompression()))
+ {
+ for (Class extends Servlet> servlet : servlets)
+ {
+ for (CompressionWrapping compressionWrapping : CompressionWrapping.values())
+ {
+ // Not compressible (not large enough)
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, 0, "empty.txt", false));
+
+ // Compressible.
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, 16, "file-tiny.txt", true));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize / 2, "file-small.txt", true));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize, "file-medium.txt", true));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize * 4, "file-large.txt", true));
+
+ // Not compressible (not a matching Content-Type)
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize / 2, "file-small.mp3", false));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize, "file-medium.mp3", false));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize * 4, "file-large.mp3", false));
+ }
+ }
+ }
+
+ return scenarios.stream();
+ }
+
+ public WorkDir workDir;
+ private Server server;
+
+ @AfterEach
+ public void stopServer()
+ {
+ LifeCycle.stop(server);
+ }
+
+ @ParameterizedTest
+ @MethodSource("scenarios")
+ public void executeScenario(Compression compression, CompressionWrapping compressionWrapping, Class extends Servlet> contentServlet, int fileSize, String fileName, boolean compressible) throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server, 1, 1);
+ server.addConnector(connector);
+
+ Path contextDir = workDir.getEmptyPathDir().resolve("context");
+ FS.ensureDirExists(contextDir);
+
+ ServletContextHandler servletContextHandler = new ServletContextHandler();
+ servletContextHandler.setContextPath("/context");
+ servletContextHandler.setBaseResourceAsPath(contextDir);
+ servletContextHandler.addServlet(contentServlet, "/*");
+ CompressionHandler compressionHandler = new CompressionHandler();
+ compressionHandler.putCompression(compression);
+
+ switch (compressionWrapping)
+ {
+ case INTERNAL:
+ servletContextHandler.insertHandler(compressionHandler);
+ server.setHandler(servletContextHandler);
+ break;
+ case EXTERNAL:
+ compressionHandler.setHandler(servletContextHandler);
+ server.setHandler(compressionHandler);
+ break;
+ }
+
+ Path file = createFile(contextDir, fileName, fileSize);
+ String expectedSha1Sum = Sha1Sum.calculate(file);
+
+ server.start();
+
+ try (HttpClient httpClient = new HttpClient())
+ {
+ httpClient.start();
+
+ AtomicReference contentEncoding = new AtomicReference<>();
+ ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort())
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, compression.getEncodingName()))
+ .path("/context/" + file.getFileName())
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncoding.set(f.getValue());
+ return true;
+ })
+ .timeout(555, TimeUnit.SECONDS)
+ .send();
+
+ assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200));
+
+ if (compressible)
+ assertThat(contentEncoding.get(), is(compression.getEncodingName()));
+ else
+ assertNull(contentEncoding.get());
+
+ byte[] responseContent = response.getContent();
+ assertThat("(Uncompressed) Content Length", responseContent.length, is(fileSize));
+ assertThat("(Uncompressed) Content Hash", Sha1Sum.calculate(responseContent), is(expectedSha1Sum));
+ }
+ }
+
+ private Path createFile(Path contextDir, String fileName, int fileSize) throws IOException
+ {
+ Path destPath = contextDir.resolve(fileName);
+ byte[] content = generateContent(fileSize);
+ Files.write(destPath, content, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+ return destPath;
+ }
+
+ private byte[] generateContent(int length)
+ {
+ String sample = """
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.
+ Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque
+ habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
+ Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam
+ at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate
+ velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.
+ Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum
+ eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa
+ sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam
+ consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.
+ Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse
+ et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.
+ """;
+ String result = sample;
+ while (result.length() < length)
+ {
+ result += sample;
+ }
+ // Make sure we are exactly at requested length. (truncate the extra)
+ if (result.length() > length)
+ result = result.substring(0, length);
+
+ return result.getBytes(UTF_8);
+ }
+
+ public enum CompressionWrapping
+ {
+ INTERNAL, EXTERNAL
+ }
+}
From 4463087e09cb2ab5a5d235014d80a9cc72d2cbfb Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Fri, 6 Dec 2024 18:21:46 +0100
Subject: [PATCH 06/20] Fixes #12618 - Update jetty-compression for OSGi.
Signed-off-by: Simone Bordet
---
jetty-core/jetty-client/pom.xml | 11 ++++++++
.../jetty-compression-brotli/pom.xml | 20 +++++++++++++++
.../jetty-compression-common/pom.xml | 3 +++
.../jetty-compression-gzip/pom.xml | 25 +++++++++++++++----
.../jetty-compression-server/pom.xml | 17 +++++++++++++
.../jetty-compression-zstandard/pom.xml | 21 ++++++++++++++++
.../jetty/ee10/osgi/test/TestOSGiUtil.java | 2 ++
.../jetty/ee9/osgi/test/TestOSGiUtil.java | 2 ++
8 files changed, 96 insertions(+), 5 deletions(-)
diff --git a/jetty-core/jetty-client/pom.xml b/jetty-core/jetty-client/pom.xml
index 2143de36defa..06e05b691107 100644
--- a/jetty-core/jetty-client/pom.xml
+++ b/jetty-core/jetty-client/pom.xml
@@ -85,6 +85,17 @@
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ ${osgi.slf4j.import.packages},org.eclipse.jetty.compression;resolution:=optional,*
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.compression.Compression)";resolution:=optional;cardinality:=multiple
+
+
+
org.apache.maven.plugins
maven-dependency-plugin
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/pom.xml b/jetty-core/jetty-compression/jetty-compression-brotli/pom.xml
index 39c3cf19d485..a582b1ac74bc 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/pom.xml
@@ -34,4 +34,24 @@
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ Brotli Compression
+ ${osgi.slf4j.import.packages},com.aayushatharva.brotli4j.*;version="${brotli4j.version}",*
+ *
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional
+ osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.compression.Compression
+ <_nouses>true
+
+
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-common/pom.xml b/jetty-core/jetty-compression/jetty-compression-common/pom.xml
index a814d9e020d0..cc450719ed2e 100644
--- a/jetty-core/jetty-compression/jetty-compression-common/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-common/pom.xml
@@ -13,9 +13,11 @@
jar
Core :: Compression :: Common
Jetty Core Compression Common
+
${project.groupId}.common
+
org.eclipse.jetty
@@ -35,4 +37,5 @@
test
+
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/pom.xml b/jetty-core/jetty-compression/jetty-compression-gzip/pom.xml
index 80ff82d414e5..c7c3dd6e1014 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/pom.xml
@@ -26,10 +26,25 @@
jetty-slf4j-impl
test
-
- org.eclipse.jetty.toolchain
- jetty-test-helper
- test
-
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ GZIP Compression
+ *
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional
+ osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.compression.Compression
+ <_nouses>true
+
+
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-server/pom.xml b/jetty-core/jetty-compression/jetty-compression-server/pom.xml
index 299ce6206087..c28c6b3e0796 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/pom.xml
@@ -31,4 +31,21 @@
test
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ ${osgi.slf4j.import.packages},org.eclipse.jetty.compression;resolution:=optional,*
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.compression.Compression)";resolution:=optional;cardinality:=multiple
+
+
+
+
+
+
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml b/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
index 3fee9449194a..fe9ace01a959 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
@@ -33,4 +33,25 @@
test
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ Brotli Compression
+ ${osgi.slf4j.import.packages},com.github.luben.zstd.*;version="${zstd-jni.version}",*
+ *
+ osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional
+ osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.compression.Compression
+ <_nouses>true
+
+
+
+
+
+
diff --git a/jetty-ee10/jetty-ee10-osgi/test-jetty-ee10-osgi/src/test/java/org/eclipse/jetty/ee10/osgi/test/TestOSGiUtil.java b/jetty-ee10/jetty-ee10-osgi/test-jetty-ee10-osgi/src/test/java/org/eclipse/jetty/ee10/osgi/test/TestOSGiUtil.java
index 8b62c569c08e..fd34475cabe6 100644
--- a/jetty-ee10/jetty-ee10-osgi/test-jetty-ee10-osgi/src/test/java/org/eclipse/jetty/ee10/osgi/test/TestOSGiUtil.java
+++ b/jetty-ee10/jetty-ee10-osgi/test-jetty-ee10-osgi/src/test/java/org/eclipse/jetty/ee10/osgi/test/TestOSGiUtil.java
@@ -213,6 +213,8 @@ public static void coreJettyDependencies(List res)
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-jndi").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-osgi").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-common").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-gzip").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-ee").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-servlet").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-webapp").versionAsInProject().start());
diff --git a/jetty-ee9/jetty-ee9-osgi/test-jetty-ee9-osgi/src/test/java/org/eclipse/jetty/ee9/osgi/test/TestOSGiUtil.java b/jetty-ee9/jetty-ee9-osgi/test-jetty-ee9-osgi/src/test/java/org/eclipse/jetty/ee9/osgi/test/TestOSGiUtil.java
index 388b36005240..50005bf9e045 100644
--- a/jetty-ee9/jetty-ee9-osgi/test-jetty-ee9-osgi/src/test/java/org/eclipse/jetty/ee9/osgi/test/TestOSGiUtil.java
+++ b/jetty-ee9/jetty-ee9-osgi/test-jetty-ee9-osgi/src/test/java/org/eclipse/jetty/ee9/osgi/test/TestOSGiUtil.java
@@ -233,6 +233,8 @@ public static void coreJettyDependencies(List res)
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-jndi").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-osgi").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-client").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-common").versionAsInProject().start());
+ res.add(mavenBundle().groupId("org.eclipse.jetty.compression").artifactId("jetty-compression-gzip").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-ee").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee9").artifactId("jetty-ee9-security").versionAsInProject().start());
res.add(mavenBundle().groupId("org.eclipse.jetty.ee9").artifactId("jetty-ee9-servlet").versionAsInProject().start());
From a1ed64d3dac54968c0b12d83d1ea442bd7e1a8fb Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Sat, 7 Dec 2024 00:34:44 +0100
Subject: [PATCH 07/20] Fixed client.mod dependencies.
Signed-off-by: Simone Bordet
---
jetty-core/jetty-client/src/main/config/modules/client.mod | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/jetty-core/jetty-client/src/main/config/modules/client.mod b/jetty-core/jetty-client/src/main/config/modules/client.mod
index 8a541ca6ae79..6d9f7190ab59 100644
--- a/jetty-core/jetty-client/src/main/config/modules/client.mod
+++ b/jetty-core/jetty-client/src/main/config/modules/client.mod
@@ -6,9 +6,10 @@ Adds the Jetty HTTP client dependencies to the server classpath.
[tags]
client
+[depends]
+gzip-compression
+
[lib]
lib/jetty-alpn-client-${jetty.version}.jar
lib/jetty-alpn-java-client-${jetty.version}.jar
lib/jetty-client-${jetty.version}.jar
-lib/jetty-compression-common-${jetty.version}.jar
-lib/jetty-compression-gzip-${jetty.version}.jar
From f7806b8deb916e4dd76b3582f3d4939be7537964 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Sat, 7 Dec 2024 12:49:42 +0100
Subject: [PATCH 08/20] Updated test dependencies.
Signed-off-by: Simone Bordet
---
.../java/org/eclipse/jetty/tests/jpms/JPMSWebSocketTest.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test-jpms/src/test/java/org/eclipse/jetty/tests/jpms/JPMSWebSocketTest.java b/tests/test-jpms/src/test/java/org/eclipse/jetty/tests/jpms/JPMSWebSocketTest.java
index 447f5e0fc191..f90ee4163e63 100644
--- a/tests/test-jpms/src/test/java/org/eclipse/jetty/tests/jpms/JPMSWebSocketTest.java
+++ b/tests/test-jpms/src/test/java/org/eclipse/jetty/tests/jpms/JPMSWebSocketTest.java
@@ -91,6 +91,8 @@ public void testJPMSWebSocket(WorkDir workDir) throws Exception
.addToModulePath("org.eclipse.jetty:jetty-util:" + jettyVersion)
.addToModulePath("org.slf4j:slf4j-api:" + slf4jVersion)
.addToModulePath("org.eclipse.jetty:jetty-slf4j-impl:" + jettyVersion)
+ .addToModulePath("org.eclipse.jetty.compression:jetty-compression-common:" + jettyVersion)
+ .addToModulePath("org.eclipse.jetty.compression:jetty-compression-gzip:" + jettyVersion)
.addToModulePath("org.eclipse.jetty.websocket:jetty-websocket-jetty-common:" + jettyVersion)
.addToModulePath("org.eclipse.jetty.websocket:jetty-websocket-core-client:" + jettyVersion)
.addToModulePath("org.eclipse.jetty.websocket:jetty-websocket-core-common:" + jettyVersion)
From 4fb21165fd4f562c1d4785390e55a7d95dcb4b06 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Sun, 8 Dec 2024 16:01:04 +0100
Subject: [PATCH 09/20] Updated documentation and migration guide.
Signed-off-by: Simone Bordet
---
.../jetty/modules/code/examples/pom.xml | 4 +
.../client/http/HTTPClientDocs.java | 14 +++
.../server/http/HTTPServerDocs.java | 79 +++++++++++++
.../programming-guide/pages/client/http.adoc | 45 +++++++-
.../pages/migration/12.0-to-12.1.adoc | 7 ++
.../programming-guide/pages/server/http.adoc | 105 +++++++++++++++++-
.../src/main/config/modules/compression.mod | 2 +-
.../server/CompressionHandler.java | 11 +-
8 files changed, 259 insertions(+), 8 deletions(-)
diff --git a/documentation/jetty/modules/code/examples/pom.xml b/documentation/jetty/modules/code/examples/pom.xml
index c50f3707ab85..417e3e2f9d2d 100644
--- a/documentation/jetty/modules/code/examples/pom.xml
+++ b/documentation/jetty/modules/code/examples/pom.xml
@@ -70,6 +70,10 @@
org.eclipse.jetty
jetty-util-ajax
+
+ org.eclipse.jetty.compression
+ jetty-compression-server
+
org.eclipse.jetty.ee11
jetty-ee11-servlet
diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
index adaeb47a05ea..29167f4c8b95 100644
--- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
+++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
@@ -426,6 +426,20 @@ public void outputStreamRequestContent() throws Exception
// end::outputStreamRequestContent[]
}
+ public void removeDecoders() throws Exception
+ {
+ // tag::removeDecoders[]
+ HttpClient httpClient = new HttpClient();
+
+ // Starting HttpClient will discover response content decoders
+ // implementations from the module-path or class-path via ServiceLoader.
+ httpClient.start();
+
+ // Remove all response content decoders.
+ httpClient.getContentDecoderFactories().clear();
+ // end::removeDecoders[]
+ }
+
public void futureResponseListener() throws Exception
{
HttpClient httpClient = new HttpClient();
diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
index c9c8227e944c..83ff17ced16f 100644
--- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
+++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
@@ -33,6 +33,8 @@
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.compression.server.CompressionConfig;
+import org.eclipse.jetty.compression.server.CompressionHandler;
import org.eclipse.jetty.ee11.servlet.DefaultServlet;
import org.eclipse.jetty.ee11.servlet.ResourceServlet;
import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
@@ -1400,6 +1402,83 @@ public boolean handle(Request request, Response response, Callback callback)
// end::contextGzipHandler[]
}
+ public void serverCompressionHandler() throws Exception
+ {
+ // tag::serverCompressionHandler[]
+ Server server = new Server();
+ Connector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ // Create and configure CompressionHandler.
+ CompressionHandler compressionHandler = new CompressionHandler();
+ server.setHandler(compressionHandler);
+
+ CompressionConfig compressionConfig = CompressionConfig.builder()
+ // Do not compress these URI paths.
+ .compressExcludePath("/uncompressed")
+ // Also compress POST responses.
+ .compressIncludeMethod("POST")
+ // Do not compress these mime types.
+ .compressExcludeMimeType("font/ttf")
+ .build();
+ compressionHandler.putConfiguration("/*", compressionConfig);
+
+ // Create a ContextHandlerCollection to manage contexts.
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ compressionHandler.setHandler(contexts);
+
+ server.start();
+ // end::serverCompressionHandler[]
+ }
+
+ public void contextCompressionHandler() throws Exception
+ {
+ class ShopHandler extends Handler.Abstract
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ // Implement the shop, remembering to complete the callback.
+ return true;
+ }
+ }
+
+ class RESTHandler extends Handler.Abstract
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ // Implement the REST APIs, remembering to complete the callback.
+ return true;
+ }
+ }
+
+ // tag::contextCompressionHandler[]
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ // Create a ContextHandlerCollection to hold contexts.
+ ContextHandlerCollection contextCollection = new ContextHandlerCollection();
+ // Link the ContextHandlerCollection to the Server.
+ server.setHandler(contextCollection);
+
+ // Create the context for the shop web application wrapped with CompressionHandler so only the shop will do compression.
+ CompressionHandler shopCompressionHandler = new CompressionHandler(new ContextHandler(new ShopHandler(), "/shop"));
+
+ // Add it to ContextHandlerCollection.
+ contextCollection.addHandler(shopCompressionHandler);
+
+ // Create the context for the API web application.
+ ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");
+
+ // Add it to ContextHandlerCollection.
+ contextCollection.addHandler(apiContext);
+
+ server.start();
+ // end::contextCompressionHandler[]
+ }
+
public void rewriteHandler() throws Exception
{
// tag::rewriteHandler[]
diff --git a/documentation/jetty/modules/programming-guide/pages/client/http.adoc b/documentation/jetty/modules/programming-guide/pages/client/http.adoc
index 29f889221611..b02887133cea 100644
--- a/documentation/jetty/modules/programming-guide/pages/client/http.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/client/http.adoc
@@ -45,6 +45,7 @@ Out of the box features that you get with the Jetty HTTP client include:
* Cookies support -- cookies sent by servers are stored and sent back to servers in matching requests.
* Authentication support -- HTTP "Basic", "Digest" and "SPNEGO" authentications are supported, others are pluggable.
* Forward proxy support -- HTTP proxying, SOCKS4 and SOCKS5 proxying.
+* Response content decoding -- response content compressed by the server is transparently decompressed.
[[start]]
== Starting HttpClient
@@ -114,7 +115,8 @@ A `HttpClient` instance can be thought as a browser instance, and it manages the
* A `CookieStore` (see <>).
* A `AuthenticationStore` (see <>).
* A `ProxyConfiguration` (see <>).
-* A set of ``Destination``s
+* A set of ``ContentDecoder.Factory``s (see <>).
+* A set of ``Destination``s, where each `Destination` manages a <>.
A `Destination` is the client-side component that represents an _origin_ server, and manages a queue of requests for that origin, and a <> to that origin.
@@ -441,6 +443,47 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ht
[[content-response]]
=== Response Content Handling
+Response content may be _encoded_ by the server, and this is signaled by the presence of the `Content-Encoding` response header.
+
+Typically, encoding the response content means that it is compressed by the server, to save bytes transmitted over the network.
+The most common encodings are:
+
+* `gzip`, using the link:https://en.wikipedia.org/wiki/Gzip[gzip] algorithm.
+* `br`, using the link:https://github.com/google/brotli[brotli] algorithm.
+* `zstd`, using the link:https://github.com/facebook/zstd[zstdandard] algorithm.
+
+Jetty's `HttpClient` supports by default the `gzip` encoding, but the support is pluggable and discovered via Java's `ServiceLoader` mechanism.
+
+In order to add support for brotli and zstandard, it is enough to add to your classpath the correspondent Jetty artifacts with the implementation, respectively:
+
+* `jetty-compression-brotli`
+* `jetty-compression-zstandard`
+
+The Maven artifact coordinates are the following:
+
+[,xml,subs=attributes+]
+----
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ {jetty-version}
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
+ {jetty-version}
+
+
+----
+
+If you want to remove support for automatic response content decoding (for example in proxies), use the following code:
+
+[,java,indent=0]
+----
+include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=removeDecoders]
+----
+
Jetty's `HttpClient` allows applications to handle response content in different ways.
You can buffer the response content in memory; this is done when using the <> and the content is buffered within a `ContentResponse` up to 2 MiB.
diff --git a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
index 701a4eedb4c8..815ac26f18a4 100644
--- a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
@@ -16,6 +16,13 @@
[[api-changes]]
== APIs Changes
+=== `HttpClient`
+
+In Jetty 12.0.x, applications could configure response content decoding through `HttpClient.getContentDecoderFactories()`, and implement their own by implementing `org.eclipse.jetty.client.ContentDecoder`.
+
+In Jetty 12.1.x, the response content decoding is now based on the newly introduced `org.eclipse.jetty.compression.Compression` classes.
+Application can still implement their own response content decoding by implementing a `Compression` subclass and the corresponding `DecoderSource`, now based on the xref:arch/io.adoc#content-source[`Content.Source`] and xref:arch/io.adoc#content-source-chunk[`Content.Chunk`] APIs.
+
=== `IteratingCallback`
Class `IteratingCallback` underwent refinements that changed the behavior of the `onCompleteFailure(Throwable)` method.
diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc
index 84ee54f0a833..45119ca00395 100644
--- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc
@@ -852,10 +852,111 @@ For example, you can specify to match only the specific HTTP request method `DEL
Notable subclasses of `ConditionalHandler` are, for example, <> and <>.
+[[handler-use-compression]]
+==== CompressionHandler
+
+`CompressionHandler` provides support for automatic decompression of compressed request content and automatic compression of response content.
+
+The Maven artifact coordinates are:
+
+[,xml,subs=attributes+]
+----
+
+ org.eclipse.jetty.compression
+ jetty-compression-server
+ {jetty-version}
+
+----
+
+The compression algorithms supported are:
+
+* link:https://github.com/google/brotli[brotli]; identified by `br` in `Accept-Encoding` and `Content-Encoding` HTTP headers.
+* link:https://en.wikipedia.org/wiki/Gzip[gzip]; identified by `gzip` in `Accept-Encoding` and `Content-Encoding` HTTP headers.
+* link:https://github.com/facebook/zstd[zstdandard]; identified by `zstd` in `Accept-Encoding` and `Content-Encoding` HTTP headers.
+
+`CompressionHandler` discovers compression algorithm implementations via Java's `ServiceLoader` mechanism, so it is enough to put the Jetty artifacts with the compression implementation in the module-path or class-path, respectively:
+
+* `jetty-compression-brotli`
+* `jetty-compression-gzip`
+* `jetty-compression-zstandard`
+
+The Maven artifact coordinates are the following:
+
+[,xml,subs=attributes+]
+----
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ {jetty-version}
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-gzip
+ {jetty-version}
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
+ {jetty-version}
+
+
+----
+
+`CompressionHandler` is a `Handler.Wrapper` that inspects the request and, if the conditions are met, it wraps the request and the response to eventually perform decompression of the request content or compression of the response content.
+The decompression/compression is not performed until the web application reads request content or writes response content.
+
+`CompressionHandler` comes with a default configuration, but can be explicitly configured through a `CompressionConfig` object that can be constructed via the builder pattern.
+
+Furthermore, `CompressionHandler` can be configured either at the server level, or at the context level.
+
+`CompressionHandler` can be configured at the server level in this way:
+
+[,java,indent=0]
+----
+include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=serverCompressionHandler]
+----
+
+The `Handler` tree structure looks like the following:
+
+[,screen]
+----
+Server
+└── CompressionHandler
+ └── ContextHandlerCollection
+ ├── ContextHandler 1
+ :── ...
+ └── ContextHandler N
+----
+
+However, in less common cases, you can configure `CompressionHandler` on a per-context basis, for example because you want to configure `CompressionHandler` with different parameters for each context, or because you want only some contexts to have compression support:
+
+[,java,indent=0]
+----
+include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextCompressionHandler]
+----
+
+The `Handler` tree structure looks like the following:
+
+[,screen]
+----
+Server
+└── ContextHandlerCollection
+ └── ContextHandlerCollection
+ ├── CompressionHandler
+ │ └── ContextHandler /shop
+ │ └── ShopHandler
+ └── ContextHandler /api
+ └── RESTHandler
+----
+
[[handler-use-gzip]]
==== GzipHandler
-`GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content.
+`GzipHandler` provides supports for automatic decompression of gzip compressed request content and automatic gzip compression of response content.
+
+`GzipHandler` is now obsoleted by <>, but still maintained for backwards compatibility.
+`CompressionHandler` currently supports `gzip` but also other compression algorithms such as `brotli` and `zstandard`.
`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
The decompression/compression is not performed until the web application reads request content or writes response content.
@@ -900,8 +1001,6 @@ Server
└── RESTHandler
----
-// TODO: does ServletContextHandler really need a special configuration?
-
[[handler-use-rewrite]]
==== RewriteHandler
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
index e22d32d40cbd..33c7d73fed17 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression.mod
@@ -4,7 +4,7 @@
Installs CompressionHandler at the root of the Handler tree,
to support decompression of requests and compression of responses.
Compression specific modules must be enabled to support specific
-compressoin algorithms, see module "compression-gzip",
+compression algorithms, see module "compression-gzip",
"compression-brotli" and "compression-zstandard".
[tags]
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
index 444c191db11e..03ef5c4268a6 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
@@ -73,7 +73,13 @@ public class CompressionHandler extends Handler.Wrapper
public CompressionHandler()
{
- addBean(pathConfigs);
+ installBean(pathConfigs);
+ }
+
+ public CompressionHandler(Handler handler)
+ {
+ super(handler);
+ installBean(pathConfigs);
}
/**
@@ -189,10 +195,9 @@ public CompressionConfig putConfiguration(String pathSpecString, CompressionConf
@Override
protected void doStart() throws Exception
{
- // If the supported encodings is empty, that means this handler wasn't manually configured with encodings.
- // Fallback to discovered encodings via the service loader instead.
if (supportedEncodings.isEmpty())
{
+ // No explicit compression configured, discover them via ServiceLoader.
TypeUtil.serviceStream(ServiceLoader.load(Compression.class)).forEach(this::putCompression);
}
From 8e2b8ac6d19999eaa37145ec8b2e5de06a7ddefa Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Mon, 9 Dec 2024 21:34:32 +0100
Subject: [PATCH 10/20] Updates from reviews. Implemented usage of
minCompressSize.
Signed-off-by: Simone Bordet
---
.../compression/brotli/BrotliCompression.java | 11 +-
.../jetty/compression/Compression.java | 11 +-
.../compression/gzip/GzipCompression.java | 11 +-
.../config/etc/jetty-compression-brotli.xml | 2 -
.../config/etc/jetty-compression-gzip.xml | 2 -
.../etc/jetty-compression-zstandard.xml | 2 -
.../compression/server/CompressionConfig.java | 142 +++++++-----------
.../server/CompressionHandler.java | 32 +---
.../server/internal/CompressionResponse.java | 16 +-
.../zstandard/ZstandardCompression.java | 11 +-
.../eclipse/jetty/util/IncludeExclude.java | 9 +-
.../eclipse/jetty/util/IncludeExcludeSet.java | 10 ++
12 files changed, 112 insertions(+), 147 deletions(-)
diff --git a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
index 4813e62a9765..98fa1c0f9cdf 100644
--- a/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-brotli/src/main/java/org/eclipse/jetty/compression/brotli/BrotliCompression.java
@@ -57,11 +57,10 @@ public class BrotliCompression extends Compression
private BrotliEncoderConfig defaultEncoderConfig = new BrotliEncoderConfig();
private BrotliDecoderConfig defaultDecoderConfig = new BrotliDecoderConfig();
- private int minCompressSize = DEFAULT_MIN_BROTLI_SIZE;
-
public BrotliCompression()
{
super(ENCODING_NAME);
+ setMinCompressSize(DEFAULT_MIN_BROTLI_SIZE);
}
@Override
@@ -121,16 +120,10 @@ public List getFileExtensionNames()
return EXTENSIONS;
}
- @Override
- public int getMinCompressSize()
- {
- return minCompressSize;
- }
-
@Override
public void setMinCompressSize(int minCompressSize)
{
- this.minCompressSize = Math.max(minCompressSize, DEFAULT_MIN_BROTLI_SIZE);
+ super.setMinCompressSize(Math.max(minCompressSize, DEFAULT_MIN_BROTLI_SIZE));
}
@Override
diff --git a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/Compression.java b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/Compression.java
index 98d8f02b175c..efe7ddf1535d 100644
--- a/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/Compression.java
+++ b/jetty-core/jetty-compression/jetty-compression-common/src/main/java/org/eclipse/jetty/compression/Compression.java
@@ -35,6 +35,7 @@ public abstract class Compression extends ContainerLifeCycle
private ByteBufferPool byteBufferPool;
private Container container;
private int bufferSize = 2048;
+ private int minCompressSize;
public Compression(String encoding)
{
@@ -188,9 +189,15 @@ public String getEtagSuffix()
*/
public abstract List getFileExtensionNames();
- public abstract int getMinCompressSize();
+ public int getMinCompressSize()
+ {
+ return minCompressSize;
+ }
- public abstract void setMinCompressSize(int minCompressSize);
+ public void setMinCompressSize(int minCompressSize)
+ {
+ this.minCompressSize = minCompressSize;
+ }
/**
* @return the name of the compression implementation.
diff --git a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
index 247d1d41510b..a064b612b13d 100644
--- a/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-gzip/src/main/java/org/eclipse/jetty/compression/gzip/GzipCompression.java
@@ -44,8 +44,6 @@ public class GzipCompression extends Compression
private static final HttpField X_CONTENT_ENCODING = new PreEncodedHttpField("X-Content-Encoding", ENCODING_NAME);
private static final HttpField CONTENT_ENCODING = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, ENCODING_NAME);
- // TODO: this field is never actually used.
- private int minCompressSize = DEFAULT_MIN_GZIP_SIZE;
private DeflaterPool deflaterPool;
private InflaterPool inflaterPool;
private GzipEncoderConfig defaultEncoderConfig = new GzipEncoderConfig();
@@ -54,6 +52,7 @@ public class GzipCompression extends Compression
public GzipCompression()
{
super(ENCODING_NAME);
+ setMinCompressSize(DEFAULT_MIN_GZIP_SIZE);
}
@Override
@@ -130,16 +129,10 @@ public void setInflaterPool(InflaterPool inflaterPool)
this.inflaterPool = inflaterPool;
}
- @Override
- public int getMinCompressSize()
- {
- return minCompressSize;
- }
-
@Override
public void setMinCompressSize(int minCompressSize)
{
- this.minCompressSize = Math.max(minCompressSize, DEFAULT_MIN_GZIP_SIZE);
+ super.setMinCompressSize(Math.max(minCompressSize, DEFAULT_MIN_GZIP_SIZE));
}
@Override
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
index 31c55130fe3d..1bed2f0f396e 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-brotli.xml
@@ -25,5 +25,3 @@
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
index 9075c2ddc8a2..e93a8b19a577 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-gzip.xml
@@ -38,5 +38,3 @@
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
index e07484875d6f..e762b0d01020 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/etc/jetty-compression-zstandard.xml
@@ -27,5 +27,3 @@
-
-
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
index 02367a4543b3..44020d7b6e3b 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
@@ -14,13 +14,13 @@
package org.eclipse.jetty.compression.server;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
@@ -33,17 +33,8 @@
import org.eclipse.jetty.util.component.AbstractLifeCycle;
/**
- * Configuration for a specific compression behavior per matching path from the {@link CompressionHandler}.
- *
- *
- * Configuration is split between compression (of responses) and decompression (of requests).
- *
- *
- *
- * Experimental Configuration, subject to change while the implementation is being settled.
- * Please provide feedback at the Jetty Issue tracker
- * to influence the direction / development of these experimental features.
- *
+ * Configuration for a specific compression behavior per matching path from the {@link CompressionHandler}.
+ * Configuration is split between compression (of responses) and decompression (of requests).
*/
@ManagedObject("Compression Configuration")
public class CompressionConfig extends AbstractLifeCycle
@@ -89,7 +80,7 @@ public class CompressionConfig extends AbstractLifeCycle
private CompressionConfig(Builder builder)
{
- this.compressPreferredEncoderOrder = builder.compressPreferredEncoderOrder;
+ this.compressPreferredEncoderOrder = List.copyOf(builder.compressPreferredEncoderOrder);
this.compressEncodings = builder.compressEncodings.asImmutable();
this.decompressEncodings = builder.decompressEncodings.asImmutable();
this.compressMethods = builder.decompressMethods.asImmutable();
@@ -115,8 +106,7 @@ public static Builder builder()
@ManagedAttribute("Set of HTTP Method Exclusions")
public Set getCompressExcludeMethods()
{
- Set excluded = compressMethods.getExcluded();
- return Collections.unmodifiableSet(excluded);
+ return compressMethods.getExcluded();
}
/**
@@ -128,8 +118,7 @@ public Set getCompressExcludeMethods()
@ManagedAttribute("Set of Mime-Types Excluded from Response compression")
public Set getCompressExcludeMimeTypes()
{
- Set excluded = compressMimeTypes.getExcluded();
- return Collections.unmodifiableSet(excluded);
+ return compressMimeTypes.getExcluded();
}
/**
@@ -141,8 +130,7 @@ public Set getCompressExcludeMimeTypes()
@ManagedAttribute("Set of Response Compression Path Exclusions")
public Set getCompressExcludePaths()
{
- Set excluded = compressPaths.getExcluded();
- return Collections.unmodifiableSet(excluded);
+ return compressPaths.getExcluded();
}
/**
@@ -154,8 +142,7 @@ public Set getCompressExcludePaths()
@ManagedAttribute("Set of HTTP Method Inclusions")
public Set getCompressIncludeMethods()
{
- Set includes = compressMethods.getIncluded();
- return Collections.unmodifiableSet(includes);
+ return compressMethods.getIncluded();
}
/**
@@ -167,8 +154,7 @@ public Set getCompressIncludeMethods()
@ManagedAttribute("Set of Mime-Types Included in Response compression")
public Set getCompressIncludeMimeTypes()
{
- Set includes = compressMimeTypes.getIncluded();
- return Collections.unmodifiableSet(includes);
+ return compressMimeTypes.getIncluded();
}
/**
@@ -180,8 +166,7 @@ public Set getCompressIncludeMimeTypes()
@ManagedAttribute("Set of Response Compression Path Inclusions")
public Set getCompressIncludePaths()
{
- Set includes = compressPaths.getIncluded();
- return Collections.unmodifiableSet(includes);
+ return compressPaths.getIncluded();
}
/**
@@ -199,7 +184,7 @@ public Set getCompressIncludePaths()
@ManagedAttribute("Preferred Compression Encoder Order")
public List getCompressPreferredEncoderOrder()
{
- return Collections.unmodifiableList(compressPreferredEncoderOrder);
+ return compressPreferredEncoderOrder;
}
/**
@@ -222,7 +207,7 @@ public String getCompressionEncoding(List requestAcceptEncoding, Request
if (matchedEncoding == null)
return null;
- if (!compressMethods.test(request.getMethod()))
+ if (!isCompressMethodSupported(request.getMethod()))
return null;
if (!compressPaths.test(pathInContext))
@@ -250,9 +235,7 @@ protected List calcPreferredEncoders(List requestAcceptEncoding)
for (String preferredEncoder: compressPreferredEncoderOrder)
{
if (requestAcceptEncoding.contains(preferredEncoder))
- {
preferredEncoderOrder.add(preferredEncoder);
- }
}
return preferredEncoderOrder;
}
@@ -262,9 +245,7 @@ protected String selectEncoderMatch(List preferredEncoders)
for (String encoding : preferredEncoders)
{
if (compressEncodings.test(encoding))
- {
return encoding;
- }
}
return null;
}
@@ -278,8 +259,7 @@ protected String selectEncoderMatch(List preferredEncoders)
@ManagedAttribute("Set of HTTP Method Exclusions")
public Set getDecompressExcludeMethods()
{
- Set excluded = decompressMethods.getExcluded();
- return Collections.unmodifiableSet(excluded);
+ return decompressMethods.getExcluded();
}
/**
@@ -291,8 +271,7 @@ public Set getDecompressExcludeMethods()
@ManagedAttribute("Set of Request Decompression Path Exclusions")
public Set getDecompressExcludePaths()
{
- Set excluded = decompressPaths.getExcluded();
- return Collections.unmodifiableSet(excluded);
+ return decompressPaths.getExcluded();
}
/**
@@ -304,8 +283,7 @@ public Set getDecompressExcludePaths()
@ManagedAttribute("Set of HTTP Method Inclusions")
public Set getDecompressIncludeMethods()
{
- Set includes = decompressMethods.getIncluded();
- return Collections.unmodifiableSet(includes);
+ return decompressMethods.getIncluded();
}
/**
@@ -317,8 +295,7 @@ public Set getDecompressIncludeMethods()
@ManagedAttribute("Set of Request Decompression Path Inclusions")
public Set getDecompressIncludePaths()
{
- Set includes = decompressPaths.getIncluded();
- return Collections.unmodifiableSet(includes);
+ return decompressPaths.getIncluded();
}
public String getDecompressionEncoding(String requestContentEncoding, Request request, String pathInContext)
@@ -328,11 +305,11 @@ public String getDecompressionEncoding(String requestContentEncoding, Request re
if (decompressEncodings.test(requestContentEncoding))
matchedEncoding = requestContentEncoding;
- if (!decompressMethods.test(request.getMethod()))
+ if (!isDecompressMethodSupported(request.getMethod()))
return null;
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
- if (!decompressMimeTypes.test(contentType))
+ if (!isDecompressMimeTypeSupported(contentType))
return null;
if (!decompressPaths.test(pathInContext))
@@ -436,9 +413,9 @@ public static class Builder
private HttpField vary = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString());
- public CompressionConfig build()
+ private Builder()
{
- return new CompressionConfig(this);
+ // Use the static builder() method instead.
}
/**
@@ -485,7 +462,7 @@ public Builder compressExcludeMimeType(String mimetype)
* A path that does not support response content compression.
*
*
- * See {@link Builder} for details on PathSpec string.
+ * See {@code Builder} for details on PathSpec string.
*
*
* @param pathSpecString the path spec string to exclude. The pathInContext
@@ -543,7 +520,7 @@ public Builder compressIncludeMimeType(String mimetype)
* A path that supports response content compression.
*
*
- * See {@link Builder} for details on PathSpec string.
+ * See {@code Builder} for details on PathSpec string.
*
*
* @param pathSpecString the path spec string to include. The pathInContext
@@ -676,7 +653,7 @@ public Builder decompressExcludeMimeType(String mimetype)
* A path that does not support request content decompression.
*
*
- * See {@link Builder} for details on PathSpec string.
+ * See {@code Builder} for details on PathSpec string.
*
*
* @param pathSpecString the path spec string to exclude. The pathInContext
@@ -734,7 +711,7 @@ public Builder decompressIncludeMimeType(String mimetype)
* A path that supports request content decompression.
*
*
- * See {@link Builder} for details on PathSpec string.
+ * See {@code Builder} for details on PathSpec string.
*
*
* @param pathSpecString the path spec string to include. The pathInContext
@@ -749,14 +726,31 @@ public Builder decompressIncludePath(String pathSpecString)
}
/**
- * Setup MimeType exclusion and path exclusion from the provided {@link MimeTypes} configuration.
+ * Specify the Response {@code Vary} header field to use.
*
- * @param mimeTypes the mime types to iterate.
- * @return this builder.
+ * @param vary the {@code Vary} HTTP field to use. If it is not an instance of {@link PreEncodedHttpField},
+ * then it will be converted to one.
+ * @return this builder
+ */
+ public Builder varyHeader(HttpField vary)
+ {
+ if (vary == null || (vary instanceof PreEncodedHttpField))
+ this.vary = vary;
+ else
+ this.vary = new PreEncodedHttpField(vary.getHeader(), vary.getName(), vary.getValue());
+ return this;
+ }
+
+ /**
+ * Configures this {@code Builder} with the default configuration.
+ * Additional configuration may be specified using the {@code Builder}
+ * methods, possibly overriding the defaults.
+ *
+ * @return this instance
*/
- public Builder from(MimeTypes mimeTypes)
+ public Builder defaults()
{
- for (String type : mimeTypes.getMimeMap().values())
+ for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
{
if ("image/svg+xml".equals(type))
{
@@ -766,8 +760,8 @@ public Builder from(MimeTypes mimeTypes)
decompressExcludePath("*.svgz");
}
else if (type.startsWith("image/") ||
- type.startsWith("audio/") ||
- type.startsWith("video/"))
+ type.startsWith("audio/") ||
+ type.startsWith("video/"))
{
compressExcludeMimeType(type);
decompressExcludeMimeType(type);
@@ -784,7 +778,8 @@ else if (type.startsWith("image/") ||
"application/x-rar-compressed",
"application/vnd.bzip3",
"application/zstd",
- // It is possible to use SSE with CompressionHandler, but only if you use `gzip` encoding with syncFlush to true which will impact performance.
+ // It is possible to use SSE with CompressionHandler, but only if you use
+ // `gzip` encoding with syncFlush to true which will impact performance.
"text/event-stream"
).forEach((type) ->
{
@@ -792,43 +787,20 @@ else if (type.startsWith("image/") ||
decompressExcludeMimeType(type);
});
- return this;
- }
+ decompressIncludeMethod(HttpMethod.POST.asString());
+
+ compressIncludeMethod(HttpMethod.GET.asString());
+ compressIncludeMethod(HttpMethod.POST.asString());
- /**
- * Initialize builder with existing {@link CompressionConfig}
- *
- * @param config existing config to base builder off of
- * @return this builder.
- */
- public Builder from(CompressionConfig config)
- {
- this.compressEncodings.addAll(config.compressEncodings);
- this.decompressEncodings.addAll(config.decompressEncodings);
- this.compressMethods.addAll(config.compressMethods);
- this.decompressMethods.addAll(config.decompressMethods);
- this.compressMimeTypes.addAll(config.compressMimeTypes);
- this.decompressMimeTypes.addAll(config.decompressMimeTypes);
- this.compressPaths.addAll(config.compressPaths);
- this.decompressPaths.addAll(config.decompressPaths);
- this.vary = config.vary;
return this;
}
/**
- * Specify the Response {@code Vary} header field to use.
- *
- * @param vary the {@code Vary} HTTP field to use. If it is not an instance of {@link PreEncodedHttpField},
- * then it will be converted to one.
- * @return this builder
+ * @return a new {@link CompressionConfig} instance configured with this {@code Builder}.
*/
- public Builder varyHeader(HttpField vary)
+ public CompressionConfig build()
{
- if (vary == null || (vary instanceof PreEncodedHttpField))
- this.vary = vary;
- else
- this.vary = new PreEncodedHttpField(vary.getHeader(), vary.getName(), vary.getValue());
- return this;
+ return new CompressionConfig(this);
}
// TODO: compression specific config (eg: compression level, strategy, etc)
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
index 03ef5c4268a6..b6c2be66476f 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
@@ -27,7 +27,6 @@
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
@@ -42,26 +41,12 @@
import org.slf4j.LoggerFactory;
/**
- * CompressionHandler to provide compression of response bodies and decompression of request bodies.
- *
- *
- * Supports any arbitrary content-encoding via {@link org.eclipse.jetty.compression.Compression} implementations
- * such as {@code gzip}, {@code zstd}, and {@code brotli}.
- * By default, there are no {@link Compression} implementations that will be automatically added.
- * It is up to the user to call {@link #putCompression(Compression)} to add which implementations that they want to use.
- *
- *
- *
- * Configuration is handled by associating a {@link CompressionConfig} against a {@link PathSpec}.
- * By default, if no configuration is specified, then a default {@link CompressionConfig} is
- * assigned to the {@code /} {@link PathSpec}.
- *
- *
- *
- * Experimental CompressionHandler, subject to change while the implementation is being settled.
- * Please provide feedback at the Jetty Issue tracker
- * to influence the direction / development of these experimental features.
- *
+ * CompressionHandler to provide compression of response bodies and decompression of request bodies.
+ * Supports any arbitrary {@code Content-Encoding} via {@link org.eclipse.jetty.compression.Compression}
+ * implementations such as {@code gzip}, {@code zstd}, and {@code brotli}, discovered via {@link ServiceLoader}.
+ * Configuration is handled by associating a {@link CompressionConfig} against a {@link PathSpec}.
+ * By default, if no configuration is specified, then a default {@link CompressionConfig} is
+ * assigned to the {@code /} {@link PathSpec}.
*/
public class CompressionHandler extends Handler.Wrapper
{
@@ -204,10 +189,7 @@ protected void doStart() throws Exception
if (pathConfigs.isEmpty())
{
// add default configuration if no paths have been configured.
- pathConfigs.put("/",
- CompressionConfig.builder()
- .from(MimeTypes.DEFAULTS)
- .build());
+ pathConfigs.put("/", CompressionConfig.builder().defaults().build());
}
// ensure that the preferred encoder order is sane for the configuration.
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
index b354e7cc3af0..ec09ab3583a9 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/internal/CompressionResponse.java
@@ -35,16 +35,16 @@ public class CompressionResponse extends Response.Wrapper
{
private static final Logger LOG = LoggerFactory.getLogger(CompressionResponse.class);
- private final CompressionConfig config;
private final Compression compression;
+ private final CompressionConfig config;
private final AtomicReference state = new AtomicReference<>(State.MIGHT_COMPRESS);
private EncoderSink encoderSink;
public CompressionResponse(Request request, Response wrapped, Compression compression, CompressionConfig config)
{
super(request, wrapped);
- this.config = config;
this.compression = compression;
+ this.config = config;
}
@Override
@@ -105,6 +105,18 @@ public void write(boolean last, ByteBuffer content, Callback callback)
return;
}
+ long contentLength = getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
+ if (contentLength < 0 && last)
+ contentLength = BufferUtil.length(content);
+ if (contentLength >= 0 && contentLength < compression.getMinCompressSize())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("no compression, too few content bytes {} {}", contentLength, this);
+ state.compareAndSet(State.MIGHT_COMPRESS, State.NOT_COMPRESSING);
+ super.write(last, content, callback);
+ return;
+ }
+
if (LOG.isDebugEnabled())
LOG.debug("compressing {} {}", compression.getEncodingName(), this);
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
index 852833559d07..762e61af953b 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/src/main/java/org/eclipse/jetty/compression/zstandard/ZstandardCompression.java
@@ -54,13 +54,14 @@ public class ZstandardCompression extends Compression
private static final HttpField CONTENT_ENCODING = new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING, ENCODING_NAME);
private static final int DEFAULT_MIN_ZSTD_SIZE = 48;
private static final List EXTENSIONS = List.of("zst");
- private int minCompressSize = DEFAULT_MIN_ZSTD_SIZE;
+
private ZstandardEncoderConfig defaultEncoderConfig = new ZstandardEncoderConfig();
private ZstandardDecoderConfig defaultDecoderConfig = new ZstandardDecoderConfig();
public ZstandardCompression()
{
super(ENCODING_NAME);
+ setMinCompressSize(DEFAULT_MIN_ZSTD_SIZE);
}
@Override
@@ -127,16 +128,10 @@ public List getFileExtensionNames()
return EXTENSIONS;
}
- @Override
- public int getMinCompressSize()
- {
- return minCompressSize;
- }
-
@Override
public void setMinCompressSize(int minCompressSize)
{
- this.minCompressSize = Math.max(minCompressSize, DEFAULT_MIN_ZSTD_SIZE);
+ super.setMinCompressSize(Math.max(minCompressSize, DEFAULT_MIN_ZSTD_SIZE));
}
@Override
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
index 695285acb36f..8d8c753c5ce1 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
@@ -36,9 +36,16 @@ public > IncludeExclude(Class setClass)
super(setClass);
}
- public > IncludeExclude(Set- includeSet, Predicate
- includePredicate, Set
- excludeSet,
+ public
> IncludeExclude(SET includeSet, Predicate- includePredicate, SET excludeSet,
Predicate
- excludePredicate)
{
super(includeSet, includePredicate, excludeSet, excludePredicate);
}
+
+ @Override
+ public IncludeExclude
- asImmutable()
+ {
+ return new IncludeExclude<>(Set.copyOf(getIncluded()), getIncludePredicate(),
+ Set.copyOf(getExcluded()), getExcludePredicate());
+ }
}
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java
index c636c10a5e3c..c0b3c4a79f9f 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java
@@ -235,11 +235,21 @@ public Set
getIncluded()
return _includes;
}
+ protected Predicate getIncludePredicate()
+ {
+ return _includePredicate;
+ }
+
public Set getExcluded()
{
return _excludes;
}
+ protected Predicate getExcludePredicate()
+ {
+ return _excludePredicate;
+ }
+
public void clear()
{
_includes.clear();
From f2eb965e06f7dd02d39c5d5a9a1b821b01d35852 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Mon, 9 Dec 2024 21:48:25 +0100
Subject: [PATCH 11/20] Fixed typo. Updated test after implementing
minCompressSize.
Signed-off-by: Simone Bordet
---
.../org/eclipse/jetty/compression/server/CompressionConfig.java | 2 +-
.../jetty/ee11/servlets/CompressionContentLengthTest.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
index 44020d7b6e3b..60532d3a67b8 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
@@ -83,7 +83,7 @@ private CompressionConfig(Builder builder)
this.compressPreferredEncoderOrder = List.copyOf(builder.compressPreferredEncoderOrder);
this.compressEncodings = builder.compressEncodings.asImmutable();
this.decompressEncodings = builder.decompressEncodings.asImmutable();
- this.compressMethods = builder.decompressMethods.asImmutable();
+ this.compressMethods = builder.compressMethods.asImmutable();
this.decompressMethods = builder.decompressMethods.asImmutable();
this.compressMimeTypes = builder.compressMimeTypes.asImmutable();
this.decompressMimeTypes = builder.decompressMimeTypes.asImmutable();
diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
index 38f7419434ab..c3ca51b72eee 100644
--- a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
+++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
@@ -136,9 +136,9 @@ public static Stream scenarios()
{
// Not compressible (not large enough)
scenarios.add(Arguments.of(compression, compressionWrapping, servlet, 0, "empty.txt", false));
+ scenarios.add(Arguments.of(compression, compressionWrapping, servlet, 16, "file-tiny.txt", false));
// Compressible.
- scenarios.add(Arguments.of(compression, compressionWrapping, servlet, 16, "file-tiny.txt", true));
scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize / 2, "file-small.txt", true));
scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize, "file-medium.txt", true));
scenarios.add(Arguments.of(compression, compressionWrapping, servlet, defaultSize * 4, "file-large.txt", true));
From 601b9409c1e8c27dd3738417e2b3ab832221ca65 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Mon, 9 Dec 2024 22:00:05 +0100
Subject: [PATCH 12/20] Fixed typos.
Signed-off-by: Simone Bordet
---
.../src/main/config/modules/compression-brotli.mod | 2 +-
.../src/main/config/modules/compression-zstandard.mod | 4 ++--
.../jetty-compression/jetty-compression-zstandard/pom.xml | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
index 0b2661152cc2..a4cb4a2fada3 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
@@ -18,7 +18,7 @@ etc/jetty-compression-brotli.xml
[ini-template]
## Minimum content length after which brotli is enabled
-# jetty.compression.brotli.minCompressSize=32
+# jetty.compression.brotli.minCompressSize=48
## Buffer Size for Decoder
# jetty.compression.brotli.decoder.bufferSize=16384
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
index 648d907db93a..9efd733810b7 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
@@ -17,8 +17,8 @@ compression
etc/jetty-compression-zstandard.xml
[ini-template]
-## Minimum content length after which brotli is enabled
-# jetty.compression.zstandard.minCompressSize=32
+## Minimum content length after which zstandard is enabled
+# jetty.compression.zstandard.minCompressSize=48
## Buffer Size for Decoder
# If unspecified, this default comes from zstd-jni's integration with the zstd libs.
diff --git a/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml b/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
index fe9ace01a959..0b15fad9e1e8 100644
--- a/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
+++ b/jetty-core/jetty-compression/jetty-compression-zstandard/pom.xml
@@ -42,7 +42,7 @@
true
- Brotli Compression
+ Zstandard Compression
${osgi.slf4j.import.packages},com.github.luben.zstd.*;version="${zstd-jni.version}",*
*
osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional
From c55a3d488c67415d9261ef59c891de74de836bc7 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Tue, 10 Dec 2024 00:08:24 +0100
Subject: [PATCH 13/20] Updates from review.
Signed-off-by: Simone Bordet
---
.../pages/migration/12.0-to-12.1.adoc | 4 +-
.../CompressionContentLengthTest.java | 2 +-
.../tests/distribution/DistributionTests.java | 46 +++++++++++--------
3 files changed, 31 insertions(+), 21 deletions(-)
diff --git a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
index 815ac26f18a4..3fbf7da1421a 100644
--- a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
@@ -20,8 +20,8 @@
In Jetty 12.0.x, applications could configure response content decoding through `HttpClient.getContentDecoderFactories()`, and implement their own by implementing `org.eclipse.jetty.client.ContentDecoder`.
-In Jetty 12.1.x, the response content decoding is now based on the newly introduced `org.eclipse.jetty.compression.Compression` classes.
-Application can still implement their own response content decoding by implementing a `Compression` subclass and the corresponding `DecoderSource`, now based on the xref:arch/io.adoc#content-source[`Content.Source`] and xref:arch/io.adoc#content-source-chunk[`Content.Chunk`] APIs.
+In Jetty 12.1.x, applications can configure response content decoding through `HttpClient.getContentDecoderFactories()` as before, but the decoding is based on the `org.eclipse.jetty.compression.Compression` classes.
+Applications can implement their own response content decoding by implementing a `Compression` subclass and the corresponding `DecoderSource`, based on the xref:arch/io.adoc#content-source[`Content.Source`] and xref:arch/io.adoc#content-source-chunk[`Content.Chunk`] APIs.
=== `IteratingCallback`
diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
index c3ca51b72eee..9f168841a79c 100644
--- a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
+++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
@@ -212,7 +212,7 @@ public void executeScenario(Compression compression, CompressionWrapping compres
contentEncoding.set(f.getValue());
return true;
})
- .timeout(555, TimeUnit.SECONDS)
+ .timeout(5, TimeUnit.SECONDS)
.send();
assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200));
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
index 1877e0a4eb69..4382686406eb 100644
--- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
@@ -39,6 +39,7 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -2222,17 +2223,25 @@ public void testLimitHandlers(String env) throws Exception
@ParameterizedTest
@ValueSource(strings = {"brotli", "gzip", "zstandard"})
- public void testCompressionHandler(String encoding) throws Exception
+ public void testCompressionHandler(String compressionName) throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.build();
+ String encoding = switch (compressionName)
+ {
+ case "brotli" -> "br";
+ case "gzip" -> "gzip";
+ case "zstandard" -> "zstd";
+ default -> throw new IllegalArgumentException();
+ };
+
String[] modules = {
"resources",
"http",
- "compression-" + encoding,
+ "compression-" + compressionName,
"ee11-webapp",
"ee11-deploy"
};
@@ -2253,24 +2262,25 @@ public void testCompressionHandler(String encoding) throws Exception
int port = Tester.freePort();
try (JettyHomeTester.Run run2 = distribution.start("--approve-all-licenses", "jetty.http.selectors=1", "jetty.http.port=" + port))
{
- try
- {
- assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
+ assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
- startHttpClient();
- URI serverUri = URI.create("http://localhost:" + port + "/test/");
- ContentResponse response = client.newRequest(serverUri)
- .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, encoding))
- .timeout(15, TimeUnit.SECONDS)
- .send();
+ startHttpClient();
+ URI serverUri = URI.create("http://localhost:" + port + "/test/");
+ AtomicReference contentEncoding = new AtomicReference<>();
+ ContentResponse response = client.newRequest(serverUri)
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, encoding))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncoding.set(f.getValue());
+ return true;
+ })
+ .timeout(15, TimeUnit.SECONDS)
+ .send();
- assertEquals(HttpStatus.OK_200, response.getStatus());
- assertThat(response.getContentAsString(), containsStringIgnoringCase("Hello World"));
- }
- finally
- {
- run2.getLogs().forEach(System.err::println);
- }
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertThat(contentEncoding.get(), is(encoding));
+ assertThat(response.getContentAsString(), containsStringIgnoringCase("Hello World"));
}
}
}
From e3cf38fd25b6d54975ea6f2991531d986e9020a0 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Tue, 10 Dec 2024 10:55:09 +0100
Subject: [PATCH 14/20] Updated test after implementing minCompressSize.
Signed-off-by: Simone Bordet
---
.../org/eclipse/jetty/compression/CompressionHandlerTest.java | 2 +-
.../jetty/ee11/servlets/CompressionContentLengthTest.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
index 09a265705456..f901c2527ea7 100644
--- a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
+++ b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
@@ -246,7 +246,7 @@ public boolean handle(Request request, Response response, Callback callback)
public void testDefaultCompressionConfiguration(Class compressionClass) throws Exception
{
newCompression(compressionClass);
- String message = "Hello Jetty!";
+ String message = "Hello Jetty!\n".repeat(10);
CompressionHandler compressionHandler = new CompressionHandler();
compressionHandler.putCompression(compression);
diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
index 9f168841a79c..c4b888189e28 100644
--- a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
+++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/CompressionContentLengthTest.java
@@ -212,7 +212,7 @@ public void executeScenario(Compression compression, CompressionWrapping compres
contentEncoding.set(f.getValue());
return true;
})
- .timeout(5, TimeUnit.SECONDS)
+ .timeout(15, TimeUnit.SECONDS)
.send();
assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200));
From 72bb0b15f9457f2a1e679cb7cca20fb4138de271 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Thu, 12 Dec 2024 17:23:52 +0100
Subject: [PATCH 15/20] Updates from review.
Signed-off-by: Simone Bordet
---
.../jetty/client/transport/HttpReceiver.java | 14 ++++++++++++--
.../HttpClientContentDecoderFactoriesTest.java | 14 ++++++++++----
.../jetty/io/content/ContentSourceTransformer.java | 2 +-
.../jetty/ee11/proxy/AsyncMiddleManServlet.java | 2 +-
4 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
index e0184631c611..751b6d44b567 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpReceiver.java
@@ -609,6 +609,7 @@ private class DecodedContentSource implements Content.Source
private final Content.Source source;
private final Response response;
private long decodedLength;
+ private boolean last;
private DecodedContentSource(Content.Source source, Response response)
{
@@ -643,8 +644,11 @@ public Content.Chunk read()
decodedLength += chunk.remaining();
- if (chunk.isLast())
+ if (chunk.isLast() && !last)
+ {
+ last = true;
afterDecoding(response, decodedLength);
+ }
return chunk;
}
@@ -672,7 +676,13 @@ public void fail(Throwable failure, boolean last)
@Override
public boolean rewind()
{
- return source.rewind();
+ boolean rewound = source.rewind();
+ if (rewound)
+ {
+ decodedLength = 0;
+ last = false;
+ }
+ return rewound;
}
}
diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
index 361696865e82..02344fd8fecb 100644
--- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
+++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
@@ -19,10 +19,12 @@
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.io.content.ContentSourceTransformer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.junit.jupiter.params.ParameterizedTest;
@@ -63,13 +65,17 @@ protected Content.Chunk transform(Content.Chunk chunk)
if (chunk.isEmpty())
return chunk.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
- ByteBuffer byteBuffer = chunk.getByteBuffer();
- byte b = byteBuffer.get();
+ ByteBuffer byteBufferIn = chunk.getByteBuffer();
+ byte b = byteBufferIn.get();
if (b == '*')
return Content.Chunk.EMPTY;
- byte lower = StringUtil.asciiToLowerCase(b);
- return Content.Chunk.from(ByteBuffer.wrap(new byte[]{lower}), false);
+ RetainableByteBuffer bufferOut = bufferPool.acquire(1, true);
+ ByteBuffer byteBufferOut = bufferOut.getByteBuffer();
+ int pos = BufferUtil.flipToFill(byteBufferOut);
+ byteBufferOut.put(StringUtil.asciiToLowerCase(b));
+ BufferUtil.flipToFlush(byteBufferOut, pos);
+ return Content.Chunk.asChunk(byteBufferOut, false, bufferOut);
}
};
}
diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
index de1fb6ff4f00..5d64366f94d0 100644
--- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
+++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
@@ -116,7 +116,7 @@ public Content.Chunk read()
// return a non-last transformed chunk to force more read() and transform().
if (transformedLast && !rawLast)
{
- if (transformedChunk == Content.Chunk.EOF)
+ if (transformedChunk.isEmpty())
transformedChunk = Content.Chunk.EMPTY;
else if (!transformedFailure)
transformedChunk = Content.Chunk.asChunk(transformedChunk.getByteBuffer(), false, transformedChunk);
diff --git a/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java b/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
index 9988107888ea..be1dc1c1bc78 100644
--- a/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
+++ b/jetty-ee11/jetty-ee11-proxy/src/main/java/org/eclipse/jetty/ee11/proxy/AsyncMiddleManServlet.java
@@ -857,7 +857,7 @@ private ByteBuffer gzip(List buffers, boolean finished) throws IOExc
private static class GZIPDecoder extends GZIPContentDecoder
{
- public GZIPDecoder(ByteBufferPool bufferPool)
+ private GZIPDecoder(ByteBufferPool bufferPool)
{
super(bufferPool, IO.DEFAULT_BUFFER_SIZE);
}
From cb2c159d1a0df5b9c4bcac83fb6a613e56c54d38 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Fri, 13 Dec 2024 12:22:44 +0100
Subject: [PATCH 16/20] Updates from review.
Signed-off-by: Simone Bordet
---
.../programming-guide/pages/migration/12.0-to-12.1.adoc | 3 ++-
.../jetty/client/HttpClientContentDecoderFactoriesTest.java | 5 ++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
index 3fbf7da1421a..069848e5b17b 100644
--- a/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/migration/12.0-to-12.1.adoc
@@ -20,7 +20,8 @@
In Jetty 12.0.x, applications could configure response content decoding through `HttpClient.getContentDecoderFactories()`, and implement their own by implementing `org.eclipse.jetty.client.ContentDecoder`.
-In Jetty 12.1.x, applications can configure response content decoding through `HttpClient.getContentDecoderFactories()` as before, but the decoding is based on the `org.eclipse.jetty.compression.Compression` classes.
+In Jetty 12.1.x, applications can configure response content decoding through `HttpClient.getContentDecoderFactories()`.
+The decoding is based on the `org.eclipse.jetty.compression.Compression` classes.
Applications can implement their own response content decoding by implementing a `Compression` subclass and the corresponding `DecoderSource`, based on the xref:arch/io.adoc#content-source[`Content.Source`] and xref:arch/io.adoc#content-source-chunk[`Content.Chunk`] APIs.
=== `IteratingCallback`
diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
index 02344fd8fecb..3651029e759b 100644
--- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
+++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContentDecoderFactoriesTest.java
@@ -68,7 +68,10 @@ protected Content.Chunk transform(Content.Chunk chunk)
ByteBuffer byteBufferIn = chunk.getByteBuffer();
byte b = byteBufferIn.get();
if (b == '*')
- return Content.Chunk.EMPTY;
+ {
+ RetainableByteBuffer.Mutable empty = bufferPool.acquire(0, true);
+ return Content.Chunk.asChunk(empty.getByteBuffer(), false, empty);
+ }
RetainableByteBuffer bufferOut = bufferPool.acquire(1, true);
ByteBuffer byteBufferOut = bufferOut.getByteBuffer();
From 1c32a085cad7f16ed9af9351ce4be173b94d3a49 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Fri, 13 Dec 2024 12:51:02 +0100
Subject: [PATCH 17/20] Fixed ContentSourceTransformer.
Signed-off-by: Simone Bordet
---
.../eclipse/jetty/io/content/ContentSourceTransformer.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
index 5d64366f94d0..d3f78e368eb8 100644
--- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
+++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceTransformer.java
@@ -113,12 +113,12 @@ public Content.Chunk read()
boolean transformedFailure = Content.Chunk.isFailure(transformedChunk);
// Transformation may be complete, but rawSource is not read until EOF,
- // return a non-last transformed chunk to force more read() and transform().
- if (transformedLast && !rawLast)
+ // change to non-last transformed chunk to force more read() and transform().
+ if (transformedLast && !transformedFailure && !rawLast)
{
if (transformedChunk.isEmpty())
transformedChunk = Content.Chunk.EMPTY;
- else if (!transformedFailure)
+ else
transformedChunk = Content.Chunk.asChunk(transformedChunk.getByteBuffer(), false, transformedChunk);
}
From fc23160f05894ed90aa47a136946e0e96df62adc Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Sun, 15 Dec 2024 21:18:55 +0100
Subject: [PATCH 18/20] Implemented support for quality values in
Accept-Encoding.
Signed-off-by: Simone Bordet
---
.../compression/server/CompressionConfig.java | 496 ++++++------------
.../server/CompressionHandler.java | 82 +--
.../server/CompressionConfigTest.java | 65 ---
.../compression/CompressionHandlerTest.java | 61 ++-
.../eclipse/jetty/http/QuotedQualityCSV.java | 70 ++-
.../jetty-test-client-transports/pom.xml | 20 +
.../transport/HttpClientCompressionTest.java | 242 +++++++++
.../eclipse/jetty/util/IncludeExclude.java | 5 +-
8 files changed, 535 insertions(+), 506 deletions(-)
delete mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/test/java/org/eclipse/jetty/compression/server/CompressionConfigTest.java
create mode 100644 jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientCompressionTest.java
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
index 60532d3a67b8..d8ca58d62f78 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionConfig.java
@@ -14,15 +14,17 @@
package org.eclipse.jetty.compression.server;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
-import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.AsciiLowerCaseSet;
@@ -74,13 +76,11 @@ public class CompressionConfig extends AbstractLifeCycle
/**
* Optional preferred order of encoders for compressing Response content.
*/
- private final List compressPreferredEncoderOrder;
-
- private final HttpField vary;
+ private final List preferredCompressEncodings;
private CompressionConfig(Builder builder)
{
- this.compressPreferredEncoderOrder = List.copyOf(builder.compressPreferredEncoderOrder);
+ this.preferredCompressEncodings = Collections.unmodifiableList(builder.compressPreferredEncodings);
this.compressEncodings = builder.compressEncodings.asImmutable();
this.decompressEncodings = builder.decompressEncodings.asImmutable();
this.compressMethods = builder.compressMethods.asImmutable();
@@ -89,221 +89,229 @@ private CompressionConfig(Builder builder)
this.decompressMimeTypes = builder.decompressMimeTypes.asImmutable();
this.compressPaths = builder.compressPaths.asImmutable();
this.decompressPaths = builder.decompressPaths.asImmutable();
- this.vary = builder.vary;
}
+ /**
+ * @return a new {@link Builder} to configure a {@code CompressionConfig} instance
+ */
public static Builder builder()
{
return new Builder();
}
/**
- * Get the set of excluded HTTP methods for Response compression.
- *
- * @return the set of excluded HTTP methods
+ * @return the encodings that disable response compression
+ * @see #getCompressIncludeEncodings()
+ */
+ @ManagedAttribute("Encodings that disable response compression")
+ public Set getCompressExcludeEncodings()
+ {
+ return compressEncodings.getExcluded();
+ }
+
+ /**
+ * @return the HTTP methods that disable response compression
* @see #getCompressIncludeMethods()
*/
- @ManagedAttribute("Set of HTTP Method Exclusions")
+ @ManagedAttribute("HTTP methods that disable response compression")
public Set getCompressExcludeMethods()
{
return compressMethods.getExcluded();
}
/**
- * Get the set of excluded MIME types for Response compression.
- *
- * @return the set of excluded MIME types
+ * @return the MIME types that disable response compression
* @see #getCompressIncludeMimeTypes()
*/
- @ManagedAttribute("Set of Mime-Types Excluded from Response compression")
+ @ManagedAttribute("MIME types that disable response compression")
public Set getCompressExcludeMimeTypes()
{
return compressMimeTypes.getExcluded();
}
/**
- * Get the set of excluded Path Specs for response compression.
- *
- * @return the set of excluded Path Specs
+ * @return the path specs that exclude response compression
* @see #getCompressIncludePaths()
*/
- @ManagedAttribute("Set of Response Compression Path Exclusions")
+ @ManagedAttribute("Path specs that exclude response compression")
public Set getCompressExcludePaths()
{
return compressPaths.getExcluded();
}
/**
- * Get the set of included HTTP methods for Response compression
- *
- * @return the set of included HTTP methods
+ * @return the encodings that enable response compression
+ * @see #getCompressExcludeEncodings()
+ */
+ @ManagedAttribute("Encodings that enable response compression")
+ public Set getCompressIncludeEncodings()
+ {
+ return compressEncodings.getIncluded();
+ }
+
+ /**
+ * @return HTTP methods that enable response compression
* @see #getCompressExcludeMethods()
*/
- @ManagedAttribute("Set of HTTP Method Inclusions")
+ @ManagedAttribute("HTTP methods that enable response compression")
public Set getCompressIncludeMethods()
{
return compressMethods.getIncluded();
}
/**
- * Get the set of included MIME types for Response compression.
- *
- * @return the filter list of included MIME types
+ * @return the MIME types that enable response compression
* @see #getCompressExcludeMimeTypes()
*/
- @ManagedAttribute("Set of Mime-Types Included in Response compression")
+ @ManagedAttribute("MIME types that enable response compression")
public Set getCompressIncludeMimeTypes()
{
return compressMimeTypes.getIncluded();
}
/**
- * Get the set of included Path Specs for response compression.
- *
- * @return the set of included Path Specs
+ * @return the path specs that enable response compression
* @see #getCompressExcludePaths()
*/
- @ManagedAttribute("Set of Response Compression Path Inclusions")
+ @ManagedAttribute("Path specs that enable response compression")
public Set getCompressIncludePaths()
{
return compressPaths.getIncluded();
}
/**
- * Get the preferred order of encoders for compressing response content.
- *
- *
- * See {@link Builder#compressPreferredEncoderOrder(List)} for details
- * on how the {@code Accept-Encoding} request header interacts with
- * this configuration.
- *
- *
- * @return the preferred order of encoders.
- * @see Builder#compressPreferredEncoderOrder(List)
+ * @return the encodings for response compression in preferred order
*/
- @ManagedAttribute("Preferred Compression Encoder Order")
- public List getCompressPreferredEncoderOrder()
+ @ManagedAttribute("Encodings for response compression in preferred order")
+ public List getCompressPreferredEncodings()
{
- return compressPreferredEncoderOrder;
+ return preferredCompressEncodings;
}
- /**
- * Return the encoder that best matches the provided details.
- *
- * @param requestAcceptEncoding the HTTP {@code Accept-Encoding} header list (includes only supported encodings,
- * and possibly the {@code *} glob value)
- * @param request the request itself
- * @param pathInContext the path in context
- * @return the selected compression encoding
- */
- public String getCompressionEncoding(List requestAcceptEncoding, Request request, String pathInContext)
+ String getCompressionEncoding(Set supportedEncodings, Request request, List requestAcceptEncoding, String pathInContext)
{
- if (requestAcceptEncoding == null || requestAcceptEncoding.isEmpty())
- return null;
-
- List preferredEncoders = calcPreferredEncoders(requestAcceptEncoding);
- String matchedEncoding = selectEncoderMatch(preferredEncoders);
-
- if (matchedEncoding == null)
+ if (requestAcceptEncoding.isEmpty())
return null;
if (!isCompressMethodSupported(request.getMethod()))
return null;
+ // MIME types checks are performed later in CompressionResponse.
+
if (!compressPaths.test(pathInContext))
return null;
- return matchedEncoding;
- }
-
- protected List calcPreferredEncoders(List requestAcceptEncoding)
- {
- if (compressPreferredEncoderOrder.isEmpty())
+ List matches = new ArrayList<>();
+ QuotedQualityCSV.QualityValue star = null;
+ QuotedQualityCSV.QualityValue identity = null;
+ for (QuotedQualityCSV.QualityValue qualityValue : requestAcceptEncoding)
{
- List result = new ArrayList<>(requestAcceptEncoding);
- result.removeIf((str) -> str.equals("*"));
- return result;
+ String value = qualityValue.getValue();
+ if ("*".equals(value))
+ {
+ star = qualityValue;
+ continue;
+ }
+ if ("identity".equalsIgnoreCase(value))
+ {
+ identity = qualityValue;
+ continue;
+ }
+ if (!qualityValue.isAcceptable())
+ continue;
+ if (!supportedEncodings.contains(value))
+ continue;
+ if (compressEncodings.test(value))
+ matches.add(value);
}
- if (requestAcceptEncoding.contains("*"))
- {
- // anything else in request Accept-Encoding is moot if glob exists.
- return compressPreferredEncoderOrder;
- }
+ List preferred = getCompressPreferredEncodings();
- List preferredEncoderOrder = new ArrayList<>();
- for (String preferredEncoder: compressPreferredEncoderOrder)
+ if (matches.isEmpty())
{
- if (requestAcceptEncoding.contains(preferredEncoder))
- preferredEncoderOrder.add(preferredEncoder);
- }
- return preferredEncoderOrder;
- }
+ // Try a default encoding if possible.
+ if (star != null && star.isAcceptable())
+ {
+ String candidate;
+ if (preferred.isEmpty())
+ candidate = supportedEncodings.stream().findFirst().orElse(null);
+ else
+ candidate = preferred.stream().filter(supportedEncodings::contains).findFirst().orElse(null);
+ if (!compressEncodings.test(candidate))
+ candidate = null;
+ if (candidate != null)
+ return candidate;
+ }
- protected String selectEncoderMatch(List preferredEncoders)
- {
- for (String encoding : preferredEncoders)
- {
- if (compressEncodings.test(encoding))
- return encoding;
+ // The only option left is identity, if acceptable.
+ if (identity != null && !identity.isAcceptable())
+ throw new HttpException.RuntimeException(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415);
+
+ // Identity is acceptable.
+ return null;
}
- return null;
+
+ // Only one match.
+ if (matches.size() == 1)
+ return matches.get(0);
+
+ // Multiple matches, return most preferred, if any.
+ return preferred.stream()
+ .filter(matches::contains)
+ .findFirst()
+ .orElse(matches.get(0));
}
/**
- * Get the set of excluded HTTP methods for Request decompression.
- *
- * @return the set of excluded HTTP methods
+ * @return the HTTP methods that disable request decompression
* @see #getDecompressIncludeMethods()
*/
- @ManagedAttribute("Set of HTTP Method Exclusions")
+ @ManagedAttribute("HTTP methods that disable request decompression")
public Set getDecompressExcludeMethods()
{
return decompressMethods.getExcluded();
}
/**
- * Get the set of excluded Path Specs for request decompression.
- *
- * @return the set of excluded Path Specs
+ * @return the path specs that disable request decompression
* @see #getDecompressIncludePaths()
*/
- @ManagedAttribute("Set of Request Decompression Path Exclusions")
+ @ManagedAttribute("Path specs that disable request decompression")
public Set getDecompressExcludePaths()
{
return decompressPaths.getExcluded();
}
/**
- * Get the set of included HTTP methods for Request decompression
- *
- * @return the set of included HTTP methods
+ * @return the HTTP methods that enable request decompression
* @see #getDecompressExcludeMethods()
*/
- @ManagedAttribute("Set of HTTP Method Inclusions")
+ @ManagedAttribute("HTTP methods that enable request decompression")
public Set getDecompressIncludeMethods()
{
return decompressMethods.getIncluded();
}
/**
- * Get the set of included Path Specs for request decompression.
- *
- * @return the set of included Path Specs
+ * @return the path specs that enable request decompression
* @see #getDecompressExcludePaths()
*/
- @ManagedAttribute("Set of Request Decompression Path Inclusions")
+ @ManagedAttribute("Path specs that enable request decompression")
public Set getDecompressIncludePaths()
{
return decompressPaths.getIncluded();
}
- public String getDecompressionEncoding(String requestContentEncoding, Request request, String pathInContext)
+ String getDecompressionEncoding(Set supportedEncodings, Request request, String requestContentEncoding, String pathInContext)
{
- String matchedEncoding = null;
+ if (requestContentEncoding == null)
+ return null;
- if (decompressEncodings.test(requestContentEncoding))
- matchedEncoding = requestContentEncoding;
+ if (!supportedEncodings.contains(requestContentEncoding))
+ return null;
+
+ if (!decompressEncodings.test(requestContentEncoding))
+ return null;
if (!isDecompressMethodSupported(request.getMethod()))
return null;
@@ -315,15 +323,7 @@ public String getDecompressionEncoding(String requestContentEncoding, Request re
if (!decompressPaths.test(pathInContext))
return null;
- return matchedEncoding;
- }
-
- /**
- * @return The VARY field to use.
- */
- public HttpField getVary()
- {
- return vary;
+ return requestContentEncoding;
}
public boolean isCompressMethodSupported(String method)
@@ -347,16 +347,10 @@ public boolean isDecompressMimeTypeSupported(String mimeType)
}
/**
- * Builder of CompressionConfig immutable instances.
- *
+ * The builder of {@link CompressionConfig} immutable instances.
* Notes about PathSpec strings
- *
- *
- * There are 2 syntaxes supported, Servlet {@code url-pattern} based, and
- * Regex based. This means that the initial characters on the path spec
- * line are very strict, and determine the behavior of the path matching.
- *
- *
+ * There are 2 syntaxes supported, Servlet {@code url-pattern} based,
+ * and regex based.
*
* If the spec starts with {@code '^'} the spec is assumed to be
* a regex based path spec and will match with normal Java regex rules.
@@ -367,51 +361,20 @@ public boolean isDecompressMimeTypeSupported(String mimeType)
* a Servlet url-pattern rules path spec for a suffix based match.
* All other syntaxes are unsupported
*
- *
- *
- * Note: inclusion take precedence over exclude.
- *
+ * For all properties, exclusion takes precedence over inclusion,
+ * as defined by {@link IncludeExcludeSet}.
*/
public static class Builder
{
- /**
- * Set of {@code Content-Encoding} encodings that are supported for decompressing Request content.
- */
private final IncludeExclude decompressEncodings = new IncludeExclude<>();
- /**
- * Set of {@code Accept-Encoding} encodings that are supported for compressing Response content.
- */
private final IncludeExclude compressEncodings = new IncludeExclude<>();
- /**
- * Set of HTTP Methods that are supported for decompressing Request content.
- */
private final IncludeExclude decompressMethods = new IncludeExclude<>();
- /**
- * Set of HTTP Methods that are supported for compressing Response content.
- */
private final IncludeExclude compressMethods = new IncludeExclude<>();
- /**
- * Set of paths that support decompressing of Request content.
- */
private final IncludeExclude decompressPaths = new IncludeExclude<>(PathSpecSet.class);
- /**
- * Set of paths that support compressing Response content.
- */
private final IncludeExclude compressPaths = new IncludeExclude<>(PathSpecSet.class);
- /**
- * Mime-Types that support decompressing of Request content.
- */
private final IncludeExclude compressMimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
- /**
- * Mime-Types that support compressing Response content.
- */
private final IncludeExclude decompressMimeTypes = new IncludeExclude<>(AsciiLowerCaseSet.class);
- /**
- * Optional preferred order of encoders for compressing Response content.
- */
- private final List compressPreferredEncoderOrder = new ArrayList<>();
-
- private HttpField vary = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString());
+ private final List compressPreferredEncodings = new ArrayList<>();
private Builder()
{
@@ -419,9 +382,7 @@ private Builder()
}
/**
- * A {@code Accept-Encoding} encoding to exclude.
- *
- * @param encoding the encoding to exclude
+ * @param encoding the encoding to exclude for response compression.
* @return this builder
*/
public Builder compressExcludeEncoding(String encoding)
@@ -431,9 +392,7 @@ public Builder compressExcludeEncoding(String encoding)
}
/**
- * An HTTP method to exclude for Response compression.
- *
- * @param method the method to exclude
+ * @param method the HTTP method to exclude for response compression
* @return this builder
*/
public Builder compressExcludeMethod(String method)
@@ -443,13 +402,7 @@ public Builder compressExcludeMethod(String method)
}
/**
- * A non-compressible mimetype to exclude for Response compression.
- *
- *
- * The response {@code Content-Type} is evaluated.
- *
- *
- * @param mimetype the mimetype to exclude
+ * @param mimetype the MIME type to exclude for response compression
* @return this builder
*/
public Builder compressExcludeMimeType(String mimetype)
@@ -459,15 +412,11 @@ public Builder compressExcludeMimeType(String mimetype)
}
/**
- * A path that does not support response content compression.
+ * A path spec to exclude for response compression.
+ * The path spec is matched against {@link Request#getPathInContext(Request)}.
*
- *
- * See {@code Builder} for details on PathSpec string.
- *
- *
- * @param pathSpecString the path spec string to exclude. The pathInContext
- * is used to match against this path spec.
- * @return this builder.
+ * @param pathSpecString the path spec to exclude for response compression.
+ * @return this builder
* @see #compressIncludePath(String)
*/
public Builder compressExcludePath(String pathSpecString)
@@ -477,9 +426,7 @@ public Builder compressExcludePath(String pathSpecString)
}
/**
- * A {@code Accept-Encoding} encoding to include.
- *
- * @param encoding the encoding to include
+ * @param encoding the encoding to include for response compression.
* @return this builder
*/
public Builder compressIncludeEncoding(String encoding)
@@ -489,9 +436,7 @@ public Builder compressIncludeEncoding(String encoding)
}
/**
- * An HTTP method to include for Response compression.
- *
- * @param method the method to include
+ * @param method the HTTP method to include for response compression
* @return this builder
*/
public Builder compressIncludeMethod(String method)
@@ -501,13 +446,7 @@ public Builder compressIncludeMethod(String method)
}
/**
- * A compressible mimetype to include for Response compression.
- *
- *
- * The response {@code Content-Type} is evaluated.
- *
- *
- * @param mimetype the mimetype to include
+ * @param mimetype the MIME type to include for response compression
* @return this builder
*/
public Builder compressIncludeMimeType(String mimetype)
@@ -517,15 +456,11 @@ public Builder compressIncludeMimeType(String mimetype)
}
/**
- * A path that supports response content compression.
+ * A path spec to include for response compression.
+ * The path spec is matched against {@link Request#getPathInContext(Request)}.
*
- *
- * See {@code Builder} for details on PathSpec string.
- *
- *
- * @param pathSpecString the path spec string to include. The pathInContext
- * is used to match against this path spec.
- * @return this builder.
+ * @param pathSpecString the path spec to include for response compression.
+ * @return this builder
* @see #compressExcludePath(String)
*/
public Builder compressIncludePath(String pathSpecString)
@@ -535,84 +470,27 @@ public Builder compressIncludePath(String pathSpecString)
}
/**
- * Control the preferred order of encoders when compressing response content.
+ * Specifies a list of encodings for response compression in preferred order.
+ * This list is only used when {@link CompressionHandler} computes more
+ * than one candidate content encoding for response compression.
+ * This happens only when {@code Accept-Encoding} specifies more than
+ * one encoding, and they are all supported by the server, or when
+ * {@code Accept-Encoding} specifies the token {@code *} and the server
+ * supports more than one encoding.
*
- *
- * If set to an empty List this preferred order is not considered
- * when selecting the encoder from the {@code Accept-Encoding} Request header.
- *
- *
- * If set, the union of matching encoders is the end result used to determine
- * what encoder should be used for compressing response content.
- *
- *
- * Of special note, the {@code Accept-Encoding: *} (glob) header value will
- * return the {@code compressPreferredEncoderOrder} if provided here, otherwise
- * the {@code *} (glob) header value will be ignored if this
- * {@code compressPreferredEncoderOrder} is not provided.
- *
- *
- * Encoder order resolution
- *
- *
- *
- *
- *
- * {@code compressPreferredEncoderOrder}
- * {@code Accept-Encoding} header
- * Resulting encoders considered
- *
- *
- *
- *
- * {@code }
- * {@code gzip, br}
- * {@code gzip, br}
- *
- *
- * {@code }
- * {@code br, gzip}
- * {@code br, gzip}
- *
- *
- * {@code br, gzip}
- * {@code gzip, br, zstd}
- * {@code br, gzip}
- *
- *
- * {@code zstd, br}
- * {@code gzip, br}
- * {@code br}
- *
- *
- * {@code zstd, br, gzip}
- * {@code *}
- * {@code zstd, br, gzip}
- *
- *
- * {@code }
- * {@code *}
- * {@code }
- *
- *
- *
- *
- * @param encoders the encoders, in order, to use for compressing response content.
- * Will replace any previously set order.
- * @return this builder.
+ * @param encodings a list of encodings for response compression in preferred order
+ * @return this builder
*/
- public Builder compressPreferredEncoderOrder(List encoders)
+ public Builder compressPreferredEncodings(List encodings)
{
- this.compressPreferredEncoderOrder.clear();
- if (encoders != null)
- this.compressPreferredEncoderOrder.addAll(encoders);
+ this.compressPreferredEncodings.clear();
+ if (encodings != null)
+ this.compressPreferredEncodings.addAll(encodings);
return this;
}
/**
- * A {@code Content-Encoding} encoding to exclude.
- *
- * @param encoding the encoding to exclude
+ * @param encoding the encoding to exclude for request decompression
* @return this builder
*/
public Builder decompressExcludeEncoding(String encoding)
@@ -622,9 +500,7 @@ public Builder decompressExcludeEncoding(String encoding)
}
/**
- * An HTTP method to exclude for Request decompression.
- *
- * @param method the method to exclude
+ * @param method the HTTP method to exclude for request decompression
* @return this builder
*/
public Builder decompressExcludeMethod(String method)
@@ -634,13 +510,7 @@ public Builder decompressExcludeMethod(String method)
}
/**
- * A non-compressed mimetype to exclude for Request decompression.
- *
- *
- * The Request {@code Content-Type} is evaluated.
- *
- *
- * @param mimetype the mimetype to exclude
+ * @param mimetype the MIME type to exclude for request decompression
* @return this builder
*/
public Builder decompressExcludeMimeType(String mimetype)
@@ -650,15 +520,11 @@ public Builder decompressExcludeMimeType(String mimetype)
}
/**
- * A path that does not support request content decompression.
- *
- *
- * See {@code Builder} for details on PathSpec string.
- *
+ * A path spec to exclude for request decompression.
+ * The path spec is matched against {@link Request#getPathInContext(Request)}.
*
- * @param pathSpecString the path spec string to exclude. The pathInContext
- * is used to match against this path spec.
- * @return this builder.
+ * @param pathSpecString the path spec to exclude for request decompression
+ * @return this builder
* @see #decompressIncludePath(String)
*/
public Builder decompressExcludePath(String pathSpecString)
@@ -668,9 +534,7 @@ public Builder decompressExcludePath(String pathSpecString)
}
/**
- * A {@code Content-Encoding} encoding to include.
- *
- * @param encoding the encoding to include
+ * @param encoding the encoding to include for request decompression
* @return this builder
*/
public Builder decompressIncludeEncoding(String encoding)
@@ -680,9 +544,7 @@ public Builder decompressIncludeEncoding(String encoding)
}
/**
- * An HTTP method to include for Request decompression.
- *
- * @param method the method to include
+ * @param method the HTTP method to include for request decompression
* @return this builder
*/
public Builder decompressIncludeMethod(String method)
@@ -692,13 +554,7 @@ public Builder decompressIncludeMethod(String method)
}
/**
- * A compressed mimetype to include for Request decompression.
- *
- *
- * The request {@code Content-Type} is evaluated.
- *
- *
- * @param mimetype the mimetype to include
+ * @param mimetype the MIME type to include for request decompression
* @return this builder
*/
public Builder decompressIncludeMimeType(String mimetype)
@@ -708,15 +564,11 @@ public Builder decompressIncludeMimeType(String mimetype)
}
/**
- * A path that supports request content decompression.
- *
- *
- * See {@code Builder} for details on PathSpec string.
- *
+ * A path spec to include for request decompression.
+ * The path spec is matched against {@link Request#getPathInContext(Request)}.
*
- * @param pathSpecString the path spec string to include. The pathInContext
- * is used to match against this path spec.
- * @return this builder.
+ * @param pathSpecString the path spec to include for request decompression
+ * @return this builder
* @see #decompressExcludePath(String)
*/
public Builder decompressIncludePath(String pathSpecString)
@@ -725,28 +577,12 @@ public Builder decompressIncludePath(String pathSpecString)
return this;
}
- /**
- * Specify the Response {@code Vary} header field to use.
- *
- * @param vary the {@code Vary} HTTP field to use. If it is not an instance of {@link PreEncodedHttpField},
- * then it will be converted to one.
- * @return this builder
- */
- public Builder varyHeader(HttpField vary)
- {
- if (vary == null || (vary instanceof PreEncodedHttpField))
- this.vary = vary;
- else
- this.vary = new PreEncodedHttpField(vary.getHeader(), vary.getName(), vary.getValue());
- return this;
- }
-
/**
* Configures this {@code Builder} with the default configuration.
* Additional configuration may be specified using the {@code Builder}
* methods, possibly overriding the defaults.
*
- * @return this instance
+ * @return this builder
*/
public Builder defaults()
{
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
index b6c2be66476f..51daad0f5f1b 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/java/org/eclipse/jetty/compression/server/CompressionHandler.java
@@ -13,9 +13,7 @@
package org.eclipse.jetty.compression.server;
-import java.util.ArrayList;
import java.util.List;
-import java.util.ListIterator;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.TreeMap;
@@ -27,7 +25,8 @@
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.pathmap.MappedResource;
+import org.eclipse.jetty.http.PreEncodedHttpField;
+import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
@@ -35,7 +34,6 @@
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
-import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,6 +51,7 @@ public class CompressionHandler extends Handler.Wrapper
public static final String HANDLER_ETAGS = CompressionHandler.class.getPackageName() + ".ETag";
private static final Logger LOG = LoggerFactory.getLogger(CompressionHandler.class);
+ private final HttpField varyAcceptEncoding = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ACCEPT_ENCODING.asString());
private final Map supportedEncodings = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private final PathMappings pathConfigs = new PathMappings<>();
@@ -188,30 +187,10 @@ protected void doStart() throws Exception
if (pathConfigs.isEmpty())
{
- // add default configuration if no paths have been configured.
+ // Add default configuration if no paths have been configured.
pathConfigs.put("/", CompressionConfig.builder().defaults().build());
}
- // ensure that the preferred encoder order is sane for the configuration.
- for (MappedResource pathConfig : pathConfigs)
- {
- List preferredEncoders = pathConfig.getResource().getCompressPreferredEncoderOrder();
- if (preferredEncoders.isEmpty())
- continue;
- ListIterator preferredIter = preferredEncoders.listIterator();
- while (preferredIter.hasNext())
- {
- String listedEncoder = preferredIter.next();
- if (!supportedEncodings.containsKey(listedEncoder))
- {
- LOG.warn("Unable to find compression encoder {} from configuration for pathspec {} in registered compression encoders [{}]",
- listedEncoder, pathConfig.getPathSpec(),
- String.join(", ", supportedEncodings.keySet()));
- preferredIter.remove(); // remove bad encoding
- }
- }
- }
-
super.doStart();
}
@@ -246,14 +225,14 @@ public boolean handle(final Request request, final Response response, final Call
// The `Content-Encoding` request header indicating that the request body content compression technique.
String requestContentEncoding = null;
// The `Accept-Encoding` request header indicating the supported list of compression encoding techniques.
- List requestAcceptEncoding = null;
+ List requestAcceptEncoding = List.of();
// Tracks the `If-Match` or `If-None-Match` request headers contains an etag separator.
boolean etagMatches = false;
+ QuotedQualityCSV qualityCSV = null;
HttpFields fields = request.getHeaders();
- for (ListIterator i = fields.listIterator(fields.size()); i.hasPrevious(); )
+ for (HttpField field : fields)
{
- HttpField field = i.previous();
HttpHeader header = field.getHeader();
if (header == null)
continue;
@@ -261,35 +240,29 @@ public boolean handle(final Request request, final Response response, final Call
{
case CONTENT_ENCODING ->
{
+ // We are only interested in the last encoding.
String contentEncoding = field.getValue();
if (supportedEncodings.containsKey(contentEncoding))
requestContentEncoding = contentEncoding;
+ else
+ requestContentEncoding = null;
}
case ACCEPT_ENCODING ->
{
- // Get ordered list of supported encodings
- List values = field.getValueList();
- if (values != null)
- {
- for (String value : values)
- {
- String lvalue = StringUtil.asciiToLowerCase(value);
- // only track encodings that are supported by this handler
- if ("*".equals(value) || supportedEncodings.containsKey(lvalue))
- {
- if (requestAcceptEncoding == null)
- requestAcceptEncoding = new ArrayList<>();
- requestAcceptEncoding.add(lvalue);
- }
- }
- }
+ // Collect all Accept-Encoding headers.
+ if (qualityCSV == null)
+ qualityCSV = new QuotedQualityCSV();
+ qualityCSV.addValue(field.getValue());
}
case IF_MATCH, IF_NONE_MATCH -> etagMatches |= field.getValue().contains(EtagUtils.ETAG_SEPARATOR);
}
}
- String decompressEncoding = config.getDecompressionEncoding(requestContentEncoding, request, pathInContext);
- String compressEncoding = config.getCompressionEncoding(requestAcceptEncoding, request, pathInContext);
+ if (qualityCSV != null)
+ requestAcceptEncoding = qualityCSV.getQualityValues();
+
+ String decompressEncoding = config.getDecompressionEncoding(supportedEncodings.keySet(), request, requestContentEncoding, pathInContext);
+ String compressEncoding = config.getCompressionEncoding(supportedEncodings.keySet(), request, requestAcceptEncoding, pathInContext);
if (LOG.isDebugEnabled())
{
@@ -297,7 +270,6 @@ public boolean handle(final Request request, final Response response, final Call
request, requestContentEncoding, requestAcceptEncoding, decompressEncoding, compressEncoding);
}
- // Can we skip looking at the request and wrapping request or response?
if (decompressEncoding == null && compressEncoding == null)
{
if (LOG.isDebugEnabled())
@@ -309,32 +281,24 @@ public boolean handle(final Request request, final Response response, final Call
Request decompressionRequest = request;
Response compressionResponse = response;
- // We need to wrap the request IFF we are inflating or have seen etags with compression separators
+ // We need to wrap the request IFF we can inflate or have seen etags with compression separators.
if (decompressEncoding != null || etagMatches)
- {
decompressionRequest = newDecompressionRequest(request, decompressEncoding);
- }
- // Wrap the response and callback IFF we can be deflated and will try to deflate
+ // Wrap the response IFF we can deflate.
if (compressEncoding != null)
{
- if (config.getVary() != null)
- {
- // The response may vary based on the presence or lack of Accept-Encoding.
- response.getHeaders().ensureField(config.getVary());
- }
-
+ // The response may vary based on the presence or lack of Accept-Encoding.
+ response.getHeaders().ensureField(varyAcceptEncoding);
compressionResponse = newCompressionResponse(request, response, compressEncoding, config);
}
if (LOG.isDebugEnabled())
LOG.debug("handle {} {} {}", decompressionRequest, compressionResponse, this);
- // Call handle() with the possibly wrapped request, response and callback
if (next.handle(decompressionRequest, compressionResponse, callback))
return true;
- // If the request was not accepted, destroy any compressRequest wrapper
if (request instanceof DecompressionRequest decompressRequest)
decompressRequest.destroy();
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/test/java/org/eclipse/jetty/compression/server/CompressionConfigTest.java b/jetty-core/jetty-compression/jetty-compression-server/src/test/java/org/eclipse/jetty/compression/server/CompressionConfigTest.java
deleted file mode 100644
index 56c296681643..000000000000
--- a/jetty-core/jetty-compression/jetty-compression-server/src/test/java/org/eclipse/jetty/compression/server/CompressionConfigTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under the
-// terms of the Eclipse Public License v. 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
-// which is available at https://www.apache.org/licenses/LICENSE-2.0.
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package org.eclipse.jetty.compression.server;
-
-import java.util.List;
-
-import org.eclipse.jetty.http.QuotedQualityCSV;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.hasSize;
-
-public class CompressionConfigTest
-{
- private static List qcsv(String rawheadervalue)
- {
- QuotedQualityCSV csv = new QuotedQualityCSV();
- csv.addValue(rawheadervalue);
- return csv.getValues();
- }
-
- @ParameterizedTest
- @CsvSource(useHeadersInDisplayName = true, delimiterString = "|", textBlock = """
- PreferredEncoders | AcceptEncodings | ExpectedResult
- | gzip, br | gzip, br
- | br, gzip | br, gzip
- br, gzip | gzip, br | br, gzip
- zstd, br | gzip, br, zstd | zstd, br
- zstd, br, gzip | * | zstd, br, gzip
- | * |
- """)
- public void testCalcPreferredEncoders(String preferredEncoderOrderCsv, String acceptEncodingHeaderValuesCsv, String expectedEncodersCsv)
- {
- List preferredEncoderOrder = qcsv(preferredEncoderOrderCsv);
- List acceptEncodingHeaderValues = qcsv(acceptEncodingHeaderValuesCsv);
- List expectedEncodersResult = qcsv(expectedEncodersCsv);
-
- CompressionConfig config = CompressionConfig.builder()
- .compressPreferredEncoderOrder(preferredEncoderOrder)
- .build();
- List result = config.calcPreferredEncoders(acceptEncodingHeaderValues);
- if (expectedEncodersResult.isEmpty())
- {
- assertThat(result, hasSize(0));
- }
- else
- {
- String[] expected = expectedEncodersResult.toArray(new String[0]);
- assertThat(result, contains(expected));
- }
- }
-}
diff --git a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
index f901c2527ea7..e98af74f7931 100644
--- a/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
+++ b/jetty-core/jetty-compression/jetty-compression-tests/src/test/java/org/eclipse/jetty/compression/CompressionHandlerTest.java
@@ -17,6 +17,7 @@
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.ContentResponse;
@@ -28,7 +29,6 @@
import org.eclipse.jetty.compression.zstandard.ZstandardCompression;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.QuotedQualityCSV;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
@@ -52,6 +52,7 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
public class CompressionHandlerTest extends AbstractCompressionTest
@@ -576,24 +577,31 @@ public boolean handle(Request request, Response response, Callback callback) thr
}
/**
- * Testing how CompressionHandler acts with multiple compression implementation added
- * (brotli, gzip, and zstandard are all added enabled),
- * and the {@link CompressionConfig#getCompressPreferredEncoderOrder()} configuration.
- * Also tests the handling of {@code Accept-Encoding: *} request headers.
+ * Testing how CompressionHandler acts with all compression implementations
+ * and the {@link CompressionConfig#getCompressPreferredEncodings()} configuration,
+ * with different values for {@code Accept-Encoding}, including {@code *}.
*/
@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, delimiterString = "|", textBlock = """
- acceptEncoding | preferredEncoding | expectedContentEncoding
+ acceptEncoding | preferredEncoding | expectedEncoding
+ | |
+ | zstd |
+ | br, gzip |
+ gzip | | gzip
+ zstd, gzip | | zstd
+ br | zstd | br
br | gzip, br | br
br, gzip | gzip, br | gzip
- | br, zstd |
- * | zstd, br, gzip | zstd
- * | |
+ br, zstd | gzip, br | br
+ gzip | zstd, br | gzip
+ * | |
+ * | zstd, gzip | zstd
+ foo, * | |
+ foo, * | br | br
+ identity,*;q=0 | |
+ identity,*;q=0 | br, gzip |
""")
- public void testCompressPreferredEncoders(
- String acceptEncodingHeader,
- String preferredEncodingCsv,
- String expectedContentEncoding) throws Exception
+ public void testPreferredCompressEncodings(String acceptEncodings, String preferredEncodings, String expectedEncoding) throws Exception
{
pool = new ArrayByteBufferPool.Tracking();
GzipCompression gzipCompression = new GzipCompression();
@@ -615,10 +623,9 @@ public void testCompressPreferredEncoders(
compressionHandler.putCompression(brotliCompression);
compressionHandler.putCompression(zstdCompression);
- QuotedQualityCSV qcsv = new QuotedQualityCSV();
- qcsv.addValue(preferredEncodingCsv);
+ preferredEncodings = preferredEncodings == null ? "" : preferredEncodings;
CompressionConfig config = CompressionConfig.builder()
- .compressPreferredEncoderOrder(qcsv.getValues())
+ .compressPreferredEncodings(List.of(StringUtil.csvSplit(preferredEncodings)))
.build();
compressionHandler.putConfiguration("/", config);
@@ -636,27 +643,19 @@ public boolean handle(Request request, Response response, Callback callback)
startServer(compressionHandler);
- URI serverURI = server.getURI();
client.getContentDecoderFactories().clear();
- ContentResponse response = client.newRequest(serverURI.getHost(), serverURI.getPort())
- .method(HttpMethod.GET)
- .headers((headers) ->
- {
- headers.put(HttpHeader.ACCEPT_ENCODING, acceptEncodingHeader);
- })
+ ContentResponse response = client.newRequest(server.getURI())
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, acceptEncodings))
.path(requestedPath)
.send();
- dumpResponse(response);
assertThat(response.getStatus(), is(200));
- if (StringUtil.isNotBlank(expectedContentEncoding))
- {
- assertThat(response.getHeaders().get(HttpHeader.CONTENT_ENCODING), is(expectedContentEncoding));
- }
- else
- {
+ if (StringUtil.isBlank(expectedEncoding))
assertFalse(response.getHeaders().contains(HttpHeader.CONTENT_ENCODING));
- }
+ else if ("".equals(expectedEncoding))
+ assertNotNull(response.getHeaders().get(HttpHeader.CONTENT_ENCODING));
+ else
+ assertThat(response.getHeaders().get(HttpHeader.CONTENT_ENCODING), is(expectedEncoding));
}
private void dumpResponse(org.eclipse.jetty.client.Response response)
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
index 7bd36fb6524e..343c55bf56a9 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
@@ -126,7 +126,7 @@ protected void parsedValueAndParams(StringBuilder buffer)
super.parsedValueAndParams(buffer);
// Collect full value with parameters
- _lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index);
+ _lastQuality = new QualityValue(buffer.toString(), _lastQuality._quality, _lastQuality._index);
_qualities.set(_lastQuality._index, _lastQuality);
}
@@ -139,7 +139,7 @@ protected void parsedValue(StringBuilder buffer)
// This is the just the value, without parameters.
// Assume a quality of ONE
- _lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size());
+ _lastQuality = new QualityValue(buffer.toString(), 1.0D, _qualities.size());
_qualities.add(_lastQuality);
}
@@ -173,7 +173,7 @@ else if (paramValue >= 0 &&
if (q != 1.0D)
{
- _lastQuality = new QualityValue(q, buffer.toString(), _lastQuality._index);
+ _lastQuality = new QualityValue(buffer.toString(), q, _lastQuality._index);
_qualities.set(_lastQuality._index, _lastQuality);
}
}
@@ -206,37 +206,69 @@ protected void sort()
_sorted = true;
}
- private class QualityValue implements Comparable
+ public List getQualityValues()
+ {
+ return _qualities.stream().sorted().toList();
+ }
+
+ /**
+ * A quality value , that is a value with an associated weight parameter, as
+ * defined in RFC 9110 .
+ * A quality value appears in HTTP headers such as {@code Accept}, {@code Accept-Encoding},
+ * etc. for example in this form:
+ * {@code
+ * Accept-Encoding: gzip; q=1, br; q=0.5, zstd; q=0.1
+ * }
+ */
+ public class QualityValue implements Comparable
{
- private final double _quality;
private final String _value;
+ private final double _quality;
private final int _index;
- private QualityValue(double quality, String value, int index)
+ private QualityValue(String value, double quality, int index)
{
- _quality = quality;
_value = value;
+ _quality = quality;
_index = index;
}
+ /**
+ * @return the value
+ */
+ public String getValue()
+ {
+ return _value;
+ }
+
+ /**
+ * @return the weight
+ */
+ public double getWeight()
+ {
+ return _quality;
+ }
+
+ /**
+ * @return whether the weight is greater than zero
+ */
+ public boolean isAcceptable()
+ {
+ return getWeight() > 0.0D;
+ }
+
@Override
public int hashCode()
{
- return Double.hashCode(_quality) ^ Objects.hash(_value, _index);
+ return Objects.hash(_quality, _value, _index);
}
@Override
public boolean equals(Object obj)
{
- if (!(obj instanceof QualityValue))
+ if (!(obj instanceof QualityValue that))
return false;
- QualityValue qv = (QualityValue)obj;
- return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index);
- }
-
- private String getValue()
- {
- return _value;
+ return _quality == that._quality && Objects.equals(_value, that._value) && _index == that._index;
}
@Override
@@ -258,11 +290,11 @@ public int compareTo(QualityValue o)
@Override
public String toString()
{
- return String.format("%s@%x[%s,q=%f,i=%d]",
+ return String.format("%s@%x[%s,q=%.3f,i=%d]",
getClass().getSimpleName(),
hashCode(),
- _value,
- _quality,
+ getValue(),
+ getWeight(),
_index);
}
}
diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/pom.xml b/jetty-core/jetty-tests/jetty-test-client-transports/pom.xml
index 3b0f12d1ad32..df21d9f98183 100644
--- a/jetty-core/jetty-tests/jetty-test-client-transports/pom.xml
+++ b/jetty-core/jetty-tests/jetty-test-client-transports/pom.xml
@@ -47,6 +47,26 @@
jetty-unixdomain-server
test
+
+ org.eclipse.jetty.compression
+ jetty-compression-brotli
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-gzip
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-server
+ test
+
+
+ org.eclipse.jetty.compression
+ jetty-compression-zstandard
+ test
+
org.eclipse.jetty.fcgi
jetty-fcgi-server
diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientCompressionTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientCompressionTest.java
new file mode 100644
index 000000000000..9ee972221bfb
--- /dev/null
+++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientCompressionTest.java
@@ -0,0 +1,242 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.test.client.transport;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
+import org.eclipse.jetty.compression.server.CompressionHandler;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class HttpClientCompressionTest extends AbstractTest
+{
+ private static final String SAMPLE_CONTENT = """
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.
+ Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque
+ habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
+ Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam
+ at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate
+ velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.
+ Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum
+ eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa
+ sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam
+ consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.
+ Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse
+ et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.
+ """;
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testEmptyAcceptEncoding(TransportType transportType) throws Exception
+ {
+ start(transportType, new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ assertEquals("", request.getHeaders().get(HttpHeader.ACCEPT_ENCODING));
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ }));
+
+ AtomicReference contentEncodingRef = new AtomicReference<>();
+ ContentResponse response = client.newRequest(newURI(transportType))
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, ""))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncodingRef.set(f.getValue());
+ return true;
+ })
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertNull(contentEncodingRef.get());
+ assertEquals(SAMPLE_CONTENT, response.getContentAsString());
+ }
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testZeroQualityAcceptEncoding(TransportType transportType) throws Exception
+ {
+ start(transportType, new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ }));
+
+ AtomicReference contentEncodingRef = new AtomicReference<>();
+ ContentResponse response = client.newRequest(newURI(transportType))
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, "gzip;q=0"))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncodingRef.set(f.getValue());
+ return true;
+ })
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertNull(contentEncodingRef.get());
+ assertEquals(SAMPLE_CONTENT, response.getContentAsString());
+ }
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testIdentityAcceptEncoding(TransportType transportType) throws Exception
+ {
+ start(transportType, new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ }));
+
+ AtomicReference contentEncodingRef = new AtomicReference<>();
+ ContentResponse response = client.newRequest(newURI(transportType))
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, "identity"))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncodingRef.set(f.getValue());
+ return true;
+ })
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertNull(contentEncodingRef.get());
+ assertEquals(SAMPLE_CONTENT, response.getContentAsString());
+ }
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testUnacceptableAcceptEncoding(TransportType transportType) throws Exception
+ {
+ CompressionHandler compressionHandler = new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ });
+ // Support only gzip on the server.
+ compressionHandler.putCompression(new GzipCompression());
+ start(transportType, compressionHandler);
+
+ AtomicReference contentEncodingRef = new AtomicReference<>();
+ ContentResponse response = client.newRequest(newURI(transportType))
+ // Do not accept gzip.
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, "br, gzip;q=0"))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncodingRef.set(f.getValue());
+ return true;
+ })
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertNull(contentEncodingRef.get());
+ assertEquals(SAMPLE_CONTENT, response.getContentAsString());
+ }
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testUnsupportedAcceptEncoding(TransportType transportType) throws Exception
+ {
+ CompressionHandler compressionHandler = new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ });
+ // Support only gzip on the server.
+ compressionHandler.putCompression(new GzipCompression());
+ start(transportType, compressionHandler);
+
+ for (String v : List.of("br, identity;q=0", "br, identity;q=0, *;q=0"))
+ {
+ ContentResponse response = client.newRequest(newURI(transportType))
+ // Do not accept identity, so the server won't be able to respond.
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, v))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE_415, response.getStatus(), "Accept-Encoding: " + v);
+ response.getHeaders().contains(HttpHeader.ACCEPT_ENCODING, "gzip");
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("transports")
+ public void testQualityAcceptEncoding(TransportType transportType) throws Exception
+ {
+ start(transportType, new CompressionHandler(new Handler.Abstract()
+ {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback)
+ {
+ Content.Sink.write(response, true, SAMPLE_CONTENT, callback);
+ return true;
+ }
+ }));
+
+ AtomicReference contentEncodingRef = new AtomicReference<>();
+ ContentResponse response = client.newRequest(newURI(transportType))
+ .headers(h -> h.put(HttpHeader.ACCEPT_ENCODING, "gzip;q=0.5, br;q=1.0"))
+ .onResponseHeader((r, f) ->
+ {
+ if (f.getHeader() == HttpHeader.CONTENT_ENCODING)
+ contentEncodingRef.set(f.getValue());
+ return true;
+ })
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertEquals("br", contentEncodingRef.get());
+ assertEquals(SAMPLE_CONTENT, response.getContentAsString());
+ }
+}
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
index 8d8c753c5ce1..7c8cc13d76e7 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExclude.java
@@ -13,6 +13,7 @@
package org.eclipse.jetty.util;
+import java.util.Collections;
import java.util.Set;
import java.util.function.Predicate;
@@ -45,7 +46,7 @@ public > IncludeExclude(SET includeSet, Predicate- in
@Override
public IncludeExclude
- asImmutable()
{
- return new IncludeExclude<>(Set.copyOf(getIncluded()), getIncludePredicate(),
- Set.copyOf(getExcluded()), getExcludePredicate());
+ return new IncludeExclude<>(Collections.unmodifiableSet(getIncluded()), getIncludePredicate(),
+ Collections.unmodifiableSet(getExcluded()), getExcludePredicate());
}
}
From 50067d7c0f408abb6a140705c330032ae94c3ecf Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Sun, 15 Dec 2024 22:01:16 +0100
Subject: [PATCH 19/20] Added module compression-all and correspondent
distribution test.
Signed-off-by: Simone Bordet
---
.../src/main/config/modules/compression-all.mod | 17 +++++++++++++++++
.../tests/distribution/DistributionTests.java | 14 ++++++++++++--
2 files changed, 29 insertions(+), 2 deletions(-)
create mode 100644 jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-all.mod
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-all.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-all.mod
new file mode 100644
index 000000000000..25a66f7efceb
--- /dev/null
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-all.mod
@@ -0,0 +1,17 @@
+# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
+
+[description]
+Enables all available compression algorithms in CompressionHandler.
+
+[tags]
+server
+handler
+compression
+brotli
+gzip
+zstandard
+
+[depend]
+compression-brotli
+compression-gzip
+compression-zstandard
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
index 4382686406eb..47bbdee7f25b 100644
--- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
@@ -2222,7 +2222,7 @@ public void testLimitHandlers(String env) throws Exception
}
@ParameterizedTest
- @ValueSource(strings = {"brotli", "gzip", "zstandard"})
+ @ValueSource(strings = {"brotli", "gzip", "zstandard", "all"})
public void testCompressionHandler(String compressionName) throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
@@ -2235,6 +2235,16 @@ public void testCompressionHandler(String compressionName) throws Exception
case "brotli" -> "br";
case "gzip" -> "gzip";
case "zstandard" -> "zstd";
+ case "all" -> "br;q=0.5, gzip;q=1, zstd;q=0.1";
+ default -> throw new IllegalArgumentException();
+ };
+
+ String expected = switch (compressionName)
+ {
+ case "brotli" -> "br";
+ case "gzip" -> "gzip";
+ case "zstandard" -> "zstd";
+ case "all" -> "gzip";
default -> throw new IllegalArgumentException();
};
@@ -2279,7 +2289,7 @@ public void testCompressionHandler(String compressionName) throws Exception
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
- assertThat(contentEncoding.get(), is(encoding));
+ assertThat(contentEncoding.get(), is(expected));
assertThat(response.getContentAsString(), containsStringIgnoringCase("Hello World"));
}
}
From 9660e127a68c650a319f159c5d0c1bfa89de2a93 Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Mon, 16 Dec 2024 12:37:58 +0100
Subject: [PATCH 20/20] Added module documentation.
Signed-off-by: Simone Bordet
---
.../server/http/HTTPServerDocs.java | 2 +
.../pages/modules/standard.adoc | 51 +++++++++++++++++++
.../config/modules/compression-brotli.mod | 2 +
.../main/config/modules/compression-gzip.mod | 2 +
.../config/modules/compression-zstandard.mod | 2 +
5 files changed, 59 insertions(+)
diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
index 83ff17ced16f..7c1effd79b39 100644
--- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
+++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
@@ -1421,6 +1421,8 @@ public void serverCompressionHandler() throws Exception
// Do not compress these mime types.
.compressExcludeMimeType("font/ttf")
.build();
+ // Map the request URI path spec '/*' with the compression configuration.
+ // You can map different path specs with different compression configurations.
compressionHandler.putConfiguration("/*", compressionConfig);
// Create a ContextHandlerCollection to manage contexts.
diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc
index e500e5be7426..33d133c6cca2 100644
--- a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc
+++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc
@@ -54,6 +54,57 @@ This property allows you to cap the max heap memory retained by the pool.
`jetty.byteBufferPool.maxDirectMemory`::
This property allows you to cap the max direct memory retained by the pool.
+[[compression]]
+== Compression Modules
+
+The compression modules allow you to configure server-wide request decompression and response compression by installing the `org.eclipse.jetty.compression.server.CompressionHandler` at the root of the `Handler` tree (see also xref:programming-guide:server/http.adoc#handler-use-compression[this section] for further details).
+
+The supported algorithms are the following:
+
+* brotli, via the <> module
+* gzip, via the <> module
+* zstandard, via the <> module
+
+You can explicitly enable one or more of the compression modules, or all of them with the <> module.
+
+[[compression-all]]
+=== Module `compression-all`
+
+This module enables request decompression and response compression for all the currently supported algorithms listed in <>.
+
+[[compression-brotli]]
+=== Module `compression-brotli`
+
+This module enables request decompression and response compression with the link:https://github.com/google/brotli[brotli] algorithm.
+
+The module properties are:
+
+----
+include::{jetty-home}/modules/compression-brotli.mod[tags=documentation]
+----
+
+[[compression-gzip]]
+=== Module `compression-gzip`
+
+This module enables request decompression and response compression with the link:https://en.wikipedia.org/wiki/Gzip[gzip] algorithm.
+
+The module properties are:
+
+----
+include::{jetty-home}/modules/compression-gzip.mod[tags=documentation]
+----
+
+[[compression-zstandard]]
+=== Module `compression-zstandard`
+
+This module enables request decompression and response compression with the link:https://github.com/facebook/zstd[zstdandard] algorithm.
+
+The module properties are:
+
+----
+include::{jetty-home}/modules/compression-zstandard.mod[tags=documentation]
+----
+
[[connectionlimit]]
== Module `connectionlimit`
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
index a4cb4a2fada3..428855fbe38c 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-brotli.mod
@@ -17,6 +17,7 @@ compression
etc/jetty-compression-brotli.xml
[ini-template]
+# tag::documentation[]
## Minimum content length after which brotli is enabled
# jetty.compression.brotli.minCompressSize=48
@@ -39,3 +40,4 @@ etc/jetty-compression-brotli.xml
## Brotli log2(LZ window size) for Encoder
# valid values from 10 to 24
# jetty.compression.brotli.encoder.lgWindow=22
+# end::documentation[]
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
index 28c72d8687f5..6a422a7aa62a 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-gzip.mod
@@ -17,6 +17,7 @@ compression
etc/jetty-compression-gzip.xml
[ini-template]
+# tag::documentation[]
## Minimum content length after which gzip is enabled
# jetty.compression.gzip.minCompressSize=32
@@ -56,3 +57,4 @@ etc/jetty-compression-gzip.xml
## syncFlush for Encoder
## true for SYNC_FLUSH, false for NO_FLUSH (default)
# jetty.compression.gzip.encoder.syncFlush=false
+# end::documentation[]
diff --git a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
index 9efd733810b7..ee56f1fc94c4 100644
--- a/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
+++ b/jetty-core/jetty-compression/jetty-compression-server/src/main/config/modules/compression-zstandard.mod
@@ -17,6 +17,7 @@ compression
etc/jetty-compression-zstandard.xml
[ini-template]
+# tag::documentation[]
## Minimum content length after which zstandard is enabled
# jetty.compression.zstandard.minCompressSize=48
@@ -52,3 +53,4 @@ etc/jetty-compression-zstandard.xml
# Enable/Disable compression checksums
# Note: browser zstandard implementations requires this to be false.
# jetty.compression.zstandard.encoder.checksum=false
+# end::documentation[]