mirror of
https://github.com/jetty/jetty.project.git
synced 2025-03-01 11:29:29 +00:00
Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-5320-WebSocketHttpClient2
This commit is contained in:
commit
80a824854b
11
VERSION.txt
11
VERSION.txt
@ -23,6 +23,15 @@ jetty-10.0.0.beta3 - 21 October 2020
|
||||
+ 5475 Update to spifly 1.3.2 and asm 9
|
||||
+ 5480 NPE from WebInfConfiguration.deconfigure during WebAppContext shutdown
|
||||
|
||||
jetty-9.4.34.v20201102 - 02 November 2020
|
||||
+ 5320 Using WebSocketClient with jetty-websocket-httpclient.xml in a Jetty
|
||||
web application causes ClassCastException
|
||||
+ 5488 jetty-dir.css not found when using JPMS
|
||||
+ 5498 ServletHolder lifecycle correctness
|
||||
+ 5521 ResourceCollection NPE in list()
|
||||
+ 5535 Support regex in SslContextFactory include/exclude of protocols
|
||||
+ 5555 NPE for servlet with no mapping
|
||||
|
||||
jetty-9.4.33.v20201020 - 20 October 2020
|
||||
+ 5022 Cleanup ServletHandler, specifically with respect to making filter
|
||||
chains more extensible
|
||||
@ -80,7 +89,7 @@ jetty-10.0.0.beta2 - 02 October 2020
|
||||
be empty string, but is `"/"`
|
||||
+ 5064 NotSerializableException for OpenIdConfiguration
|
||||
+ 5069 HttpClientTimeoutTests can occasionally fail due to unreachable network
|
||||
+ 5079 :authority header for IPv6 address not having square brackets
|
||||
+ 5079 :authority header for IPv6 address not having square brackets
|
||||
+ 5081 Review HouseKeeper locking
|
||||
+ 5083 Convert synchronized usages to AutoLock
|
||||
+ 5096 using JettyWebSocketServlet without having a WebSocketUpgradeFilter
|
||||
|
@ -21,14 +21,14 @@
|
||||
<id>generate-xml-files</id>
|
||||
<phase>process-resources</phase>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/plugin-context.xml">
|
||||
<filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" />
|
||||
</concat>
|
||||
<concat destfile="${project.build.directory}/test-jndi.xml">
|
||||
<filelist dir="src/main/templates/" files="jetty-test-jndi-header.xml,env-definitions.xml" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
|
@ -85,14 +85,14 @@
|
||||
<id>generate-xml-files</id>
|
||||
<phase>process-resources</phase>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/plugin-context.xml">
|
||||
<filelist dir="src/main/templates/" files="plugin-context-header.xml,env-definitions.xml" />
|
||||
</concat>
|
||||
<concat destfile="${project.build.directory}/test-spec.xml">
|
||||
<filelist dir="src/main/templates/" files="annotations-context-header.xml,env-definitions.xml" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
|
@ -88,9 +88,9 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<mkdir dir="${sources-directory}" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -1377,9 +1377,12 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
assertThat(sslFills.get(), Matchers.lessThan(100));
|
||||
// The new HttpInput impl tends to call fill and parse more often than the previous one
|
||||
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
|
||||
// this runs the parser an goes to the OS more often but requires less rescheduling.
|
||||
assertThat(sslFills.get(), Matchers.lessThan(150));
|
||||
assertThat(sslFlushes.get(), Matchers.lessThan(50));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(100));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(150));
|
||||
|
||||
assertNull(request.get(5, TimeUnit.SECONDS));
|
||||
|
||||
@ -1399,9 +1402,12 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
assertThat(sslFills.get(), Matchers.lessThan(100));
|
||||
// The new HttpInput impl tends to call fill and parse more often than the previous one
|
||||
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
|
||||
// this runs the parser an goes to the OS more often but requires less rescheduling.
|
||||
assertThat(sslFills.get(), Matchers.lessThan(150));
|
||||
assertThat(sslFlushes.get(), Matchers.lessThan(50));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(100));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(150));
|
||||
|
||||
closeClient(client);
|
||||
}
|
||||
@ -1596,9 +1602,12 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
assertThat(sslFills.get(), Matchers.lessThan(50));
|
||||
// The new HttpInput impl tends to call fill and parse more often than the previous one
|
||||
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
|
||||
// this runs the parser and goes to the OS more often but requires less rescheduling.
|
||||
assertThat(sslFills.get(), Matchers.lessThan(70));
|
||||
assertThat(sslFlushes.get(), Matchers.lessThan(20));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(50));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(70));
|
||||
|
||||
closeClient(client);
|
||||
}
|
||||
@ -1743,9 +1752,12 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
assertThat(sslFills.get(), Matchers.lessThan(50));
|
||||
// The new HttpInput impl tends to call fill and parse more often than the previous one
|
||||
// b/c HttpChannel.needContent() does a fill and parse before doing a fill interested;
|
||||
// this runs the parser and goes to the OS more often but requires less rescheduling.
|
||||
assertThat(sslFills.get(), Matchers.lessThan(80));
|
||||
assertThat(sslFlushes.get(), Matchers.lessThan(20));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(100));
|
||||
assertThat(httpParses.get(), Matchers.lessThan(120));
|
||||
|
||||
closeClient(client);
|
||||
}
|
||||
|
@ -21,10 +21,10 @@
|
||||
|
||||
There are two main concepts on which the Eclipse Jetty standalone server is based:
|
||||
|
||||
* the xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features
|
||||
* the xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features, you need for your web applications
|
||||
* The xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features
|
||||
* The xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features you need for your web applications
|
||||
|
||||
After installing Jetty, you want to setup a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules].
|
||||
After installing Jetty, you will want to set up a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules].
|
||||
|
||||
[[og-begin-arch-modules]]
|
||||
===== Eclipse Jetty Architecture: Modules
|
||||
@ -33,19 +33,19 @@ The Jetty standalone server is made of components that are assembled together, c
|
||||
|
||||
A Jetty _module_ is made of one or more components that work together to provide typically one feature, although they may provide more than one feature.
|
||||
|
||||
A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files rather than using Java APIs.
|
||||
What you can do in Java code to assemble Jetty components, it can be done using Jetty modules.
|
||||
A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files.
|
||||
What you can do in Java code to assemble Jetty components can be done using Jetty modules.
|
||||
|
||||
A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module, that in turn depends on the `threadpool` and `logging` Jetty modules.
|
||||
A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module which in turn depends on the `threadpool` and `logging` Jetty modules.
|
||||
|
||||
Every feature in a Jetty server is enabled by enabling correspondent Jetty modules.
|
||||
Every feature in a Jetty server is enabled by enabling the corresponding Jetty module(s).
|
||||
|
||||
For example, if you enable only the `http` Jetty module, then your Jetty standalone server will only be able to listen to a network port for clear-text HTTP requests.
|
||||
It will not be able to process secure HTTP (i.e. `https`) requests, it will not be able to process WebSocket, or HTTP/2 or any other protocol because the correspondent modules have not been enabled.
|
||||
|
||||
You can even start a Jetty server _without_ listening on a network port -- for example because you have enabled a custom module you wrote that provides the features you need.
|
||||
|
||||
This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk that client use a module that you did not know was even there.
|
||||
This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk a client using a module that you did not know was even there.
|
||||
|
||||
For more detailed information about the Jetty module system, see xref:og-modules[this section].
|
||||
|
||||
@ -60,7 +60,7 @@ This separation between `$JETTY_HOME` and `$JETTY_BASE` allows upgrades without
|
||||
`$JETTY_HOME` contains the Jetty runtime and libraries and the default configuration, while a `$JETTY_BASE` contains your web applications and any override of the default configuration.
|
||||
|
||||
For example, with the `$JETTY_HOME` installation the default value for the network port for clear-text HTTP is `8080`.
|
||||
However, you want that port to be `6060`, for example because you are behind a load balancer that is configured to forward to the backend on port `6060`.
|
||||
However, you want that port to be `6060`, because you are behind a load balancer that is configured to forward to the backend on port `6060`.
|
||||
|
||||
Instead, you want to configure the clear-text HTTP port in your `$JETTY_BASE`.
|
||||
When you upgrade Jetty, you will upgrade only files in `$JETTY_HOME`, and all the configuration in `$JETTY_BASE` will remain unchanged.
|
||||
|
@ -21,5 +21,5 @@
|
||||
|
||||
The Eclipse Jetty distribution is available for download from link:https://www.eclipse.org/jetty/download.html[]
|
||||
|
||||
The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operative systems.
|
||||
The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operating systems.
|
||||
|
||||
|
@ -30,6 +30,6 @@ The rest of the instructions in this documentation will refer to this location a
|
||||
IMPORTANT: It is important that *only* stable release versions are used in production environments.
|
||||
Versions that have been deprecated or are released as Milestones (M), Alpha, Beta or Release Candidates (RC) are *not* suitable for production as they may contain security flaws or incomplete/non-functioning feature sets.
|
||||
|
||||
If you are new to Jetty, read the xref:og-begin-arch[Jetty architecture short section] to become familiar with the terms used in this document.
|
||||
Otherwise, you can jump to the xref:og-begin-start[start Jetty section].
|
||||
If you are new to Jetty, you should read the xref:og-begin-arch[Jetty architecture section below] to become familiar with the terms used in this documentation.
|
||||
Otherwise, you can jump to the xref:og-begin-start[section on starting Jetty].
|
||||
|
||||
|
@ -18,7 +18,11 @@
|
||||
|
||||
package org.eclipse.jetty.fcgi.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@ -34,8 +38,10 @@ import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpInput;
|
||||
import org.eclipse.jetty.server.HttpTransport;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -43,6 +49,9 @@ public class HttpChannelOverFCGI extends HttpChannel
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class);
|
||||
|
||||
private final Queue<HttpInput.Content> _contentQueue = new LinkedList<>();
|
||||
private final AutoLock _lock = new AutoLock();
|
||||
private HttpInput.Content _specialContent;
|
||||
private final HttpFields.Mutable fields = HttpFields.build();
|
||||
private final Dispatcher dispatcher;
|
||||
private String method;
|
||||
@ -57,6 +66,101 @@ public class HttpChannelOverFCGI extends HttpChannel
|
||||
this.dispatcher = new Dispatcher(connector.getServer().getThreadPool(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContent(HttpInput.Content content)
|
||||
{
|
||||
boolean b = super.onContent(content);
|
||||
|
||||
Throwable failure;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
failure = _specialContent == null ? null : _specialContent.getError();
|
||||
if (failure == null)
|
||||
_contentQueue.offer(content);
|
||||
}
|
||||
if (failure != null)
|
||||
content.failed(failure);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
boolean hasContent = _specialContent != null || !_contentQueue.isEmpty();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("needContent has content? {}", hasContent);
|
||||
return hasContent;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
HttpInput.Content content;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
content = _contentQueue.poll();
|
||||
if (content == null)
|
||||
content = _specialContent;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceContent has produced {}", content);
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failing all content with {}", (Object)failure);
|
||||
List<HttpInput.Content> copy;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
copy = new ArrayList<>(_contentQueue);
|
||||
_contentQueue.clear();
|
||||
}
|
||||
copy.forEach(c -> c.failed(failure));
|
||||
HttpInput.Content lastContent = copy.isEmpty() ? null : copy.get(copy.size() - 1);
|
||||
boolean atEof = lastContent != null && lastContent.isEof();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content, EOF = {}", atEof);
|
||||
return atEof;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed " + x);
|
||||
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
Throwable error = _specialContent == null ? null : _specialContent.getError();
|
||||
|
||||
if (error != null && error != x)
|
||||
error.addSuppressed(x);
|
||||
else
|
||||
_specialContent = new HttpInput.ErrorContent(x);
|
||||
}
|
||||
|
||||
return getRequest().getHttpInput().onContentProducible();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("received EOF");
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
_specialContent = new HttpInput.EofContent();
|
||||
}
|
||||
return getRequest().getHttpInput().onContentProducible();
|
||||
}
|
||||
|
||||
protected void header(HttpField field)
|
||||
{
|
||||
String name = field.getName();
|
||||
@ -127,12 +231,46 @@ public class HttpChannelOverFCGI extends HttpChannel
|
||||
|
||||
public boolean onIdleTimeout(Throwable timeout)
|
||||
{
|
||||
boolean handle = getRequest().getHttpInput().onIdleTimeout(timeout);
|
||||
boolean handle = doOnIdleTimeout(timeout);
|
||||
if (handle)
|
||||
execute(this);
|
||||
return !handle;
|
||||
}
|
||||
|
||||
private boolean doOnIdleTimeout(Throwable x)
|
||||
{
|
||||
boolean neverDispatched = getState().isIdle();
|
||||
boolean waitingForContent;
|
||||
HttpInput.Content specialContent;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
waitingForContent = _contentQueue.isEmpty() || _contentQueue.peek().remaining() == 0;
|
||||
specialContent = _specialContent;
|
||||
}
|
||||
if ((waitingForContent || neverDispatched) && specialContent == null)
|
||||
{
|
||||
x.addSuppressed(new Throwable("HttpInput idle timeout"));
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
_specialContent = new HttpInput.ErrorContent(x);
|
||||
}
|
||||
return getRequest().getHttpInput().onContentProducible();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle()
|
||||
{
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
if (!_contentQueue.isEmpty())
|
||||
throw new AssertionError("unconsumed content: " + _contentQueue);
|
||||
_specialContent = null;
|
||||
}
|
||||
super.recycle();
|
||||
}
|
||||
|
||||
private static class Dispatcher implements Runnable
|
||||
{
|
||||
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
|
||||
|
@ -122,10 +122,10 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/gcloud/\2-\3.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -135,12 +135,12 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/gcloud-datastore.mod">
|
||||
<fileset file="src/main/config-template/modules/gcloud-datastore.mod" />
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -514,9 +514,9 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<chmod dir="${assembly-directory}/bin" perm="755" includes="**/*.sh" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -53,7 +53,7 @@
|
||||
<dependency>
|
||||
<groupId>com.sun.xml.ws</groupId>
|
||||
<artifactId>jaxws-rt</artifactId>
|
||||
<version>2.3.0.2</version>
|
||||
<version>2.3.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@ -22,6 +22,7 @@ import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.WritePendingException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -235,6 +236,21 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||
return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING || state == CloseState.CLOSED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllData(Throwable x)
|
||||
{
|
||||
List<DataEntry> copy;
|
||||
try (AutoLock l = lock.lock())
|
||||
{
|
||||
dataDemand = 0;
|
||||
copy = new ArrayList<>(dataQueue);
|
||||
dataQueue.clear();
|
||||
}
|
||||
copy.forEach(dataEntry -> dataEntry.callback.failed(x));
|
||||
DataEntry lastDataEntry = copy.isEmpty() ? null : copy.get(copy.size() - 1);
|
||||
return lastDataEntry != null && lastDataEntry.frame.isEndStream();
|
||||
}
|
||||
|
||||
public boolean isLocallyClosed()
|
||||
{
|
||||
return closeState.get() == CloseState.LOCALLY_CLOSED;
|
||||
|
@ -216,6 +216,9 @@ public abstract class HTTP2StreamEndPoint implements EndPoint
|
||||
else
|
||||
{
|
||||
entry.succeed();
|
||||
// WebSocket does not have a backpressure API so you must always demand
|
||||
// the next frame after succeeding the previous one.
|
||||
stream.demand(1);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
@ -119,6 +119,14 @@ public interface IStream extends Stream, Attachable, Closeable
|
||||
*/
|
||||
boolean isRemotelyClosed();
|
||||
|
||||
/**
|
||||
* Fail all data queued in the stream and reset
|
||||
* demand to 0.
|
||||
* @param x the exception to fail the data with.
|
||||
* @return true if the end of the stream was reached, false otherwise.
|
||||
*/
|
||||
boolean failAllData(Throwable x);
|
||||
|
||||
/**
|
||||
* @return whether this stream has been reset (locally or remotely) or has been failed
|
||||
* @see #isReset()
|
||||
|
@ -242,7 +242,10 @@ public interface Stream
|
||||
* @param callback the callback to complete when the bytes of the DATA frame have been consumed
|
||||
* @see #onDataDemanded(Stream, DataFrame, Callback)
|
||||
*/
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback);
|
||||
public default void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Callback method invoked when a DATA frame has been demanded.</p>
|
||||
|
@ -0,0 +1,26 @@
|
||||
@startuml
|
||||
|
||||
null:
|
||||
content:
|
||||
DEMANDING:
|
||||
EOF:
|
||||
|
||||
[*] --> null
|
||||
|
||||
null --> DEMANDING : demand()
|
||||
null --> EOF : eof()
|
||||
null -left-> null : onTimeout()
|
||||
|
||||
DEMANDING --> DEMANDING : demand()
|
||||
DEMANDING --> content : onContent()\n onTimeout()
|
||||
DEMANDING --> EOF : eof()
|
||||
|
||||
EOF --> EOF : eof()\n onTimeout()
|
||||
|
||||
note bottom of content: content1 -> content2 is only\nvalid if content1 is special
|
||||
note top of content: content -> null only happens\nwhen content is not special
|
||||
content --> content : onContent()\n onTimeout()
|
||||
content --> null: take()
|
||||
content --> EOF: eof()
|
||||
|
||||
@enduml
|
@ -157,7 +157,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
getConnection().onData((IStream)stream, frame, callback);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.server;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
@ -57,10 +58,12 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
private boolean _expect100Continue;
|
||||
private boolean _delayedUntilContent;
|
||||
private boolean _useOutputDirectByteBuffers;
|
||||
private final ContentDemander _contentDemander;
|
||||
|
||||
public HttpChannelOverHTTP2(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransportOverHTTP2 transport)
|
||||
{
|
||||
super(connector, configuration, endPoint, transport);
|
||||
_contentDemander = new ContentDemander();
|
||||
}
|
||||
|
||||
protected IStream getStream()
|
||||
@ -131,9 +134,18 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
_delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() &&
|
||||
!endStream && !_expect100Continue && !connect;
|
||||
|
||||
// Delay the demand of DATA frames for CONNECT with :protocol.
|
||||
if (!connect || request.getProtocol() == null)
|
||||
getStream().demand(1);
|
||||
// Delay the demand of DATA frames for CONNECT with :protocol
|
||||
// or for normal requests expecting 100 continue.
|
||||
if (connect)
|
||||
{
|
||||
if (request.getProtocol() == null)
|
||||
_contentDemander.demand(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_delayedUntilContent)
|
||||
_contentDemander.demand(false);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
@ -204,6 +216,7 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
{
|
||||
_expect100Continue = false;
|
||||
_delayedUntilContent = false;
|
||||
_contentDemander.recycle();
|
||||
super.recycle();
|
||||
getHttpTransport().recycle();
|
||||
}
|
||||
@ -224,26 +237,16 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
@Override
|
||||
public Runnable onData(DataFrame frame, Callback callback)
|
||||
{
|
||||
return onRequestContent(frame, callback);
|
||||
}
|
||||
|
||||
public Runnable onRequestContent(DataFrame frame, final Callback callback)
|
||||
{
|
||||
Stream stream = getStream();
|
||||
if (stream.isReset())
|
||||
{
|
||||
// Consume previously queued content to
|
||||
// enlarge the session flow control window.
|
||||
consumeInput();
|
||||
// Consume immediately this content.
|
||||
callback.succeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = frame.getData();
|
||||
int length = buffer.remaining();
|
||||
boolean handle = onContent(new HttpInput.Content(buffer)
|
||||
HttpInput.Content content = new HttpInput.Content(buffer)
|
||||
{
|
||||
@Override
|
||||
public boolean isEof()
|
||||
{
|
||||
return frame.isEndStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
@ -261,23 +264,31 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
{
|
||||
return callback.getInvocationType();
|
||||
}
|
||||
});
|
||||
};
|
||||
boolean needed = _contentDemander.onContent(content);
|
||||
boolean handle = onContent(content);
|
||||
|
||||
boolean endStream = frame.isEndStream();
|
||||
if (endStream)
|
||||
{
|
||||
boolean handleContent = onContentComplete();
|
||||
// This will generate EOF -> must happen before onContentProducible.
|
||||
boolean handleRequest = onRequestComplete();
|
||||
handle |= handleContent | handleRequest;
|
||||
}
|
||||
|
||||
boolean woken = needed && getRequest().getHttpInput().onContentProducible();
|
||||
handle |= woken;
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, handle: {}",
|
||||
Stream stream = getStream();
|
||||
LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, woken: {}, needed: {}, handle: {}",
|
||||
stream.getId(),
|
||||
Integer.toHexString(stream.getSession().hashCode()),
|
||||
length,
|
||||
endStream ? "last" : "some",
|
||||
woken,
|
||||
needed,
|
||||
handle);
|
||||
}
|
||||
|
||||
@ -286,6 +297,326 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
return handle || wasDelayed ? this : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demanding content is a marker content that is used to remember that a demand was
|
||||
* registered into the stream. The {@code needed} flag indicates if the demand originated
|
||||
* from a call to {@link #produceContent()} when false or {@link #needContent()}
|
||||
* when true, as {@link HttpInput#onContentProducible()} must only be called
|
||||
* only when {@link #needContent()} was called.
|
||||
* Instances of this class must never escape the scope of this channel impl,
|
||||
* so {@link #produceContent()} must never return one.
|
||||
*/
|
||||
private static final class DemandingContent extends HttpInput.SpecialContent
|
||||
{
|
||||
private final boolean needed;
|
||||
|
||||
private DemandingContent(boolean needed)
|
||||
{
|
||||
this.needed = needed;
|
||||
}
|
||||
}
|
||||
|
||||
private static final HttpInput.Content EOF = new HttpInput.EofContent();
|
||||
private static final HttpInput.Content DEMANDING_NEEDED = new DemandingContent(true);
|
||||
private static final HttpInput.Content DEMANDING_NOT_NEEDED = new DemandingContent(false);
|
||||
|
||||
private class ContentDemander
|
||||
{
|
||||
private final AtomicReference<HttpInput.Content> _content = new AtomicReference<>();
|
||||
|
||||
public void recycle()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("recycle {}", this);
|
||||
HttpInput.Content c = _content.getAndSet(null);
|
||||
if (c != null && !c.isSpecial())
|
||||
throw new AssertionError("unconsumed content: " + c);
|
||||
}
|
||||
|
||||
public HttpInput.Content poll()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("poll, content = {}", c);
|
||||
if (c == null || c.isSpecial() || _content.compareAndSet(c, c.isEof() ? EOF : null))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("returning current content");
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean demand(boolean needed)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("demand({}), content = {}", needed, c);
|
||||
if (c instanceof DemandingContent)
|
||||
{
|
||||
if (needed && !((DemandingContent)c).needed)
|
||||
{
|
||||
if (!_content.compareAndSet(c, DEMANDING_NEEDED))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("already demanding but switched needed flag to true");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("already demanding, returning false");
|
||||
return false;
|
||||
}
|
||||
if (c != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("content available, returning true");
|
||||
return true;
|
||||
}
|
||||
if (_content.compareAndSet(null, needed ? DEMANDING_NEEDED : DEMANDING_NOT_NEEDED))
|
||||
{
|
||||
IStream stream = getStream();
|
||||
if (stream == null)
|
||||
{
|
||||
_content.set(null);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("no content available, switched to demanding but stream is now null");
|
||||
return false;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("no content available, demanding stream {}", stream);
|
||||
stream.demand(1);
|
||||
c = _content.get();
|
||||
boolean hasContent = !(c instanceof DemandingContent) && c != null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("has content now? {}", hasContent);
|
||||
return hasContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onContent(HttpInput.Content content)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("content delivered by stream: {}, current content: {}", content, c);
|
||||
if (c instanceof DemandingContent)
|
||||
{
|
||||
if (_content.compareAndSet(c, content))
|
||||
{
|
||||
boolean needed = ((DemandingContent)c).needed;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing demand content with {} succeeded; returning {}", content, needed);
|
||||
return needed;
|
||||
}
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
if (!content.isSpecial())
|
||||
{
|
||||
// This should never happen, consider as a bug.
|
||||
content.failed(new IllegalStateException("Non special content without demand : " + content));
|
||||
return false;
|
||||
}
|
||||
if (_content.compareAndSet(null, content))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing null content with {} succeeded", content);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (c.isEof() && content.isEof() && content.isEmpty())
|
||||
{
|
||||
content.succeeded();
|
||||
return true;
|
||||
}
|
||||
else if (content.getError() != null)
|
||||
{
|
||||
if (c.getError() != null)
|
||||
{
|
||||
if (c.getError() != content.getError())
|
||||
c.getError().addSuppressed(content.getError());
|
||||
return true;
|
||||
}
|
||||
if (_content.compareAndSet(c, content))
|
||||
{
|
||||
c.failed(content.getError());
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with {} succeeded", content);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.getError() != null && content.remaining() == 0)
|
||||
{
|
||||
content.succeeded();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should never happen, consider as a bug.
|
||||
content.failed(new IllegalStateException("Cannot overwrite exiting content " + c + " with " + content));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onTimeout(Throwable failure)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onTimeout with current content: {} and failure = {}", c, failure);
|
||||
if (!(c instanceof DemandingContent))
|
||||
return false;
|
||||
if (_content.compareAndSet(c, new HttpInput.ErrorContent(failure)))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with error succeeded");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void eof()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("eof with current content: {}", c);
|
||||
if (c instanceof DemandingContent)
|
||||
{
|
||||
if (_content.compareAndSet(c, EOF))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with special EOF succeeded");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (c == null)
|
||||
{
|
||||
if (_content.compareAndSet(null, EOF))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing null content with special EOF succeeded");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (c.isEof())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("current content already is EOF");
|
||||
return;
|
||||
}
|
||||
else if (c.remaining() == 0)
|
||||
{
|
||||
if (_content.compareAndSet(c, EOF))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with special EOF succeeded");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// EOF may arrive with HEADERS frame (e.g. a trailer) that is not flow controlled, so we need to wrap the existing content.
|
||||
// Covered by HttpTrailersTest.testRequestTrailersWithContent.
|
||||
HttpInput.Content content = new HttpInput.WrappingContent(c, true);
|
||||
if (_content.compareAndSet(c, content))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with {} succeeded", content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean failContent(Throwable failure)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = _content.get();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failing current content: {} with failure: {}", c, failure);
|
||||
if (c == null)
|
||||
return false;
|
||||
if (c.isSpecial())
|
||||
return c.isEof();
|
||||
if (_content.compareAndSet(c, null))
|
||||
{
|
||||
c.failed(failure);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("replacing current content with null succeeded");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getClass().getSimpleName() + "@" + hashCode() + " _content=" + _content;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
boolean hasContent = _contentDemander.demand(true);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("needContent has content? {}", hasContent);
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
HttpInput.Content content = null;
|
||||
if (_contentDemander.demand(false))
|
||||
content = _contentDemander.poll();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceContent produced {}", content);
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failing all content with {}", (Object)failure);
|
||||
boolean atEof = getStream().failAllData(failure);
|
||||
atEof |= _contentDemander.failContent(failure);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content, reached EOF? {}", atEof);
|
||||
return atEof;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed " + x);
|
||||
|
||||
_contentDemander.onContent(new HttpInput.ErrorContent(x));
|
||||
|
||||
return getRequest().getHttpInput().onContentProducible();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
_contentDemander.eof();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable onTrailer(HeadersFrame frame)
|
||||
{
|
||||
@ -301,7 +632,10 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
System.lineSeparator(), trailers);
|
||||
}
|
||||
|
||||
// This will generate EOF -> need to call onContentProducible.
|
||||
boolean handle = onRequestComplete();
|
||||
boolean woken = getRequest().getHttpInput().onContentProducible();
|
||||
handle |= woken;
|
||||
|
||||
boolean wasDelayed = _delayedUntilContent;
|
||||
_delayedUntilContent = false;
|
||||
@ -320,25 +654,30 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
|
||||
final boolean delayed = _delayedUntilContent;
|
||||
_delayedUntilContent = false;
|
||||
|
||||
boolean result = isIdle();
|
||||
if (result)
|
||||
boolean reset = isIdle();
|
||||
if (reset)
|
||||
consumeInput();
|
||||
|
||||
getHttpTransport().onStreamTimeout(failure);
|
||||
if (getRequest().getHttpInput().onIdleTimeout(failure) || delayed)
|
||||
|
||||
failure.addSuppressed(new Throwable("HttpInput idle timeout"));
|
||||
_contentDemander.onTimeout(failure);
|
||||
boolean needed = getRequest().getHttpInput().onContentProducible();
|
||||
|
||||
if (needed || delayed)
|
||||
{
|
||||
consumer.accept(this::handleWithContext);
|
||||
result = false;
|
||||
reset = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
return reset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable onFailure(Throwable failure, Callback callback)
|
||||
{
|
||||
getHttpTransport().onStreamFailure(failure);
|
||||
boolean handle = getRequest().getHttpInput().failed(failure);
|
||||
boolean handle = failed(failure);
|
||||
consumeInput();
|
||||
return new FailureTask(failure, callback, handle);
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
||||
<dependency>
|
||||
<groupId>org.infinispan.protostream</groupId>
|
||||
<artifactId>protostream</artifactId>
|
||||
<version>4.2.2.Final</version>
|
||||
<version>${infinispan.protostream.version}</version>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
@ -45,10 +45,10 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -58,12 +58,12 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/infinispan-embedded-query-libs.mod">
|
||||
<fileset file="src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod" />
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -46,10 +46,10 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -59,12 +59,12 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/infinispan-embedded-libs.mod">
|
||||
<fileset file="src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod" />
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -45,11 +45,11 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -59,12 +59,12 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/infinispan-remote-query-libs.mod">
|
||||
<fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod" />
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -46,11 +46,11 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/infinispan/\2-\3-\4.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):.*$" replace="maven://\1/\2/\3|lib/infinispan/\2-\3.jar" byline="true" />
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match="The following files have been resolved:" replace="[files]" />
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@ -60,12 +60,12 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<concat destfile="${project.build.directory}/infinispan-remote-libs.mod">
|
||||
<fileset file="src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod" />
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@ -111,7 +111,7 @@
|
||||
<dependency>
|
||||
<groupId>org.infinispan.protostream</groupId>
|
||||
<artifactId>protostream</artifactId>
|
||||
<version>4.2.2.Final</version>
|
||||
<version>${infinispan.protostream.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -46,14 +46,14 @@
|
||||
<execution>
|
||||
<phase>process-resources</phase>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<!--delete file="target/classes/META-INF/MANIFEST.MF" /-->
|
||||
<copy todir="target/classes/jettyhome">
|
||||
<fileset dir="jettyhome">
|
||||
<exclude name="**/*.log" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
|
@ -43,11 +43,11 @@
|
||||
<execution>
|
||||
<phase>process-resources</phase>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<copy todir="target/classes/contexts">
|
||||
<fileset dir="contexts" />
|
||||
</copy>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<osgi-version>3.6.0.v20100517</osgi-version>
|
||||
<osgi-version>3.7.1</osgi-version>
|
||||
<osgi-services-version>3.2.100.v20100503</osgi-services-version>
|
||||
<equinox-http-servlet-version>1.0.0-v20070606</equinox-http-servlet-version>
|
||||
</properties>
|
||||
|
@ -30,6 +30,7 @@ import org.eclipse.jetty.server.Authentication;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpChannelState;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpInput;
|
||||
import org.eclipse.jetty.server.HttpOutput;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
@ -62,6 +63,36 @@ public class SpnegoAuthenticatorTest
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpOutput newHttpOutput()
|
||||
{
|
||||
@ -97,6 +128,36 @@ public class SpnegoAuthenticatorTest
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpOutput newHttpOutput()
|
||||
{
|
||||
|
@ -0,0 +1,354 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Non-blocking {@link ContentProducer} implementation. Calling {@link #nextContent()} will never block
|
||||
* but will return null when there is no available content.
|
||||
*/
|
||||
class AsyncContentProducer implements ContentProducer
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AsyncContentProducer.class);
|
||||
|
||||
private final HttpChannel _httpChannel;
|
||||
private HttpInput.Interceptor _interceptor;
|
||||
private HttpInput.Content _rawContent;
|
||||
private HttpInput.Content _transformedContent;
|
||||
private boolean _error;
|
||||
private long _firstByteTimeStamp = Long.MIN_VALUE;
|
||||
private long _rawContentArrived;
|
||||
|
||||
AsyncContentProducer(HttpChannel httpChannel)
|
||||
{
|
||||
_httpChannel = httpChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("recycling {}", this);
|
||||
_interceptor = null;
|
||||
_rawContent = null;
|
||||
_transformedContent = null;
|
||||
_error = false;
|
||||
_firstByteTimeStamp = Long.MIN_VALUE;
|
||||
_rawContentArrived = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Interceptor getInterceptor()
|
||||
{
|
||||
return _interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInterceptor(HttpInput.Interceptor interceptor)
|
||||
{
|
||||
this._interceptor = interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available()
|
||||
{
|
||||
HttpInput.Content content = nextTransformedContent();
|
||||
int available = content == null ? 0 : content.remaining();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("available = {}", available);
|
||||
return available;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContent()
|
||||
{
|
||||
boolean hasContent = _rawContent != null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("hasContent = {}", hasContent);
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isError()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isError = {}", _error);
|
||||
return _error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMinDataRate()
|
||||
{
|
||||
long minRequestDataRate = _httpChannel.getHttpConfiguration().getMinRequestDataRate();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("checkMinDataRate [m={},t={}]", minRequestDataRate, _firstByteTimeStamp);
|
||||
if (minRequestDataRate > 0 && _firstByteTimeStamp != Long.MIN_VALUE)
|
||||
{
|
||||
long period = System.nanoTime() - _firstByteTimeStamp;
|
||||
if (period > 0)
|
||||
{
|
||||
long minimumData = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1);
|
||||
if (getRawContentArrived() < minimumData)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("checkMinDataRate check failed");
|
||||
BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,
|
||||
String.format("Request content data rate < %d B/s", minRequestDataRate));
|
||||
if (_httpChannel.getState().isResponseCommitted())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("checkMinDataRate aborting channel");
|
||||
_httpChannel.abort(bad);
|
||||
}
|
||||
failCurrentContent(bad);
|
||||
throw bad;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRawContentArrived()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("getRawContentArrived = {}", _rawContentArrived);
|
||||
return _rawContentArrived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAll(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("consumeAll [e={}]", (Object)x);
|
||||
failCurrentContent(x);
|
||||
// A specific HttpChannel mechanism must be used as the following code
|
||||
// does not guarantee that the channel will synchronously deliver all
|
||||
// content it already contains:
|
||||
// while (true)
|
||||
// {
|
||||
// HttpInput.Content content = _httpChannel.produceContent();
|
||||
// ...
|
||||
// }
|
||||
// as the HttpChannel's produceContent() contract makes no such promise;
|
||||
// for instance the H2 implementation calls Stream.demand() that may
|
||||
// deliver the content asynchronously. Tests in StreamResetTest cover this.
|
||||
boolean atEof = _httpChannel.failAllContent(x);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content of http channel; at EOF? {}", atEof);
|
||||
return atEof;
|
||||
}
|
||||
|
||||
private void failCurrentContent(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failing currently held content [r={},t={}]", _rawContent, _transformedContent, x);
|
||||
if (_transformedContent != null && !_transformedContent.isSpecial())
|
||||
{
|
||||
if (_transformedContent != _rawContent)
|
||||
{
|
||||
_transformedContent.skip(_transformedContent.remaining());
|
||||
_transformedContent.failed(x);
|
||||
}
|
||||
_transformedContent = null;
|
||||
}
|
||||
|
||||
if (_rawContent != null && !_rawContent.isSpecial())
|
||||
{
|
||||
_rawContent.skip(_rawContent.remaining());
|
||||
_rawContent.failed(x);
|
||||
_rawContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContentProducible()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onContentProducible");
|
||||
return _httpChannel.getState().onReadReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content nextContent()
|
||||
{
|
||||
HttpInput.Content content = nextTransformedContent();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nextContent = {}", content);
|
||||
if (content != null)
|
||||
_httpChannel.getState().onReadIdle();
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reclaim(HttpInput.Content content)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("reclaim {} [t={}]", content, _transformedContent);
|
||||
if (_transformedContent == content)
|
||||
{
|
||||
content.succeeded();
|
||||
if (_transformedContent == _rawContent)
|
||||
_rawContent = null;
|
||||
_transformedContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady()
|
||||
{
|
||||
HttpInput.Content content = nextTransformedContent();
|
||||
if (content == null)
|
||||
{
|
||||
_httpChannel.getState().onReadUnready();
|
||||
if (_httpChannel.needContent())
|
||||
{
|
||||
content = nextTransformedContent();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isReady got transformed content after needContent retry {}", content);
|
||||
if (content != null)
|
||||
_httpChannel.getState().onContentAdded();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isReady has no transformed content after needContent");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isReady got transformed content {}", content);
|
||||
_httpChannel.getState().onContentAdded();
|
||||
}
|
||||
boolean ready = content != null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isReady = {}", ready);
|
||||
return ready;
|
||||
}
|
||||
|
||||
private HttpInput.Content nextTransformedContent()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nextTransformedContent [r={},t={}]", _rawContent, _transformedContent);
|
||||
if (_rawContent == null)
|
||||
{
|
||||
_rawContent = produceRawContent();
|
||||
if (_rawContent == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_transformedContent != null && _transformedContent.isEmpty())
|
||||
{
|
||||
if (_transformedContent != _rawContent)
|
||||
_transformedContent.succeeded();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nulling depleted transformed content");
|
||||
_transformedContent = null;
|
||||
}
|
||||
|
||||
while (_transformedContent == null)
|
||||
{
|
||||
if (_rawContent.isSpecial())
|
||||
{
|
||||
// TODO does EOF need to be passed to the interceptors?
|
||||
|
||||
_error = _rawContent.getError() != null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("raw content is special (with error = {}), returning it", _error);
|
||||
return _rawContent;
|
||||
}
|
||||
|
||||
if (_interceptor != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("using interceptor {} to transform raw content", _interceptor);
|
||||
_transformedContent = _interceptor.readFrom(_rawContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("null interceptor, transformed content = raw content");
|
||||
_transformedContent = _rawContent;
|
||||
}
|
||||
|
||||
if (_transformedContent != null && _transformedContent.isEmpty())
|
||||
{
|
||||
if (_transformedContent != _rawContent)
|
||||
_transformedContent.succeeded();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nulling depleted transformed content");
|
||||
_transformedContent = null;
|
||||
}
|
||||
|
||||
if (_transformedContent == null)
|
||||
{
|
||||
if (_rawContent.isEmpty())
|
||||
{
|
||||
_rawContent.succeeded();
|
||||
_rawContent = null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nulling depleted raw content");
|
||||
_rawContent = produceRawContent();
|
||||
if (_rawContent == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produced null raw content, returning null");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("raw content is not empty");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("transformed content is not empty");
|
||||
}
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("returning transformed content {}", _transformedContent);
|
||||
return _transformedContent;
|
||||
}
|
||||
|
||||
private HttpInput.Content produceRawContent()
|
||||
{
|
||||
HttpInput.Content content = _httpChannel.produceContent();
|
||||
if (content != null)
|
||||
{
|
||||
_rawContentArrived += content.remaining();
|
||||
if (_firstByteTimeStamp == Long.MIN_VALUE)
|
||||
_firstByteTimeStamp = System.nanoTime();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceRawContent updated rawContentArrived to {} and firstByteTimeStamp to {}", _rawContentArrived, _firstByteTimeStamp);
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceRawContent produced {}", content);
|
||||
return content;
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Blocking implementation of {@link ContentProducer}. Calling {@link #nextContent()} will block when
|
||||
* there is no available content but will never return null.
|
||||
*/
|
||||
class BlockingContentProducer implements ContentProducer
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BlockingContentProducer.class);
|
||||
|
||||
private final Semaphore _semaphore = new Semaphore(0);
|
||||
private final AsyncContentProducer _asyncContentProducer;
|
||||
|
||||
BlockingContentProducer(AsyncContentProducer delegate)
|
||||
{
|
||||
_asyncContentProducer = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("recycling {}", this);
|
||||
_asyncContentProducer.recycle();
|
||||
_semaphore.drainPermits();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available()
|
||||
{
|
||||
return _asyncContentProducer.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContent()
|
||||
{
|
||||
return _asyncContentProducer.hasContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isError()
|
||||
{
|
||||
return _asyncContentProducer.isError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMinDataRate()
|
||||
{
|
||||
_asyncContentProducer.checkMinDataRate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRawContentArrived()
|
||||
{
|
||||
return _asyncContentProducer.getRawContentArrived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAll(Throwable x)
|
||||
{
|
||||
return _asyncContentProducer.consumeAll(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content nextContent()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content content = _asyncContentProducer.nextContent();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nextContent async producer returned {}", content);
|
||||
if (content != null)
|
||||
return content;
|
||||
|
||||
// IFF isReady() returns false then HttpChannel.needContent() has been called,
|
||||
// thus we know that eventually a call to onContentProducible will come.
|
||||
if (_asyncContentProducer.isReady())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nextContent async producer is ready, retrying");
|
||||
continue;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("nextContent async producer is not ready, waiting on semaphore {}", _semaphore);
|
||||
|
||||
try
|
||||
{
|
||||
_semaphore.acquire();
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
return new HttpInput.ErrorContent(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reclaim(HttpInput.Content content)
|
||||
{
|
||||
_asyncContentProducer.reclaim(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady()
|
||||
{
|
||||
boolean ready = available() > 0;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("isReady = {}", ready);
|
||||
return ready;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Interceptor getInterceptor()
|
||||
{
|
||||
return _asyncContentProducer.getInterceptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInterceptor(HttpInput.Interceptor interceptor)
|
||||
{
|
||||
_asyncContentProducer.setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContentProducible()
|
||||
{
|
||||
// In blocking mode, the dispatched thread normally does not have to be rescheduled as it is normally in state
|
||||
// DISPATCHED blocked on the semaphore that just needs to be released for the dispatched thread to resume. This is why
|
||||
// this method always returns false.
|
||||
// But async errors can occur while the dispatched thread is NOT blocked reading (i.e.: in state WAITING),
|
||||
// so the WAITING to WOKEN transition must be done by the error-notifying thread which then has to reschedule the
|
||||
// dispatched thread after HttpChannelState.asyncError() is called.
|
||||
// Calling _asyncContentProducer.wakeup() changes the channel state from WAITING to WOKEN which would prevent the
|
||||
// subsequent call to HttpChannelState.asyncError() from rescheduling the thread.
|
||||
// AsyncServletTest.testStartAsyncThenClientStreamIdleTimeout() tests this.
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onContentProducible releasing semaphore {}", _semaphore);
|
||||
_semaphore.release();
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
/**
|
||||
* ContentProducer is the bridge between {@link HttpInput} and {@link HttpChannel}.
|
||||
* It wraps a {@link HttpChannel} and uses the {@link HttpChannel#needContent()},
|
||||
* {@link HttpChannel#produceContent()} and {@link HttpChannel#failAllContent(Throwable)}
|
||||
* methods, tracks the current state of the channel's input by updating the
|
||||
* {@link HttpChannelState} and provides the necessary mechanism to unblock
|
||||
* the reader thread when using a blocking implementation or to know if the reader thread
|
||||
* has to be rescheduled when using an async implementation.
|
||||
*/
|
||||
public interface ContentProducer
|
||||
{
|
||||
/**
|
||||
* Reset all internal state and clear any held resources.
|
||||
*/
|
||||
void recycle();
|
||||
|
||||
/**
|
||||
* Fail all content currently available in this {@link ContentProducer} instance
|
||||
* as well as in the underlying {@link HttpChannel}.
|
||||
*
|
||||
* This call is always non-blocking.
|
||||
* Doesn't change state.
|
||||
* @return true if EOF was reached.
|
||||
*/
|
||||
boolean consumeAll(Throwable x);
|
||||
|
||||
/**
|
||||
* Check if the current data rate consumption is above the minimal rate.
|
||||
* Abort the channel, fail the content currently available and throw a
|
||||
* BadMessageException(REQUEST_TIMEOUT_408) if the check fails.
|
||||
*/
|
||||
void checkMinDataRate();
|
||||
|
||||
/**
|
||||
* Get the byte count produced by the underlying {@link HttpChannel}.
|
||||
*
|
||||
* This call is always non-blocking.
|
||||
* Doesn't change state.
|
||||
* @return the byte count produced by the underlying {@link HttpChannel}.
|
||||
*/
|
||||
long getRawContentArrived();
|
||||
|
||||
/**
|
||||
* Get the byte count that can immediately be read from this
|
||||
* {@link ContentProducer} instance or the underlying {@link HttpChannel}.
|
||||
*
|
||||
* This call is always non-blocking.
|
||||
* Doesn't change state.
|
||||
* @return the available byte count.
|
||||
*/
|
||||
int available();
|
||||
|
||||
/**
|
||||
* Check if this {@link ContentProducer} instance contains some
|
||||
* content without querying the underlying {@link HttpChannel}.
|
||||
*
|
||||
* This call is always non-blocking.
|
||||
* Doesn't change state.
|
||||
* Doesn't query the HttpChannel.
|
||||
* @return true if this {@link ContentProducer} instance contains content, false otherwise.
|
||||
*/
|
||||
boolean hasContent();
|
||||
|
||||
/**
|
||||
* Check if the underlying {@link HttpChannel} reached an error content.
|
||||
* This call is always non-blocking.
|
||||
* Doesn't change state.
|
||||
* Doesn't query the HttpChannel.
|
||||
* @return true if the underlying {@link HttpChannel} reached an error content, false otherwise.
|
||||
*/
|
||||
boolean isError();
|
||||
|
||||
/**
|
||||
* Get the next content that can be read from or that describes the special condition
|
||||
* that was reached (error, eof).
|
||||
* This call may or may not block until some content is available, depending on the implementation.
|
||||
* The returned content is decoded by the interceptor set with {@link #setInterceptor(HttpInput.Interceptor)}
|
||||
* or left as-is if no intercept is set.
|
||||
* After this call, state can be either of UNREADY or IDLE.
|
||||
* @return the next content that can be read from or null if the implementation does not block
|
||||
* and has no available content.
|
||||
*/
|
||||
HttpInput.Content nextContent();
|
||||
|
||||
/**
|
||||
* Free up the content by calling {@link HttpInput.Content#succeeded()} on it
|
||||
* and updating this instance' internal state.
|
||||
*/
|
||||
void reclaim(HttpInput.Content content);
|
||||
|
||||
/**
|
||||
* Check if this {@link ContentProducer} instance has some content that can be read without blocking.
|
||||
* If there is some, the next call to {@link #nextContent()} will not block.
|
||||
* If there isn't any and the implementation does not block, this method will trigger a
|
||||
* {@link javax.servlet.ReadListener} callback once some content is available.
|
||||
* This call is always non-blocking.
|
||||
* After this call, state can be either of UNREADY or READY.
|
||||
* @return true if some content is immediately available, false otherwise.
|
||||
*/
|
||||
boolean isReady();
|
||||
|
||||
/**
|
||||
* Get the {@link org.eclipse.jetty.server.HttpInput.Interceptor}.
|
||||
* @return The {@link org.eclipse.jetty.server.HttpInput.Interceptor}, or null if none set.
|
||||
*/
|
||||
HttpInput.Interceptor getInterceptor();
|
||||
|
||||
/**
|
||||
* Set the interceptor.
|
||||
* @param interceptor The interceptor to use.
|
||||
*/
|
||||
void setInterceptor(HttpInput.Interceptor interceptor);
|
||||
|
||||
/**
|
||||
* Wake up the thread that is waiting for the next content.
|
||||
* After this call, state can be READY.
|
||||
* @return true if the thread has to be rescheduled, false otherwise.
|
||||
*/
|
||||
boolean onContentProducible();
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ import org.slf4j.LoggerFactory;
|
||||
* HttpParser.RequestHandler callbacks. The completion of the active phase is signalled by a call to
|
||||
* HttpTransport.completed().
|
||||
*/
|
||||
public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
{
|
||||
public static Listener NOOP_LISTENER = new Listener() {};
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpChannel.class);
|
||||
@ -119,11 +119,53 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
return _state.isSendError();
|
||||
}
|
||||
|
||||
protected HttpInput newHttpInput(HttpChannelState state)
|
||||
private HttpInput newHttpInput(HttpChannelState state)
|
||||
{
|
||||
return new HttpInput(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the channel that content is needed. If some content is immediately available, true is returned and
|
||||
* {@link #produceContent()} has to be called and will return a non-null object.
|
||||
* If no content is immediately available, {@link HttpInput#onContentProducible()} is called once some content arrives
|
||||
* and {@link #produceContent()} can be called without returning null.
|
||||
* If a failure happens, then {@link HttpInput#onContentProducible()} will be called and an error content will return the
|
||||
* error on the next call to {@link #produceContent()}.
|
||||
* @return true if content is immediately available.
|
||||
*/
|
||||
public abstract boolean needContent();
|
||||
|
||||
/**
|
||||
* Produce a {@link HttpInput.Content} object with data currently stored within the channel. The produced content
|
||||
* can be special (meaning calling {@link HttpInput.Content#isSpecial()} returns true) if the channel reached a special
|
||||
* state, like EOF or an error.
|
||||
* Once a special content has been returned, all subsequent calls to this method will always return a special content
|
||||
* of the same kind and {@link #needContent()} will always return true.
|
||||
* The returned content is "raw", i.e.: not decoded.
|
||||
* @return a {@link HttpInput.Content} object if one is immediately available without blocking, null otherwise.
|
||||
*/
|
||||
public abstract HttpInput.Content produceContent();
|
||||
|
||||
/**
|
||||
* Fail all content that is currently stored within the channel.
|
||||
* @param failure the failure to fail the content with.
|
||||
* @return true if EOF was reached while failing all content, false otherwise.
|
||||
*/
|
||||
public abstract boolean failAllContent(Throwable failure);
|
||||
|
||||
/**
|
||||
* Fail the channel's input.
|
||||
* @param failure the failure.
|
||||
* @return true if the channel needs to be rescheduled.
|
||||
*/
|
||||
public abstract boolean failed(Throwable failure);
|
||||
|
||||
/**
|
||||
* Mark the channel's input as EOF.
|
||||
* @return true if the channel needs to be rescheduled.
|
||||
*/
|
||||
protected abstract boolean eof();
|
||||
|
||||
protected HttpOutput newHttpOutput()
|
||||
{
|
||||
return new HttpOutput(this);
|
||||
@ -303,19 +345,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
_transientListeners.clear();
|
||||
}
|
||||
|
||||
public void onAsyncWaitForContent()
|
||||
{
|
||||
}
|
||||
|
||||
public void onBlockWaitForContent()
|
||||
{
|
||||
}
|
||||
|
||||
public void onBlockWaitForContentFailure(Throwable failure)
|
||||
{
|
||||
getRequest().getHttpInput().failed(failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
@ -445,18 +474,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
throw _state.getAsyncContextEvent().getThrowable();
|
||||
}
|
||||
|
||||
case READ_REGISTER:
|
||||
{
|
||||
onAsyncWaitForContent();
|
||||
break;
|
||||
}
|
||||
|
||||
case READ_PRODUCE:
|
||||
{
|
||||
_request.getHttpInput().asyncReadProduce();
|
||||
break;
|
||||
}
|
||||
|
||||
case READ_CALLBACK:
|
||||
{
|
||||
ContextHandler handler = _state.getContextHandler();
|
||||
@ -706,7 +723,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onContent {} {}", this, content);
|
||||
_combinedListener.onRequestContent(_request, content.getByteBuffer());
|
||||
return _request.getHttpInput().addContent(content);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onContentComplete()
|
||||
@ -729,7 +746,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onRequestComplete {}", this);
|
||||
boolean result = _request.getHttpInput().eof();
|
||||
boolean result = eof();
|
||||
_combinedListener.onRequestEnd(_request);
|
||||
return result;
|
||||
}
|
||||
@ -765,11 +782,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
_transport.onCompleted();
|
||||
}
|
||||
|
||||
public boolean onEarlyEOF()
|
||||
{
|
||||
return _request.getHttpInput().earlyEOF();
|
||||
}
|
||||
|
||||
public void onBadMessage(BadMessageException failure)
|
||||
{
|
||||
int status = failure.getCode();
|
||||
|
@ -40,6 +40,7 @@ import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -50,6 +51,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHttp.class);
|
||||
private static final HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c");
|
||||
private static final HttpInput.Content EOF = new HttpInput.EofContent();
|
||||
private final HttpConnection _httpConnection;
|
||||
private final RequestBuilder _requestBuilder = new RequestBuilder();
|
||||
private MetaData.Request _metadata;
|
||||
@ -61,6 +63,14 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
private boolean _expect102Processing = false;
|
||||
private List<String> _complianceViolations;
|
||||
private HttpFields.Mutable _trailers;
|
||||
// Field _content doesn't need to be volatile nor protected by a lock
|
||||
// as it is always accessed by the same thread, i.e.: we get notified by onFillable
|
||||
// that the socket contains new bytes and either schedule an onDataAvailable
|
||||
// call that is going to read the socket or release the blocking semaphore to wake up
|
||||
// the blocked reader and make it read the socket. The same logic is true for async
|
||||
// events like timeout: we get notified and either schedule onError or release the
|
||||
// blocking semaphore.
|
||||
private HttpInput.Content _content;
|
||||
|
||||
public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
|
||||
{
|
||||
@ -75,6 +85,79 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
_httpConnection.getGenerator().setPersistent(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
if (_content != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("needContent has content immediately available: {}", _content);
|
||||
return true;
|
||||
}
|
||||
_httpConnection.parseAndFillForContent();
|
||||
if (_content != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("needContent has content after parseAndFillForContent: {}", _content);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("needContent has no content");
|
||||
_httpConnection.asyncReadFillInterested();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
if (_content == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceContent has no content, parsing and filling");
|
||||
_httpConnection.parseAndFillForContent();
|
||||
}
|
||||
HttpInput.Content result = _content;
|
||||
if (result != null && !result.isSpecial())
|
||||
_content = result.isEof() ? EOF : null;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("produceContent produced {}", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failing all content with {}", (Object)failure);
|
||||
if (_content != null && !_content.isSpecial())
|
||||
{
|
||||
_content.failed(failure);
|
||||
_content = _content.isEof() ? EOF : null;
|
||||
if (_content == EOF)
|
||||
return true;
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content c = produceContent();
|
||||
if (c == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content, EOF was not reached");
|
||||
return false;
|
||||
}
|
||||
c.skip(c.remaining());
|
||||
c.failed(failure);
|
||||
if (c.isSpecial())
|
||||
{
|
||||
boolean atEof = c.isEof();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content, EOF = {}", atEof);
|
||||
return atEof;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void badMessage(BadMessageException failure)
|
||||
{
|
||||
@ -85,7 +168,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
if (_metadata == null)
|
||||
_metadata = _requestBuilder.build();
|
||||
onRequest(_metadata);
|
||||
getRequest().getHttpInput().earlyEOF();
|
||||
markEarlyEOF();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -96,12 +179,23 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer content)
|
||||
public boolean content(ByteBuffer buffer)
|
||||
{
|
||||
HttpInput.Content c = _httpConnection.newContent(content);
|
||||
boolean handle = onContent(c) || _delayedForContent;
|
||||
_delayedForContent = false;
|
||||
return handle;
|
||||
HttpInput.Content content = _httpConnection.newContent(buffer);
|
||||
if (_content != null)
|
||||
{
|
||||
if (_content.isSpecial())
|
||||
content.failed(_content.getError());
|
||||
else
|
||||
throw new AssertionError("Cannot overwrite exiting content " + _content + " with " + content);
|
||||
}
|
||||
else
|
||||
{
|
||||
_content = content;
|
||||
onContent(_content);
|
||||
_delayedForContent = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -147,12 +241,69 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
_httpConnection.getGenerator().setPersistent(false);
|
||||
// If we have no request yet, just close
|
||||
if (_metadata == null)
|
||||
_httpConnection.close();
|
||||
else if (onEarlyEOF() || _delayedForContent)
|
||||
{
|
||||
_delayedForContent = false;
|
||||
handle();
|
||||
_httpConnection.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
markEarlyEOF();
|
||||
if (_delayedForContent)
|
||||
{
|
||||
_delayedForContent = false;
|
||||
handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markEarlyEOF()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("received early EOF, content = {}", _content);
|
||||
EofException failure = new EofException("Early EOF");
|
||||
if (_content != null)
|
||||
_content.failed(failure);
|
||||
_content = new HttpInput.ErrorContent(failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("received EOF, content = {}", _content);
|
||||
if (_content == null)
|
||||
{
|
||||
_content = EOF;
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpInput.Content c = _content;
|
||||
_content = new HttpInput.WrappingContent(c, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed {}, content = {}", x, _content);
|
||||
|
||||
Throwable error = null;
|
||||
if (_content != null && _content.isSpecial())
|
||||
error = _content.getError();
|
||||
|
||||
if (error != null && error != x)
|
||||
{
|
||||
error.addSuppressed(x);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_content != null)
|
||||
_content.failed(x);
|
||||
_content = new HttpInput.ErrorContent(x);
|
||||
}
|
||||
|
||||
return getRequest().getHttpInput().onContentProducible();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -309,24 +460,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
return onRequestComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAsyncWaitForContent()
|
||||
{
|
||||
_httpConnection.asyncReadFillInterested();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockWaitForContent()
|
||||
{
|
||||
_httpConnection.blockingReadFillInterested();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockWaitForContentFailure(Throwable failure)
|
||||
{
|
||||
_httpConnection.blockingReadFailure(failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details)
|
||||
{
|
||||
@ -434,6 +567,9 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
_upgrade = null;
|
||||
_trailers = null;
|
||||
_metadata = null;
|
||||
if (_content != null && !_content.isSpecial())
|
||||
throw new AssertionError("unconsumed content: " + _content);
|
||||
_content = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -459,12 +595,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
super.handleException(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpInput newHttpInput(HttpChannelState state)
|
||||
{
|
||||
return new HttpInputOverHTTP(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Attempts to perform an HTTP/1.1 upgrade.</p>
|
||||
* <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector
|
||||
@ -534,13 +664,24 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
|
||||
if (_delayedForContent)
|
||||
{
|
||||
_delayedForContent = false;
|
||||
getRequest().getHttpInput().onIdleTimeout(timeout);
|
||||
doOnIdleTimeout(timeout);
|
||||
execute(this);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void doOnIdleTimeout(Throwable x)
|
||||
{
|
||||
boolean neverDispatched = getState().isIdle();
|
||||
boolean waitingForContent = _content == null || _content.remaining() == 0;
|
||||
if ((waitingForContent || neverDispatched) && (_content == null || !_content.isSpecial()))
|
||||
{
|
||||
x.addSuppressed(new Throwable("HttpInput idle timeout"));
|
||||
_content = new HttpInput.ErrorContent(x);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestBuilder
|
||||
{
|
||||
private final HttpFields.Mutable _fieldsBuilder = HttpFields.build();
|
||||
|
@ -107,12 +107,9 @@ public class HttpChannelState
|
||||
*/
|
||||
private enum InputState
|
||||
{
|
||||
IDLE, // No isReady; No data
|
||||
REGISTER, // isReady()==false handling; No data
|
||||
REGISTERED, // isReady()==false !handling; No data
|
||||
POSSIBLE, // isReady()==false async read callback called (http/1 only)
|
||||
PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only)
|
||||
READY // isReady() was false, onContentAdded has been called
|
||||
IDLE, // No isReady; No data
|
||||
UNREADY, // isReady()==false; No data
|
||||
READY // isReady() was false; data is available
|
||||
}
|
||||
|
||||
/*
|
||||
@ -137,8 +134,6 @@ public class HttpChannelState
|
||||
ASYNC_ERROR, // handle an async error
|
||||
ASYNC_TIMEOUT, // call asyncContext onTimeout
|
||||
WRITE_CALLBACK, // handle an IO write callback
|
||||
READ_REGISTER, // Register for fill interest
|
||||
READ_PRODUCE, // Check is a read is possible by parsing/filling
|
||||
READ_CALLBACK, // handle an IO read callback
|
||||
COMPLETE, // Complete the response by closing output
|
||||
TERMINATED, // No further actions
|
||||
@ -465,19 +460,12 @@ public class HttpChannelState
|
||||
case ASYNC:
|
||||
switch (_inputState)
|
||||
{
|
||||
case POSSIBLE:
|
||||
_inputState = InputState.PRODUCING;
|
||||
return Action.READ_PRODUCE;
|
||||
case IDLE:
|
||||
case UNREADY:
|
||||
break;
|
||||
case READY:
|
||||
_inputState = InputState.IDLE;
|
||||
return Action.READ_CALLBACK;
|
||||
case REGISTER:
|
||||
case PRODUCING:
|
||||
_inputState = InputState.REGISTERED;
|
||||
return Action.READ_REGISTER;
|
||||
case IDLE:
|
||||
case REGISTERED:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(getStatusStringLocked());
|
||||
@ -1222,99 +1210,8 @@ public class HttpChannelState
|
||||
_channel.getRequest().setAttribute(name, attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to signal async read isReady() has returned false.
|
||||
* This indicates that there is no content available to be consumed
|
||||
* and that once the channel enters the ASYNC_WAIT state it will
|
||||
* register for read interest by calling {@link HttpChannel#onAsyncWaitForContent()}
|
||||
* either from this method or from a subsequent call to {@link #unhandle()}.
|
||||
*/
|
||||
public void onReadUnready()
|
||||
{
|
||||
boolean interested = false;
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onReadUnready {}", toStringLocked());
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case IDLE:
|
||||
case READY:
|
||||
if (_state == State.WAITING)
|
||||
{
|
||||
interested = true;
|
||||
_inputState = InputState.REGISTERED;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputState = InputState.REGISTER;
|
||||
}
|
||||
break;
|
||||
|
||||
case REGISTER:
|
||||
case REGISTERED:
|
||||
case POSSIBLE:
|
||||
case PRODUCING:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(toStringLocked());
|
||||
}
|
||||
}
|
||||
|
||||
if (interested)
|
||||
_channel.onAsyncWaitForContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to signal that content is now available to read.
|
||||
* If the channel is in ASYNC_WAIT state and unready (ie isReady() has
|
||||
* returned false), then the state is changed to ASYNC_WOKEN and true
|
||||
* is returned.
|
||||
*
|
||||
* @return True IFF the channel was unready and in ASYNC_WAIT state
|
||||
*/
|
||||
public boolean onContentAdded()
|
||||
{
|
||||
boolean woken = false;
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onContentAdded {}", toStringLocked());
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case IDLE:
|
||||
case READY:
|
||||
break;
|
||||
|
||||
case PRODUCING:
|
||||
_inputState = InputState.READY;
|
||||
break;
|
||||
|
||||
case REGISTER:
|
||||
case REGISTERED:
|
||||
_inputState = InputState.READY;
|
||||
if (_state == State.WAITING)
|
||||
{
|
||||
woken = true;
|
||||
_state = State.WOKEN;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(toStringLocked());
|
||||
}
|
||||
}
|
||||
return woken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to signal that the channel is ready for a callback.
|
||||
* This is similar to calling {@link #onReadUnready()} followed by
|
||||
* {@link #onContentAdded()}, except that as content is already
|
||||
* available, read interest is never set.
|
||||
*
|
||||
* @return true if woken
|
||||
*/
|
||||
@ -1328,7 +1225,11 @@ public class HttpChannelState
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case READY:
|
||||
_inputState = InputState.READY;
|
||||
break;
|
||||
case IDLE:
|
||||
case UNREADY:
|
||||
_inputState = InputState.READY;
|
||||
if (_state == State.WAITING)
|
||||
{
|
||||
@ -1344,25 +1245,20 @@ public class HttpChannelState
|
||||
return woken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to indicate that more content may be available,
|
||||
* but that a handling thread may need to produce (fill/parse)
|
||||
* it. Typically called by the async read success callback.
|
||||
*
|
||||
* @return {@code true} if more content may be available
|
||||
*/
|
||||
public boolean onReadPossible()
|
||||
public boolean onReadEof()
|
||||
{
|
||||
boolean woken = false;
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onReadPossible {}", toStringLocked());
|
||||
LOG.debug("onReadEof {}", toStringLocked());
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case REGISTERED:
|
||||
_inputState = InputState.POSSIBLE;
|
||||
case IDLE:
|
||||
case READY:
|
||||
case UNREADY:
|
||||
_inputState = InputState.READY;
|
||||
if (_state == State.WAITING)
|
||||
{
|
||||
woken = true;
|
||||
@ -1377,29 +1273,72 @@ public class HttpChannelState
|
||||
return woken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to signal that a read has read -1.
|
||||
* Will wake if the read was called while in ASYNC_WAIT state
|
||||
*
|
||||
* @return {@code true} if woken
|
||||
*/
|
||||
public boolean onReadEof()
|
||||
public void onContentAdded()
|
||||
{
|
||||
boolean woken = false;
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onEof {}", toStringLocked());
|
||||
LOG.debug("onContentAdded {}", toStringLocked());
|
||||
|
||||
// Force read ready so onAllDataRead can be called
|
||||
_inputState = InputState.READY;
|
||||
if (_state == State.WAITING)
|
||||
switch (_inputState)
|
||||
{
|
||||
woken = true;
|
||||
_state = State.WOKEN;
|
||||
case IDLE:
|
||||
case UNREADY:
|
||||
case READY:
|
||||
_inputState = InputState.READY;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(toStringLocked());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onReadIdle()
|
||||
{
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onReadIdle {}", toStringLocked());
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case UNREADY:
|
||||
case READY:
|
||||
case IDLE:
|
||||
_inputState = InputState.IDLE;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(toStringLocked());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to indicate that more content may be available,
|
||||
* but that a handling thread may need to produce (fill/parse)
|
||||
* it. Typically called by the async read success callback.
|
||||
*/
|
||||
public void onReadUnready()
|
||||
{
|
||||
try (AutoLock l = lock())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onReadUnready {}", toStringLocked());
|
||||
|
||||
switch (_inputState)
|
||||
{
|
||||
case IDLE:
|
||||
case UNREADY:
|
||||
case READY: // READY->UNREADY is needed by AsyncServletIOTest.testStolenAsyncRead
|
||||
_inputState = InputState.UNREADY;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(toStringLocked());
|
||||
}
|
||||
}
|
||||
return woken;
|
||||
}
|
||||
|
||||
public boolean onWritePossible()
|
||||
|
@ -0,0 +1,84 @@
|
||||
@startuml
|
||||
title HttpChannelState
|
||||
|
||||
note top of onReadReady_inputState: onReadReady
|
||||
|
||||
state "input state" as onReadReady_inputState {
|
||||
state "IDLE" as onReadReady_IDLE
|
||||
state "UNREADY" as onReadReady_UNREADY
|
||||
state "READY" as onReadReady_READY
|
||||
|
||||
state "channel state" as onReadReady_channelState {
|
||||
state "WAITING" as onReadReady_WAITING
|
||||
state "WOKEN" as onReadReady_WOKEN
|
||||
onReadReady_WAITING --> onReadReady_WOKEN
|
||||
}
|
||||
|
||||
onReadReady_IDLE --> onReadReady_channelState
|
||||
onReadReady_UNREADY --> onReadReady_channelState
|
||||
|
||||
onReadReady_channelState --> onReadReady_READY
|
||||
onReadReady_READY --> onReadReady_READY
|
||||
}
|
||||
|
||||
|
||||
note top of onReadEof_inputState: onReadEof
|
||||
|
||||
state "input state" as onReadEof_inputState {
|
||||
state "IDLE" as onReadEof_IDLE
|
||||
state "UNREADY" as onReadEof_UNREADY
|
||||
state "READY" as onReadEof_READY
|
||||
|
||||
state "channel state" as onReadEof_channelState {
|
||||
state "WAITING" as onReadEof_WAITING
|
||||
state "WOKEN" as onReadEof_WOKEN
|
||||
onReadEof_WAITING --> onReadEof_WOKEN
|
||||
}
|
||||
|
||||
onReadEof_IDLE --> onReadEof_channelState
|
||||
onReadEof_UNREADY --> onReadEof_channelState
|
||||
onReadEof_READY --> onReadEof_channelState
|
||||
|
||||
onReadEof_channelState --> onReadEof_READY
|
||||
}
|
||||
|
||||
|
||||
note top of onReadIdle_inputState: onReadIdle
|
||||
|
||||
state "input state" as onReadIdle_inputState {
|
||||
state "IDLE" as onReadIdle_IDLE
|
||||
state "UNREADY" as onReadIdle_UNREADY
|
||||
state "READY" as onReadIdle_READY
|
||||
|
||||
onReadIdle_IDLE --> onReadIdle_IDLE
|
||||
onReadIdle_UNREADY --> onReadIdle_IDLE
|
||||
onReadIdle_READY --> onReadIdle_IDLE
|
||||
}
|
||||
|
||||
|
||||
note top of onReadUnready_inputState: onReadUnready
|
||||
|
||||
state "input state" as onReadUnready_inputState {
|
||||
state "IDLE" as onReadUnready_IDLE
|
||||
state "UNREADY" as onReadUnready_UNREADY
|
||||
state "READY" as onReadUnready_READY
|
||||
|
||||
onReadUnready_IDLE --> onReadUnready_UNREADY
|
||||
onReadUnready_UNREADY --> onReadUnready_UNREADY
|
||||
onReadUnready_READY --> onReadUnready_UNREADY
|
||||
}
|
||||
|
||||
|
||||
note top of onContentAdded_inputState: onContentAdded
|
||||
|
||||
state "input state" as onContentAdded_inputState {
|
||||
state "IDLE" as onContentAdded_IDLE
|
||||
state "UNREADY" as onContentAdded_UNREADY
|
||||
state "READY" as onContentAdded_READY
|
||||
|
||||
onContentAdded_IDLE --> onContentAdded_READY
|
||||
onContentAdded_UNREADY --> onContentAdded_READY
|
||||
onContentAdded_READY --> onContentAdded_READY
|
||||
}
|
||||
|
||||
@enduml
|
@ -33,7 +33,6 @@ import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpParser;
|
||||
import org.eclipse.jetty.http.HttpParser.RequestHandler;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
@ -68,7 +67,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||
private final HttpParser _parser;
|
||||
private final AtomicInteger _contentBufferReferences = new AtomicInteger();
|
||||
private volatile ByteBuffer _requestBuffer = null;
|
||||
private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback();
|
||||
private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback();
|
||||
private final SendCallback _sendCallback = new SendCallback();
|
||||
private final boolean _recordHttpComplianceViolations;
|
||||
@ -316,21 +314,20 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill and parse data looking for content
|
||||
*
|
||||
* @return true if an {@link RequestHandler} method was called and it returned true;
|
||||
* Parse and fill data, looking for content
|
||||
*/
|
||||
protected boolean fillAndParseForContent()
|
||||
void parseAndFillForContent()
|
||||
{
|
||||
boolean handled = false;
|
||||
// When fillRequestBuffer() is called, it must always be followed by a parseRequestBuffer() call otherwise this method
|
||||
// doesn't trigger EOF/earlyEOF which breaks AsyncRequestReadTest.testPartialReadThenShutdown()
|
||||
int filled = Integer.MAX_VALUE;
|
||||
while (_parser.inContentState())
|
||||
{
|
||||
int filled = fillRequestBuffer();
|
||||
handled = parseRequestBuffer();
|
||||
if (handled || filled <= 0 || _input.hasContent())
|
||||
boolean handled = parseRequestBuffer();
|
||||
if (handled || filled <= 0)
|
||||
break;
|
||||
filled = fillRequestBuffer();
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
private int fillRequestBuffer()
|
||||
@ -600,25 +597,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||
|
||||
public void asyncReadFillInterested()
|
||||
{
|
||||
getEndPoint().fillInterested(_asyncReadCallback);
|
||||
}
|
||||
|
||||
public void blockingReadFillInterested()
|
||||
{
|
||||
// We try fillInterested here because of SSL and
|
||||
// spurious wakeups. With blocking reads, we read in a loop
|
||||
// that tries to read/parse content and blocks waiting if there is
|
||||
// none available. The loop can be woken up by incoming encrypted
|
||||
// bytes, which due to SSL might not produce any decrypted bytes.
|
||||
// Thus the loop needs to register fill interest again. However if
|
||||
// the loop is woken up spuriously, then the register interest again
|
||||
// can result in a pending read exception, unless we use tryFillInterested.
|
||||
getEndPoint().tryFillInterested(_blockingReadCallback);
|
||||
}
|
||||
|
||||
public void blockingReadFailure(Throwable e)
|
||||
{
|
||||
_blockingReadCallback.failed(e);
|
||||
getEndPoint().tryFillInterested(_asyncReadCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -655,8 +634,15 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
if (_contentBufferReferences.decrementAndGet() == 0)
|
||||
int counter = _contentBufferReferences.decrementAndGet();
|
||||
if (counter == 0)
|
||||
releaseRequestBuffer();
|
||||
// TODO: this should do something (warn? fail?) if _contentBufferReferences goes below 0
|
||||
if (counter < 0)
|
||||
{
|
||||
LOG.warn("Content reference counting went below zero: {}", counter);
|
||||
_contentBufferReferences.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -666,44 +652,30 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockingReadCallback implements Callback
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
_input.unblock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
_input.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
// This callback does not block, rather it wakes up the
|
||||
// thread that is blocked waiting on the read.
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncReadCallback implements Callback
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
if (_channel.getState().onReadPossible())
|
||||
if (_channel.getRequest().getHttpInput().onContentProducible())
|
||||
_channel.handle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
if (_input.failed(x))
|
||||
if (_channel.failed(x))
|
||||
_channel.handle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
// This callback does not block when the HttpInput is in blocking mode,
|
||||
// rather it wakes up the thread that is blocked waiting on the read;
|
||||
// but it can if it is in async mode, hence the varying InvocationType.
|
||||
return _channel.getRequest().getHttpInput().isAsync() ? InvocationType.BLOCKING : InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
|
||||
private class SendCallback extends IteratingCallback
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpInputOverHTTP extends HttpInput
|
||||
{
|
||||
public HttpInputOverHTTP(HttpChannelState state)
|
||||
{
|
||||
super(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void produceContent() throws IOException
|
||||
{
|
||||
((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).fillAndParseForContent();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
@startuml
|
||||
|
||||
IDLE:
|
||||
READY:
|
||||
UNREADY:
|
||||
|
||||
[*] --> IDLE
|
||||
|
||||
IDLE --> UNREADY : isReady
|
||||
IDLE -right->READY : isReady
|
||||
|
||||
UNREADY -up-> READY : ASYNC onContentProducible
|
||||
|
||||
READY -left->IDLE : nextContent
|
||||
|
||||
@enduml
|
@ -0,0 +1,114 @@
|
||||
@startuml
|
||||
title "HttpInput"
|
||||
|
||||
participant AsyncContentDelivery as "[async\ncontent\ndelivery]"
|
||||
participant HttpChannel as "Http\nChannel\n"
|
||||
participant HttpChannelState as "Http\nChannel\nState"
|
||||
participant HttpInputInterceptor as "Http\nInput.\nInterceptor"
|
||||
participant AsyncContentProducer as "Async\nContent\nProducer"
|
||||
participant HttpInput as "Http\nInput\n"
|
||||
participant Application as "\nApplication\n"
|
||||
|
||||
autoactivate on
|
||||
|
||||
== Async Read ==
|
||||
|
||||
Application->HttpInput: read
|
||||
activate Application
|
||||
HttpInput->AsyncContentProducer: nextContent
|
||||
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
|
||||
AsyncContentProducer->HttpChannel: produceContent
|
||||
return raw content or null
|
||||
alt if raw content is not null
|
||||
AsyncContentProducer->HttpInputInterceptor: readFrom
|
||||
return transformed content
|
||||
end
|
||||
return
|
||||
alt if transformed content is not null
|
||||
AsyncContentProducer->HttpChannelState: onReadIdle
|
||||
return
|
||||
end
|
||||
return content or null
|
||||
note over HttpInput
|
||||
throw ISE
|
||||
if content
|
||||
is null
|
||||
end note
|
||||
HttpInput->AsyncContentProducer: reclaim
|
||||
return
|
||||
return
|
||||
deactivate Application
|
||||
|
||||
== isReady ==
|
||||
|
||||
Application->HttpInput: isReady
|
||||
activate Application
|
||||
HttpInput->AsyncContentProducer: isReady
|
||||
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
|
||||
AsyncContentProducer->HttpChannel: produceContent
|
||||
return raw content or null
|
||||
alt if raw content is not null
|
||||
AsyncContentProducer->HttpInputInterceptor: readFrom
|
||||
return transformed content
|
||||
end
|
||||
return
|
||||
alt if transformed content is not null
|
||||
AsyncContentProducer->HttpChannelState: onContentAdded
|
||||
return
|
||||
else transformed content is null
|
||||
AsyncContentProducer->HttpChannelState: onReadUnready
|
||||
return
|
||||
AsyncContentProducer->HttpChannel: needContent
|
||||
return
|
||||
alt if needContent returns true
|
||||
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
|
||||
return
|
||||
alt if transformed content is not null
|
||||
AsyncContentProducer->HttpChannelState: onContentAdded
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
return boolean\n[transformed\ncontent is not null]
|
||||
return
|
||||
deactivate Application
|
||||
|
||||
alt if content arrives
|
||||
AsyncContentDelivery->HttpInput: onContentProducible
|
||||
HttpInput->AsyncContentProducer: onContentProducible
|
||||
alt if not at EOF
|
||||
AsyncContentProducer->HttpChannelState: onReadReady
|
||||
return true if woken
|
||||
else if at EOF
|
||||
AsyncContentProducer->HttpChannelState: onReadEof
|
||||
return true if woken
|
||||
end
|
||||
return true if woken
|
||||
return true if woken
|
||||
alt onContentProducible returns true
|
||||
AsyncContentDelivery->HttpChannel: execute(HttpChannel)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
|||
|
||||
|
||||
== available ==
|
||||
|
||||
Application->HttpInput: available
|
||||
activate Application
|
||||
HttpInput->AsyncContentProducer: available
|
||||
AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent
|
||||
AsyncContentProducer->HttpChannel: produceContent
|
||||
return raw content or null
|
||||
alt if raw content is not null
|
||||
AsyncContentProducer->HttpInputInterceptor: readFrom
|
||||
return transformed content
|
||||
end
|
||||
return
|
||||
return content size or\n0 if content is null
|
||||
return
|
||||
deactivate Application
|
||||
|
||||
|||
|
||||
@enduml
|
@ -0,0 +1,64 @@
|
||||
@startuml
|
||||
title "HttpInput"
|
||||
|
||||
participant AsyncContentDelivery as "[async\ncontent\ndelivery]"
|
||||
participant HttpChannel as "Http\nChannel\n"
|
||||
participant HttpChannelState as "Http\nChannel\nState"
|
||||
participant AsyncContentProducer as "Async\nContent\nProducer"
|
||||
participant Semaphore as "\nSemaphore\n"
|
||||
participant BlockingContentProducer as "Blocking\nContent\nProducer"
|
||||
participant HttpInput as "Http\nInput\n"
|
||||
participant Application as "\nApplication\n"
|
||||
|
||||
autoactivate on
|
||||
|
||||
== Blocking Read ==
|
||||
|
||||
Application->HttpInput: read
|
||||
activate Application
|
||||
HttpInput->BlockingContentProducer: nextContent
|
||||
loop
|
||||
BlockingContentProducer->AsyncContentProducer: nextContent
|
||||
AsyncContentProducer->AsyncContentProducer: nextTransformedContent
|
||||
AsyncContentProducer->HttpChannel: produceContent
|
||||
return
|
||||
return
|
||||
alt content is not null
|
||||
AsyncContentProducer->HttpChannelState: onReadIdle
|
||||
return
|
||||
end
|
||||
return content or null
|
||||
alt content is null
|
||||
BlockingContentProducer->HttpChannelState: onReadUnready
|
||||
return
|
||||
BlockingContentProducer->HttpChannel: needContent
|
||||
return
|
||||
alt needContent returns false
|
||||
BlockingContentProducer->Semaphore: acquire
|
||||
return
|
||||
else needContent returns true
|
||||
note over BlockingContentProducer
|
||||
continue loop
|
||||
end note
|
||||
end
|
||||
else content is not null
|
||||
return non-null content
|
||||
end
|
||||
end
|
||||
' return from BlockingContentProducer: nextContent
|
||||
HttpInput->BlockingContentProducer: reclaim
|
||||
BlockingContentProducer->AsyncContentProducer: reclaim
|
||||
return
|
||||
return
|
||||
return
|
||||
deactivate Application
|
||||
|
||||
alt if content arrives
|
||||
AsyncContentDelivery->HttpInput: wakeup
|
||||
HttpInput->BlockingContentProducer: wakeup
|
||||
BlockingContentProducer->Semaphore: release
|
||||
return
|
||||
return false
|
||||
return false
|
||||
end
|
||||
@enduml
|
@ -749,7 +749,7 @@ public class Request implements HttpServletRequest
|
||||
|
||||
public long getContentRead()
|
||||
{
|
||||
return _input.getContentConsumed();
|
||||
return _input.getContentReceived();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -48,6 +48,7 @@ import javax.servlet.http.HttpSessionListener;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.Syntax;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.SessionIdManager;
|
||||
@ -645,7 +646,7 @@ public class SessionHandler extends ScopedHandler
|
||||
HttpCookie cookie = null;
|
||||
|
||||
cookie = new HttpCookie(
|
||||
_cookieConfig.getName(),
|
||||
getSessionCookieName(_cookieConfig),
|
||||
id,
|
||||
_cookieConfig.getDomain(),
|
||||
sessionPath,
|
||||
@ -1334,6 +1335,13 @@ public class SessionHandler extends ScopedHandler
|
||||
public Session getSession();
|
||||
}
|
||||
|
||||
public static String getSessionCookieName(SessionCookieConfig config)
|
||||
{
|
||||
if (config == null || config.getName() == null)
|
||||
return __DefaultSessionCookie;
|
||||
return config.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* CookieConfig
|
||||
*
|
||||
@ -1423,6 +1431,10 @@ public class SessionHandler extends ScopedHandler
|
||||
{
|
||||
if (_context != null && _context.getContextHandler().isAvailable())
|
||||
throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
|
||||
if ("".equals(name))
|
||||
throw new IllegalArgumentException("Blank cookie name");
|
||||
if (name != null)
|
||||
Syntax.requireValidRFC2616Token(name, "Bad Session cookie name");
|
||||
_sessionCookie = name;
|
||||
}
|
||||
|
||||
@ -1596,12 +1608,12 @@ public class SessionHandler extends ScopedHandler
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null && cookies.length > 0)
|
||||
{
|
||||
final String sessionCookie = getSessionCookieConfig().getName();
|
||||
for (int i = 0; i < cookies.length; i++)
|
||||
final String sessionCookie = getSessionCookieName(getSessionCookieConfig());
|
||||
for (Cookie cookie : cookies)
|
||||
{
|
||||
if (sessionCookie.equalsIgnoreCase(cookies[i].getName()))
|
||||
if (sessionCookie.equalsIgnoreCase(cookie.getName()))
|
||||
{
|
||||
String id = cookies[i].getValue();
|
||||
String id = cookie.getValue();
|
||||
requestedSessionIdFromCookie = true;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Got Session ID {} from cookie {}", id, sessionCookie);
|
||||
|
@ -0,0 +1,340 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.BrokenBarrierException;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
|
||||
import org.eclipse.jetty.util.compression.CompressionPool;
|
||||
import org.eclipse.jetty.util.compression.InflaterPool;
|
||||
import org.hamcrest.core.Is;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
public class AsyncContentProducerTest
|
||||
{
|
||||
private ScheduledExecutorService scheduledExecutorService;
|
||||
private InflaterPool inflaterPool;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp()
|
||||
{
|
||||
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
inflaterPool = new InflaterPool(-1, true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentProducerNoInterceptor() throws Exception
|
||||
{
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(buffers);
|
||||
final String originalContentString = asString(buffers);
|
||||
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
|
||||
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentProducerNoInterceptorWithError() throws Exception
|
||||
{
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(buffers);
|
||||
final String originalContentString = asString(buffers);
|
||||
final Throwable expectedError = new EofException("Early EOF");
|
||||
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
|
||||
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
|
||||
assertThat(error, Is.is(expectedError));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentProducerGzipInterceptor() throws Exception
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
|
||||
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentProducerGzipInterceptorWithTinyBuffers() throws Exception
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
|
||||
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier));
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, totalContentBytesCount + buffers.length + 2, 25, 4, barrier);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerGzipInterceptorWithError() throws Exception
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
final Throwable expectedError = new Throwable("HttpInput idle timeout");
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
|
||||
ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier));
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier);
|
||||
assertThat(error, Is.is(expectedError));
|
||||
}
|
||||
|
||||
private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, ContentProducer contentProducer, int totalContentCount, int readyCount, int notReadyCount, CyclicBarrier barrier) throws InterruptedException, BrokenBarrierException, TimeoutException
|
||||
{
|
||||
int readBytes = 0;
|
||||
String consumedString = "";
|
||||
int nextContentCount = 0;
|
||||
int isReadyFalseCount = 0;
|
||||
int isReadyTrueCount = 0;
|
||||
Throwable error = null;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (contentProducer.isReady())
|
||||
isReadyTrueCount++;
|
||||
else
|
||||
isReadyFalseCount++;
|
||||
|
||||
HttpInput.Content content = contentProducer.nextContent();
|
||||
nextContentCount++;
|
||||
if (content == null)
|
||||
{
|
||||
barrier.await(5, TimeUnit.SECONDS);
|
||||
content = contentProducer.nextContent();
|
||||
nextContentCount++;
|
||||
}
|
||||
assertThat(content, notNullValue());
|
||||
|
||||
if (content.isSpecial())
|
||||
{
|
||||
if (content.isEof())
|
||||
break;
|
||||
error = content.getError();
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] b = new byte[content.remaining()];
|
||||
readBytes += b.length;
|
||||
content.getByteBuffer().get(b);
|
||||
consumedString += new String(b, StandardCharsets.ISO_8859_1);
|
||||
content.skip(content.remaining());
|
||||
}
|
||||
|
||||
assertThat(nextContentCount, is(totalContentCount));
|
||||
assertThat(readBytes, is(totalContentBytesCount));
|
||||
assertThat(consumedString, is(originalContentString));
|
||||
assertThat(isReadyFalseCount, is(notReadyCount));
|
||||
assertThat(isReadyTrueCount, is(readyCount));
|
||||
return error;
|
||||
}
|
||||
|
||||
private static int countRemaining(ByteBuffer[] byteBuffers)
|
||||
{
|
||||
int total = 0;
|
||||
for (ByteBuffer byteBuffer : byteBuffers)
|
||||
{
|
||||
total += byteBuffer.remaining();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static String asString(ByteBuffer[] buffers)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ByteBuffer buffer : buffers)
|
||||
{
|
||||
byte[] b = new byte[buffer.remaining()];
|
||||
buffer.duplicate().get(b);
|
||||
sb.append(new String(b, StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
GZIPOutputStream output = new GZIPOutputStream(baos);
|
||||
|
||||
byte[] b = new byte[uncompressedBuffer.remaining()];
|
||||
uncompressedBuffer.get(b);
|
||||
output.write(b);
|
||||
|
||||
output.close();
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArrayDelayedHttpChannel extends HttpChannel
|
||||
{
|
||||
private final ByteBuffer[] byteBuffers;
|
||||
private final HttpInput.Content finalContent;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
private final CyclicBarrier barrier;
|
||||
private int counter;
|
||||
private volatile HttpInput.Content nextContent;
|
||||
|
||||
public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, CyclicBarrier barrier)
|
||||
{
|
||||
super(new MockConnector(), new HttpConfiguration(), null, null);
|
||||
this.byteBuffers = new ByteBuffer[byteBuffers.length];
|
||||
this.finalContent = finalContent;
|
||||
this.scheduledExecutorService = scheduledExecutorService;
|
||||
this.barrier = barrier;
|
||||
for (int i = 0; i < byteBuffers.length; i++)
|
||||
{
|
||||
this.byteBuffers[i] = byteBuffers[i].duplicate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
if (nextContent != null)
|
||||
return true;
|
||||
scheduledExecutorService.schedule(() ->
|
||||
{
|
||||
if (byteBuffers.length > counter)
|
||||
nextContent = new HttpInput.Content(byteBuffers[counter++]);
|
||||
else
|
||||
nextContent = finalContent;
|
||||
try
|
||||
{
|
||||
barrier.await(5, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}, 50, TimeUnit.MILLISECONDS);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
HttpInput.Content result = nextContent;
|
||||
nextContent = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
nextContent = null;
|
||||
counter = byteBuffers.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
|
||||
import org.eclipse.jetty.util.compression.InflaterPool;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
|
||||
public class BlockingContentProducerTest
|
||||
{
|
||||
private ScheduledExecutorService scheduledExecutorService;
|
||||
private InflaterPool inflaterPool;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp()
|
||||
{
|
||||
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
inflaterPool = new InflaterPool(-1, true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
scheduledExecutorService.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerNoInterceptor()
|
||||
{
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(buffers);
|
||||
final String originalContentString = asString(buffers);
|
||||
|
||||
AtomicReference<ContentProducer> ref = new AtomicReference<>();
|
||||
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
|
||||
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
|
||||
ref.set(contentProducer);
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerNoInterceptorWithError()
|
||||
{
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(buffers);
|
||||
final String originalContentString = asString(buffers);
|
||||
final Throwable expectedError = new EofException("Early EOF");
|
||||
|
||||
AtomicReference<ContentProducer> ref = new AtomicReference<>();
|
||||
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible());
|
||||
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
|
||||
ref.set(contentProducer);
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
|
||||
assertThat(error, is(expectedError));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerGzipInterceptor()
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
AtomicReference<ContentProducer> ref = new AtomicReference<>();
|
||||
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
|
||||
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
|
||||
ref.set(contentProducer);
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerGzipInterceptorWithTinyBuffers()
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
AtomicReference<ContentProducer> ref = new AtomicReference<>();
|
||||
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible());
|
||||
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
|
||||
ref.set(contentProducer);
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, totalContentBytesCount + 1, contentProducer);
|
||||
assertThat(error, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingContentProducerGzipInterceptorWithError()
|
||||
{
|
||||
ByteBuffer[] uncompressedBuffers = new ByteBuffer[3];
|
||||
uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1));
|
||||
uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1));
|
||||
final int totalContentBytesCount = countRemaining(uncompressedBuffers);
|
||||
final String originalContentString = asString(uncompressedBuffers);
|
||||
final Throwable expectedError = new Throwable("HttpInput idle timeout");
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[3];
|
||||
buffers[0] = gzipByteBuffer(uncompressedBuffers[0]);
|
||||
buffers[1] = gzipByteBuffer(uncompressedBuffers[1]);
|
||||
buffers[2] = gzipByteBuffer(uncompressedBuffers[2]);
|
||||
|
||||
AtomicReference<ContentProducer> ref = new AtomicReference<>();
|
||||
ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible());
|
||||
ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel));
|
||||
ref.set(contentProducer);
|
||||
contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32));
|
||||
|
||||
Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer);
|
||||
assertThat(error, is(expectedError));
|
||||
}
|
||||
|
||||
private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, int totalContentCount, ContentProducer contentProducer)
|
||||
{
|
||||
int readBytes = 0;
|
||||
int nextContentCount = 0;
|
||||
String consumedString = "";
|
||||
Throwable error = null;
|
||||
while (true)
|
||||
{
|
||||
HttpInput.Content content = contentProducer.nextContent();
|
||||
nextContentCount++;
|
||||
|
||||
if (content.isSpecial())
|
||||
{
|
||||
if (content.isEof())
|
||||
break;
|
||||
error = content.getError();
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] b = new byte[content.remaining()];
|
||||
content.getByteBuffer().get(b);
|
||||
consumedString += new String(b, StandardCharsets.ISO_8859_1);
|
||||
|
||||
readBytes += b.length;
|
||||
}
|
||||
assertThat(readBytes, is(totalContentBytesCount));
|
||||
assertThat(nextContentCount, is(totalContentCount));
|
||||
assertThat(consumedString, is(originalContentString));
|
||||
return error;
|
||||
}
|
||||
|
||||
private static int countRemaining(ByteBuffer[] byteBuffers)
|
||||
{
|
||||
int total = 0;
|
||||
for (ByteBuffer byteBuffer : byteBuffers)
|
||||
{
|
||||
total += byteBuffer.remaining();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static String asString(ByteBuffer[] buffers)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ByteBuffer buffer : buffers)
|
||||
{
|
||||
byte[] b = new byte[buffer.remaining()];
|
||||
buffer.duplicate().get(b);
|
||||
sb.append(new String(b, StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
GZIPOutputStream output = new GZIPOutputStream(baos);
|
||||
|
||||
byte[] b = new byte[uncompressedBuffer.remaining()];
|
||||
uncompressedBuffer.get(b);
|
||||
output.write(b);
|
||||
|
||||
output.close();
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private interface ContentListener
|
||||
{
|
||||
void onContent();
|
||||
}
|
||||
|
||||
private static class ArrayDelayedHttpChannel extends HttpChannel
|
||||
{
|
||||
private final ByteBuffer[] byteBuffers;
|
||||
private final HttpInput.Content finalContent;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
private final ContentListener contentListener;
|
||||
private int counter;
|
||||
private volatile HttpInput.Content nextContent;
|
||||
|
||||
public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, ContentListener contentListener)
|
||||
{
|
||||
super(new MockConnector(), new HttpConfiguration(), null, null);
|
||||
this.byteBuffers = new ByteBuffer[byteBuffers.length];
|
||||
this.finalContent = finalContent;
|
||||
this.scheduledExecutorService = scheduledExecutorService;
|
||||
this.contentListener = contentListener;
|
||||
for (int i = 0; i < byteBuffers.length; i++)
|
||||
{
|
||||
this.byteBuffers[i] = byteBuffers[i].duplicate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
if (nextContent != null)
|
||||
return true;
|
||||
scheduledExecutorService.schedule(() ->
|
||||
{
|
||||
if (byteBuffers.length > counter)
|
||||
nextContent = new HttpInput.Content(byteBuffers[counter++]);
|
||||
else
|
||||
nextContent = finalContent;
|
||||
contentListener.onContent();
|
||||
}, 50, TimeUnit.MILLISECONDS);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
HttpInput.Content result = nextContent;
|
||||
nextContent = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
nextContent = null;
|
||||
counter = byteBuffers.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,735 +0,0 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import javax.servlet.ReadListener;
|
||||
|
||||
import org.eclipse.jetty.server.HttpChannelState.Action;
|
||||
import org.eclipse.jetty.server.HttpInput.Content;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.eclipse.jetty.server.HttpInput.EARLY_EOF_CONTENT;
|
||||
import static org.eclipse.jetty.server.HttpInput.EOF_CONTENT;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* this tests HttpInput and its interaction with HttpChannelState
|
||||
*/
|
||||
|
||||
public class HttpInputAsyncStateTest
|
||||
{
|
||||
|
||||
private static final Queue<String> __history = new LinkedBlockingQueue<>();
|
||||
private ByteBuffer _expected = BufferUtil.allocate(16 * 1024);
|
||||
private boolean _eof;
|
||||
private boolean _noReadInDataAvailable;
|
||||
private boolean _completeInOnDataAvailable;
|
||||
|
||||
private final ReadListener _listener = new ReadListener()
|
||||
{
|
||||
@Override
|
||||
public void onError(Throwable t)
|
||||
{
|
||||
__history.add("onError:" + t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException
|
||||
{
|
||||
__history.add("onDataAvailable");
|
||||
if (!_noReadInDataAvailable && readAvailable() && _completeInOnDataAvailable)
|
||||
{
|
||||
__history.add("complete");
|
||||
_state.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException
|
||||
{
|
||||
__history.add("onAllDataRead");
|
||||
}
|
||||
};
|
||||
private HttpInput _in;
|
||||
HttpChannelState _state;
|
||||
|
||||
public static class TContent extends HttpInput.Content
|
||||
{
|
||||
public TContent(String content)
|
||||
{
|
||||
super(BufferUtil.toBuffer(content));
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void before()
|
||||
{
|
||||
_noReadInDataAvailable = false;
|
||||
_in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
|
||||
{
|
||||
@Override
|
||||
public void onAsyncWaitForContent()
|
||||
{
|
||||
__history.add("onAsyncWaitForContent");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
{
|
||||
@Override
|
||||
public void onReadUnready()
|
||||
{
|
||||
super.onReadUnready();
|
||||
__history.add("onReadUnready");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContentAdded()
|
||||
{
|
||||
boolean wake = super.onContentAdded();
|
||||
__history.add("onReadPossible " + wake);
|
||||
return wake;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReadReady()
|
||||
{
|
||||
boolean wake = super.onReadReady();
|
||||
__history.add("onReadReady " + wake);
|
||||
return wake;
|
||||
}
|
||||
})
|
||||
{
|
||||
@Override
|
||||
public void wake()
|
||||
{
|
||||
__history.add("wake");
|
||||
}
|
||||
};
|
||||
|
||||
_state = _in.getHttpChannelState();
|
||||
__history.clear();
|
||||
}
|
||||
|
||||
private void check(String... history)
|
||||
{
|
||||
if (history == null || history.length == 0)
|
||||
assertThat(__history, empty());
|
||||
else
|
||||
assertThat(__history.toArray(new String[__history.size()]), Matchers.arrayContaining(history));
|
||||
__history.clear();
|
||||
}
|
||||
|
||||
private void wake()
|
||||
{
|
||||
handle(null);
|
||||
}
|
||||
|
||||
private void handle()
|
||||
{
|
||||
handle(null);
|
||||
}
|
||||
|
||||
private void handle(Runnable run)
|
||||
{
|
||||
Action action = _state.handling();
|
||||
loop:
|
||||
while (true)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case DISPATCH:
|
||||
if (run == null)
|
||||
fail("Run is null during DISPATCH");
|
||||
run.run();
|
||||
break;
|
||||
|
||||
case READ_CALLBACK:
|
||||
_in.run();
|
||||
break;
|
||||
|
||||
case TERMINATED:
|
||||
case WAIT:
|
||||
break loop;
|
||||
|
||||
case COMPLETE:
|
||||
__history.add("COMPLETE");
|
||||
break;
|
||||
|
||||
case READ_REGISTER:
|
||||
_state.getHttpChannel().onAsyncWaitForContent();
|
||||
break;
|
||||
|
||||
default:
|
||||
fail("Bad Action: " + action);
|
||||
}
|
||||
action = _state.unhandle();
|
||||
}
|
||||
}
|
||||
|
||||
private void deliver(Content... content)
|
||||
{
|
||||
if (content != null)
|
||||
{
|
||||
for (Content c : content)
|
||||
{
|
||||
if (c == EOF_CONTENT)
|
||||
{
|
||||
_in.eof();
|
||||
_eof = true;
|
||||
}
|
||||
else if (c == HttpInput.EARLY_EOF_CONTENT)
|
||||
{
|
||||
_in.earlyEOF();
|
||||
_eof = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_in.addContent(c);
|
||||
BufferUtil.append(_expected, c.getByteBuffer().slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean readAvailable() throws IOException
|
||||
{
|
||||
int len = 0;
|
||||
try
|
||||
{
|
||||
while (_in.isReady())
|
||||
{
|
||||
int b = _in.read();
|
||||
|
||||
if (b < 0)
|
||||
{
|
||||
if (len > 0)
|
||||
__history.add("read " + len);
|
||||
__history.add("read -1");
|
||||
assertTrue(BufferUtil.isEmpty(_expected));
|
||||
assertTrue(_eof);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
len++;
|
||||
assertFalse(BufferUtil.isEmpty(_expected));
|
||||
int a = 0xff & _expected.get();
|
||||
assertThat(b, equalTo(a));
|
||||
}
|
||||
}
|
||||
__history.add("read " + len);
|
||||
assertTrue(BufferUtil.isEmpty(_expected));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (len > 0)
|
||||
__history.add("read " + len);
|
||||
__history.add("read " + e);
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after()
|
||||
{
|
||||
assertThat(__history.poll(), Matchers.nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialEmptyListenInHandle() throws Exception
|
||||
{
|
||||
deliver(EOF_CONTENT);
|
||||
check();
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady false");
|
||||
});
|
||||
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialEmptyListenAfterHandle() throws Exception
|
||||
{
|
||||
deliver(EOF_CONTENT);
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenInHandleEmpty() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(EOF_CONTENT);
|
||||
check("onReadPossible true");
|
||||
handle();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyListenAfterHandle() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
deliver(EOF_CONTENT);
|
||||
check();
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenAfterHandleEmpty() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onAsyncWaitForContent", "onReadUnready");
|
||||
|
||||
deliver(EOF_CONTENT);
|
||||
check("onReadPossible true");
|
||||
|
||||
handle();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialEarlyEOFListenInHandle() throws Exception
|
||||
{
|
||||
deliver(EARLY_EOF_CONTENT);
|
||||
check();
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady false");
|
||||
});
|
||||
|
||||
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialEarlyEOFListenAfterHandle() throws Exception
|
||||
{
|
||||
deliver(EARLY_EOF_CONTENT);
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenInHandleEarlyEOF() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(EARLY_EOF_CONTENT);
|
||||
check("onReadPossible true");
|
||||
handle();
|
||||
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEarlyEOFListenAfterHandle() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
deliver(EARLY_EOF_CONTENT);
|
||||
check();
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenAfterHandleEarlyEOF() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onAsyncWaitForContent", "onReadUnready");
|
||||
|
||||
deliver(EARLY_EOF_CONTENT);
|
||||
check("onReadPossible true");
|
||||
|
||||
handle();
|
||||
check("onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialAllContentListenInHandle() throws Exception
|
||||
{
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check();
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady false");
|
||||
});
|
||||
|
||||
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialAllContentListenAfterHandle() throws Exception
|
||||
{
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenInHandleAllContent() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
handle();
|
||||
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllContentListenAfterHandle() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check();
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenAfterHandleAllContent() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onAsyncWaitForContent", "onReadUnready");
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
|
||||
handle();
|
||||
check("onDataAvailable", "read 5", "read -1", "onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialIncompleteContentListenInHandle() throws Exception
|
||||
{
|
||||
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
|
||||
check();
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady false");
|
||||
});
|
||||
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read org.eclipse.jetty.io.EofException: Early EOF",
|
||||
"onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialPartialContentListenAfterHandle() throws Exception
|
||||
{
|
||||
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read org.eclipse.jetty.io.EofException: Early EOF",
|
||||
"onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenInHandlePartialContent() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
handle();
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read org.eclipse.jetty.io.EofException: Early EOF",
|
||||
"onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialContentListenAfterHandle() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
|
||||
check();
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadReady true", "wake");
|
||||
wake();
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read org.eclipse.jetty.io.EofException: Early EOF",
|
||||
"onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenAfterHandlePartialContent() throws Exception
|
||||
{
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
check();
|
||||
});
|
||||
|
||||
_in.setReadListener(_listener);
|
||||
check("onAsyncWaitForContent", "onReadUnready");
|
||||
|
||||
deliver(new TContent("Hello"), EARLY_EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
|
||||
handle();
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read org.eclipse.jetty.io.EofException: Early EOF",
|
||||
"onError:org.eclipse.jetty.io.EofException: Early EOF");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAfterOnDataAvailable() throws Exception
|
||||
{
|
||||
_noReadInDataAvailable = true;
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
|
||||
handle();
|
||||
check("onDataAvailable");
|
||||
|
||||
readAvailable();
|
||||
check("wake", "read 5", "read -1");
|
||||
wake();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOnlyExpectedAfterOnDataAvailable() throws Exception
|
||||
{
|
||||
_noReadInDataAvailable = true;
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
|
||||
handle();
|
||||
check("onDataAvailable");
|
||||
|
||||
byte[] buffer = new byte[_expected.remaining()];
|
||||
assertThat(_in.read(buffer), equalTo(buffer.length));
|
||||
assertThat(new String(buffer), equalTo(BufferUtil.toString(_expected)));
|
||||
BufferUtil.clear(_expected);
|
||||
check();
|
||||
|
||||
assertTrue(_in.isReady());
|
||||
check();
|
||||
|
||||
assertThat(_in.read(), equalTo(-1));
|
||||
check("wake");
|
||||
|
||||
wake();
|
||||
check("onAllDataRead");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadAndCompleteInOnDataAvailable() throws Exception
|
||||
{
|
||||
_completeInOnDataAvailable = true;
|
||||
handle(() ->
|
||||
{
|
||||
_state.startAsync(null);
|
||||
_in.setReadListener(_listener);
|
||||
check("onReadUnready");
|
||||
});
|
||||
|
||||
check("onAsyncWaitForContent");
|
||||
|
||||
deliver(new TContent("Hello"), EOF_CONTENT);
|
||||
check("onReadPossible true", "onReadPossible false");
|
||||
|
||||
handle(() ->
|
||||
{
|
||||
__history.add(_state.getState().toString());
|
||||
});
|
||||
System.err.println(__history);
|
||||
check(
|
||||
"onDataAvailable",
|
||||
"read 5",
|
||||
"read -1",
|
||||
"complete",
|
||||
"COMPLETE"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,614 +0,0 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under
|
||||
// the terms of the Eclipse Public License 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// This Source Code may also be made available under the following
|
||||
// Secondary Licenses when the conditions for such availability set
|
||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
||||
// the Apache License v2.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.server;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import javax.servlet.ReadListener;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class HttpInputTest
|
||||
{
|
||||
private final Queue<String> _history = new LinkedBlockingQueue<>();
|
||||
private final Queue<String> _fillAndParseSimulate = new LinkedBlockingQueue<>();
|
||||
private final ReadListener _listener = new ReadListener()
|
||||
{
|
||||
@Override
|
||||
public void onError(Throwable t)
|
||||
{
|
||||
_history.add("l.onError:" + t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException
|
||||
{
|
||||
_history.add("l.onDataAvailable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException
|
||||
{
|
||||
_history.add("l.onAllDataRead");
|
||||
}
|
||||
};
|
||||
private HttpInput _in;
|
||||
|
||||
public class TContent extends HttpInput.Content
|
||||
{
|
||||
private final String _content;
|
||||
|
||||
public TContent(String content)
|
||||
{
|
||||
super(BufferUtil.toBuffer(content));
|
||||
_content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
_history.add("Content succeeded " + _content);
|
||||
super.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
_history.add("Content failed " + _content);
|
||||
super.failed(x);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestHttpInput extends HttpInput
|
||||
{
|
||||
public TestHttpInput(HttpChannelState state)
|
||||
{
|
||||
super(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void produceContent() throws IOException
|
||||
{
|
||||
_history.add("produceContent " + _fillAndParseSimulate.size());
|
||||
|
||||
for (String s = _fillAndParseSimulate.poll(); s != null; s = _fillAndParseSimulate.poll())
|
||||
{
|
||||
if ("_EOF_".equals(s))
|
||||
_in.eof();
|
||||
else
|
||||
_in.addContent(new TContent(s));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void blockForContent() throws IOException
|
||||
{
|
||||
_history.add("blockForContent");
|
||||
super.blockForContent();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestHttpChannelState extends HttpChannelState
|
||||
{
|
||||
private boolean _fakeAsyncState;
|
||||
|
||||
public TestHttpChannelState(HttpChannel channel)
|
||||
{
|
||||
super(channel);
|
||||
}
|
||||
|
||||
public boolean isFakeAsyncState()
|
||||
{
|
||||
return _fakeAsyncState;
|
||||
}
|
||||
|
||||
public void setFakeAsyncState(boolean fakeAsyncState)
|
||||
{
|
||||
_fakeAsyncState = fakeAsyncState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncStarted()
|
||||
{
|
||||
if (isFakeAsyncState())
|
||||
return true;
|
||||
return super.isAsyncStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadUnready()
|
||||
{
|
||||
_history.add("s.onReadUnready");
|
||||
super.onReadUnready();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReadPossible()
|
||||
{
|
||||
_history.add("s.onReadPossible");
|
||||
return super.onReadPossible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContentAdded()
|
||||
{
|
||||
_history.add("s.onDataAvailable");
|
||||
return super.onContentAdded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReadReady()
|
||||
{
|
||||
_history.add("s.onReadReady");
|
||||
return super.onReadReady();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void before()
|
||||
{
|
||||
_in = new TestHttpInput(new TestHttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
|
||||
{
|
||||
@Override
|
||||
public void onAsyncWaitForContent()
|
||||
{
|
||||
_history.add("asyncReadInterested");
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after()
|
||||
{
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmpty() throws Exception
|
||||
{
|
||||
assertThat(_in.available(), equalTo(0));
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead() throws Exception
|
||||
{
|
||||
_in.addContent(new TContent("AB"));
|
||||
_in.addContent(new TContent("CD"));
|
||||
_fillAndParseSimulate.offer("EF");
|
||||
_fillAndParseSimulate.offer("GH");
|
||||
assertThat(_in.available(), equalTo(2));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
|
||||
assertThat(_in.getContentConsumed(), equalTo(0L));
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
assertThat(_in.getContentConsumed(), equalTo(1L));
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
assertThat(_in.getContentConsumed(), equalTo(2L));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'C'));
|
||||
assertThat(_in.read(), equalTo((int)'D'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'E'));
|
||||
assertThat(_in.read(), equalTo((int)'F'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("produceContent 2"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded EF"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'G'));
|
||||
assertThat(_in.read(), equalTo((int)'H'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded GH"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.getContentConsumed(), equalTo(8L));
|
||||
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingRead() throws Exception
|
||||
{
|
||||
new Thread(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(500);
|
||||
_in.addContent(new TContent("AB"));
|
||||
}
|
||||
catch (Throwable th)
|
||||
{
|
||||
th.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("blockForContent"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadEOF() throws Exception
|
||||
{
|
||||
_in.addContent(new TContent("AB"));
|
||||
_in.addContent(new TContent("CD"));
|
||||
_in.eof();
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.available(), equalTo(2));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'C'));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.read(), equalTo((int)'D'));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
|
||||
assertThat(_in.read(), equalTo(-1));
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadEarlyEOF() throws Exception
|
||||
{
|
||||
_in.addContent(new TContent("AB"));
|
||||
_in.addContent(new TContent("CD"));
|
||||
_in.earlyEOF();
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.available(), equalTo(2));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'C'));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.read(), equalTo((int)'D'));
|
||||
|
||||
assertThrows(EOFException.class, () -> _in.read());
|
||||
assertTrue(_in.isFinished());
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingEOF() throws Exception
|
||||
{
|
||||
new Thread(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(500);
|
||||
_in.eof();
|
||||
}
|
||||
catch (Throwable th)
|
||||
{
|
||||
th.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.read(), equalTo(-1));
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("blockForContent"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncEmpty() throws Exception
|
||||
{
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncRead() throws Exception
|
||||
{
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.addContent(new TContent("AB"));
|
||||
_fillAndParseSimulate.add("CD");
|
||||
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
_in.run();
|
||||
assertThat(_history.poll(), equalTo("l.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_history.poll(), equalTo("produceContent 1"));
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo((int)'C'));
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.read(), equalTo((int)'D'));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncEOF() throws Exception
|
||||
{
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.eof();
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.read(), equalTo(-1));
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncReadEOF() throws Exception
|
||||
{
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.addContent(new TContent("AB"));
|
||||
_fillAndParseSimulate.add("_EOF_");
|
||||
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.run();
|
||||
assertThat(_history.poll(), equalTo("l.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_in.read(), equalTo((int)'B'));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_history.poll(), equalTo("produceContent 1"));
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isFinished(), equalTo(false));
|
||||
assertThat(_in.read(), equalTo(-1));
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncError() throws Exception
|
||||
{
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), equalTo("s.onReadUnready"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(false));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.failed(new TimeoutException());
|
||||
assertThat(_history.poll(), equalTo("s.onDataAvailable"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
_in.run();
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
assertThat(_history.poll(), equalTo("l.onError:java.util.concurrent.TimeoutException"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
|
||||
assertThat(_in.isReady(), equalTo(true));
|
||||
|
||||
IOException e = assertThrows(IOException.class, () -> _in.read());
|
||||
assertThat(e.getCause(), instanceOf(TimeoutException.class));
|
||||
assertThat(_in.isFinished(), equalTo(true));
|
||||
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetListenerWithNull() throws Exception
|
||||
{
|
||||
//test can't be null
|
||||
assertThrows(NullPointerException.class, () ->
|
||||
{
|
||||
_in.setReadListener(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetListenerNotAsync() throws Exception
|
||||
{
|
||||
//test not async
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
{
|
||||
_in.setReadListener(_listener);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetListenerAlreadySet() throws Exception
|
||||
{
|
||||
//set up a listener
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true);
|
||||
_in.setReadListener(_listener);
|
||||
//throw away any events generated by setting the listener
|
||||
_history.clear();
|
||||
((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false);
|
||||
//now test that you can't set another listener
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
{
|
||||
_in.setReadListener(_listener);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecycle() throws Exception
|
||||
{
|
||||
testAsyncRead();
|
||||
_in.recycle();
|
||||
testAsyncRead();
|
||||
_in.recycle();
|
||||
testReadEOF();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumeAll() throws Exception
|
||||
{
|
||||
_in.addContent(new TContent("AB"));
|
||||
_in.addContent(new TContent("CD"));
|
||||
_fillAndParseSimulate.offer("EF");
|
||||
_fillAndParseSimulate.offer("GH");
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
|
||||
assertFalse(_in.consumeAll());
|
||||
assertThat(_in.getContentConsumed(), equalTo(8L));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), equalTo("produceContent 2"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded EF"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded GH"));
|
||||
assertThat(_history.poll(), equalTo("produceContent 0"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumeAllEOF() throws Exception
|
||||
{
|
||||
_in.addContent(new TContent("AB"));
|
||||
_in.addContent(new TContent("CD"));
|
||||
_fillAndParseSimulate.offer("EF");
|
||||
_fillAndParseSimulate.offer("GH");
|
||||
_fillAndParseSimulate.offer("_EOF_");
|
||||
assertThat(_in.read(), equalTo((int)'A'));
|
||||
|
||||
assertTrue(_in.consumeAll());
|
||||
assertThat(_in.getContentConsumed(), equalTo(8L));
|
||||
|
||||
assertThat(_history.poll(), equalTo("Content succeeded AB"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded CD"));
|
||||
assertThat(_history.poll(), equalTo("produceContent 3"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded EF"));
|
||||
assertThat(_history.poll(), equalTo("Content succeeded GH"));
|
||||
assertThat(_history.poll(), nullValue());
|
||||
}
|
||||
}
|
@ -48,11 +48,41 @@ public class HttpWriterTest
|
||||
|
||||
HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null)
|
||||
{
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufferPool getByteBufferPool()
|
||||
{
|
||||
return pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
_httpOut = new HttpOutput(channel)
|
||||
|
@ -175,7 +175,38 @@ public class ResponseTest
|
||||
{
|
||||
_channelError = failure;
|
||||
}
|
||||
});
|
||||
})
|
||||
{
|
||||
@Override
|
||||
public boolean needContent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInput.Content produceContent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
@ -488,21 +488,8 @@ public class ServletHandler extends ScopedHandler
|
||||
FilterChain chain = null;
|
||||
|
||||
// find the servlet
|
||||
if (target.startsWith("/"))
|
||||
{
|
||||
if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0)
|
||||
chain = getFilterChain(baseRequest, target, servletHolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (servletHolder != null)
|
||||
{
|
||||
if (_filterMappings != null && _filterMappings.length > 0)
|
||||
{
|
||||
chain = getFilterChain(baseRequest, null, servletHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0)
|
||||
chain = getFilterChain(baseRequest, target.startsWith("/") ? target : null, servletHolder);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("chain={}", chain);
|
||||
@ -561,6 +548,7 @@ public class ServletHandler extends ScopedHandler
|
||||
|
||||
private FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder)
|
||||
{
|
||||
Objects.requireNonNull(servletHolder);
|
||||
String key = pathInContext == null ? servletHolder.getName() : pathInContext;
|
||||
int dispatch = FilterMapping.dispatch(baseRequest.getDispatcherType());
|
||||
|
||||
@ -576,7 +564,7 @@ public class ServletHandler extends ScopedHandler
|
||||
// The mappings lists have been reversed to make this simple and fast.
|
||||
FilterChain chain = null;
|
||||
|
||||
if (servletHolder != null && _filterNameMappings != null && !_filterNameMappings.isEmpty())
|
||||
if (_filterNameMappings != null && !_filterNameMappings.isEmpty())
|
||||
{
|
||||
if (_wildFilterNameMappings != null)
|
||||
for (FilterMapping mapping : _wildFilterNameMappings)
|
||||
@ -1658,6 +1646,7 @@ public class ServletHandler extends ScopedHandler
|
||||
|
||||
ChainEnd(ServletHolder holder)
|
||||
{
|
||||
Objects.requireNonNull(holder);
|
||||
_servletHolder = holder;
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<target>
|
||||
<copy file="${project.build.directory}/deps.txt" tofile="${project.build.directory}/deps-orig.txt" />
|
||||
<!-- regex the deps with classifiers first -->
|
||||
<replaceregexp file="${project.build.directory}/deps.txt" match=" *(.*):(.*):jar:(.*):(.*):.*$" replace="maven://\1/\2/\4/jar/\3|lib/jnr/\2-\4-\3.jar" byline="true" />
|
||||
@ -88,7 +88,7 @@
|
||||
<fileset file="${project.build.directory}/deps.txt" />
|
||||
<fileset file="src/main/config-template/modules/unixsocket-suffix.mod" />
|
||||
</concat>
|
||||
</tasks>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
@ -144,7 +144,7 @@ public class JarFileResource extends JarResource
|
||||
String fileUrl = _urlString.substring(4, _urlString.length() - 2);
|
||||
try
|
||||
{
|
||||
return newResource(fileUrl).exists();
|
||||
return _directory = newResource(fileUrl).exists();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -236,15 +236,10 @@ public class JarFileResource extends JarResource
|
||||
return _exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the represented resource is a container/directory.
|
||||
* If the resource is not a file, resources ending with "/" are
|
||||
* considered directories.
|
||||
*/
|
||||
@Override
|
||||
public boolean isDirectory()
|
||||
{
|
||||
return _urlString.endsWith("/") || exists() && _directory;
|
||||
return exists() && _directory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -320,8 +320,6 @@ public abstract class Resource implements ResourceFactory, Closeable
|
||||
|
||||
/**
|
||||
* @return true if the represented resource is a container/directory.
|
||||
* if the resource is not a file, resources ending with "/" are
|
||||
* considered directories.
|
||||
*/
|
||||
public abstract boolean isDirectory();
|
||||
|
||||
@ -412,7 +410,7 @@ public abstract class Resource implements ResourceFactory, Closeable
|
||||
|
||||
/**
|
||||
* Returns the resource contained inside the current resource with the
|
||||
* given name.
|
||||
* given name, which may or may not exist.
|
||||
*
|
||||
* @param path The path segment to add, which is not encoded
|
||||
* @return the Resource for the resolved path within this Resource, never null
|
||||
|
@ -62,8 +62,19 @@ public class ResourceCollection extends Resource
|
||||
* @param resources the resources to be added to collection
|
||||
*/
|
||||
public ResourceCollection(Resource... resources)
|
||||
{
|
||||
this(Arrays.asList(resources));
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new resource collection.
|
||||
*
|
||||
* @param resources the resources to be added to collection
|
||||
*/
|
||||
public ResourceCollection(Collection<Resource> resources)
|
||||
{
|
||||
_resources = new ArrayList<>();
|
||||
|
||||
for (Resource r : resources)
|
||||
{
|
||||
if (r == null)
|
||||
@ -82,17 +93,6 @@ public class ResourceCollection extends Resource
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new resource collection.
|
||||
*
|
||||
* @param resources the resources to be added to collection
|
||||
*/
|
||||
public ResourceCollection(Collection<Resource> resources)
|
||||
{
|
||||
_resources = new ArrayList<>();
|
||||
_resources.addAll(resources);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new resource collection.
|
||||
*
|
||||
@ -226,8 +226,16 @@ public class ResourceCollection extends Resource
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a path to the resource collection.
|
||||
* @param path The path segment to add
|
||||
* @return The contained resource (found first) in the collection of resources
|
||||
* @return The resulting resource(s) :
|
||||
* <ul>
|
||||
* <li>is a file that exists in at least one of the collection, then the first one found is returned</li>
|
||||
* <li>is a directory that exists in at exactly one of the collection, then that directory resource is returned </li>
|
||||
* <li>is a directory that exists in several of the collection, then a ResourceCollection of those directories is returned</li>
|
||||
* <li>do not exist in any of the collection, then a new non existent resource relative to the first in the collection is returned.</li>
|
||||
* </ul>
|
||||
* @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed against any of the collection
|
||||
*/
|
||||
@Override
|
||||
public Resource addPath(String path) throws IOException
|
||||
@ -247,27 +255,28 @@ public class ResourceCollection extends Resource
|
||||
ArrayList<Resource> resources = null;
|
||||
|
||||
// Attempt a simple (single) Resource lookup that exists
|
||||
Resource addedResource = null;
|
||||
for (Resource res : _resources)
|
||||
{
|
||||
Resource r = res.addPath(path);
|
||||
if (!r.isDirectory() && r.exists())
|
||||
{
|
||||
// Return simple (non-directory) Resource
|
||||
return r;
|
||||
}
|
||||
|
||||
addedResource = res.addPath(path);
|
||||
if (!addedResource.exists())
|
||||
continue;
|
||||
if (!addedResource.isDirectory())
|
||||
return addedResource; // Return simple (non-directory) Resource
|
||||
if (resources == null)
|
||||
{
|
||||
resources = new ArrayList<>();
|
||||
}
|
||||
resources.add(addedResource);
|
||||
}
|
||||
|
||||
resources.add(r);
|
||||
if (resources == null)
|
||||
{
|
||||
if (addedResource != null)
|
||||
return addedResource; // This will not exist
|
||||
return EmptyResource.INSTANCE;
|
||||
}
|
||||
|
||||
if (resources.size() == 1)
|
||||
{
|
||||
return resources.get(0);
|
||||
}
|
||||
|
||||
return new ResourceCollection(resources);
|
||||
}
|
||||
@ -384,7 +393,6 @@ public class ResourceCollection extends Resource
|
||||
public boolean isDirectory()
|
||||
{
|
||||
assertResourcesSet();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -128,11 +128,6 @@ public class URLResource extends Resource
|
||||
return _in != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the represented resource is a container/directory.
|
||||
* If the resource is not a file, resources ending with "/" are
|
||||
* considered directories.
|
||||
*/
|
||||
@Override
|
||||
public boolean isDirectory()
|
||||
{
|
||||
|
@ -35,7 +35,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.emptyArray;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -189,7 +188,7 @@ public class ResourceCollectionTest
|
||||
|
||||
assertThat(Arrays.asList(rc1.list()), contains("1.txt", "2.txt", "3.txt", "dir/"));
|
||||
assertThat(Arrays.asList(rc1.addPath("dir").list()), contains("1.txt", "2.txt", "3.txt"));
|
||||
assertThat(rc1.addPath("unknown").list(), emptyArray());
|
||||
assertThat(rc1.addPath("unknown").list(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -215,15 +215,15 @@ public class ResourceTest
|
||||
cases.addCase(new Scenario(tdata1, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
|
||||
cases.addCase(new Scenario(tdata2, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
|
||||
|
||||
cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, DIR));
|
||||
cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, DIR));
|
||||
cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, !DIR));
|
||||
cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, !DIR));
|
||||
|
||||
String urlRef = cases.uriRef.toASCIIString();
|
||||
Scenario zdata = new Scenario("jar:" + urlRef + "TestData/test.zip!/", EXISTS, DIR);
|
||||
cases.addCase(zdata);
|
||||
|
||||
cases.addCase(new Scenario(zdata, "Unknown", !EXISTS, !DIR));
|
||||
cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, DIR));
|
||||
cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, !DIR));
|
||||
|
||||
cases.addCase(new Scenario(zdata, "subdir", EXISTS, DIR));
|
||||
cases.addCase(new Scenario(zdata, "/subdir/", EXISTS, DIR));
|
||||
|
@ -38,6 +38,7 @@ import org.eclipse.jetty.http.pathmap.ServletPathSpec;
|
||||
import org.eclipse.jetty.security.ConstraintAware;
|
||||
import org.eclipse.jetty.security.ConstraintMapping;
|
||||
import org.eclipse.jetty.security.authentication.FormAuthenticator;
|
||||
import org.eclipse.jetty.server.session.SessionHandler;
|
||||
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.FilterMapping;
|
||||
@ -745,7 +746,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
|
||||
case WebFragment:
|
||||
{
|
||||
//a web-fragment set the value, all web-fragments must have the same value
|
||||
if (!context.getSessionHandler().getSessionCookieConfig().getName().equals(name))
|
||||
if (!name.equals(SessionHandler.getSessionCookieName(context.getSessionHandler().getSessionCookieConfig())))
|
||||
throw new IllegalStateException("Conflicting cookie-config name " + name + " in " + descriptor.getResource());
|
||||
break;
|
||||
}
|
||||
@ -821,7 +822,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
|
||||
case WebFragment:
|
||||
{
|
||||
//a web-fragment set the value, all web-fragments must have the same value
|
||||
if (!context.getSessionHandler().getSessionCookieConfig().getPath().equals(path))
|
||||
if (!path.equals(context.getSessionHandler().getSessionCookieConfig().getPath()))
|
||||
throw new IllegalStateException("Conflicting cookie-config path " + path + " in " + descriptor.getResource());
|
||||
break;
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import java.util.Collections;
|
||||
import java.util.EventListener;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.core.extensions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -28,10 +29,13 @@ import org.eclipse.jetty.client.HttpRequest;
|
||||
import org.eclipse.jetty.client.HttpResponse;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.websocket.core.CoreSession;
|
||||
import org.eclipse.jetty.websocket.core.ExtensionConfig;
|
||||
import org.eclipse.jetty.websocket.core.Frame;
|
||||
import org.eclipse.jetty.websocket.core.FrameHandler;
|
||||
import org.eclipse.jetty.websocket.core.OpCode;
|
||||
import org.eclipse.jetty.websocket.core.TestFrameHandler;
|
||||
import org.eclipse.jetty.websocket.core.WebSocketServer;
|
||||
import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest;
|
||||
@ -291,7 +295,8 @@ public class PerMessageDeflaterBufferSizeTest
|
||||
|
||||
// We should now only be able to send this message in multiple frames as it exceeds deflate_buffer_size.
|
||||
String message = "0123456789";
|
||||
clientHandler.sendText(message);
|
||||
ByteBuffer payload = toBuffer(message, true);
|
||||
clientHandler.getCoreSession().sendFrame(new Frame(OpCode.TEXT, payload), Callback.NOOP, false);
|
||||
|
||||
// Verify the frame has been fragmented into multiple parts.
|
||||
int numFrames = 0;
|
||||
@ -315,4 +320,18 @@ public class PerMessageDeflaterBufferSizeTest
|
||||
assertNull(serverHandler.getError());
|
||||
assertNull(clientHandler.getError());
|
||||
}
|
||||
|
||||
public ByteBuffer toBuffer(String string, boolean direct)
|
||||
{
|
||||
ByteBuffer buffer = BufferUtil.allocate(string.length(), direct);
|
||||
BufferUtil.clearToFill(buffer);
|
||||
BufferUtil.put(BufferUtil.toBuffer(string), buffer);
|
||||
BufferUtil.flipToFlush(buffer, 0);
|
||||
|
||||
// Sanity checks.
|
||||
assertThat(buffer.hasArray(), is(!direct));
|
||||
assertThat(BufferUtil.toString(buffer), is(string));
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,10 @@
|
||||
<filter>
|
||||
<filter-name>wsuf-test</filter-name>
|
||||
<filter-class>org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter</filter-class>
|
||||
<init-param>
|
||||
<param-name>jetty.websocket.WebSocketMapping</param-name>
|
||||
<param-value>jetty.websocket.defaultMapping</param-value>
|
||||
</init-param>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
|
@ -91,6 +91,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
|
||||
|
||||
private final ServletContextHandler contextHandler;
|
||||
private final WebSocketMapping webSocketMapping;
|
||||
private final WebSocketComponents components;
|
||||
private final FrameHandlerFactory frameHandlerFactory;
|
||||
private final Executor executor;
|
||||
private final Configuration.ConfigurationCustomizer customizer = new Configuration.ConfigurationCustomizer();
|
||||
@ -102,14 +103,15 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
|
||||
* Main entry point for {@link JettyWebSocketServletContainerInitializer}.
|
||||
*
|
||||
* @param webSocketMapping the {@link WebSocketMapping} that this container belongs to
|
||||
* @param webSocketComponents the {@link WebSocketComponents} instance to use
|
||||
* @param components the {@link WebSocketComponents} instance to use
|
||||
* @param executor the {@link Executor} to use
|
||||
*/
|
||||
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, Executor executor)
|
||||
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents components, Executor executor)
|
||||
{
|
||||
this.contextHandler = contextHandler;
|
||||
this.webSocketMapping = webSocketMapping;
|
||||
this.executor = executor;
|
||||
this.components = components;
|
||||
|
||||
// Ensure there is a FrameHandlerFactory
|
||||
JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class);
|
||||
@ -155,6 +157,11 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
|
||||
});
|
||||
}
|
||||
|
||||
public WebSocketComponents getWebSocketComponents()
|
||||
{
|
||||
return components;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor getExecutor()
|
||||
{
|
||||
|
@ -92,11 +92,9 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
|
||||
private static JettyWebSocketServerContainer initialize(ServletContextHandler context)
|
||||
{
|
||||
WebSocketComponents components = WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), context.getServletContext());
|
||||
WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY);
|
||||
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext());
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("configureContext {} {} {}", container, mapping, components);
|
||||
LOG.debug("initialize {} {}", container, components);
|
||||
|
||||
return container;
|
||||
}
|
||||
@ -105,6 +103,8 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain
|
||||
public void onStartup(Set<Class<?>> c, ServletContext context)
|
||||
{
|
||||
ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "Jetty WebSocket SCI");
|
||||
JettyWebSocketServletContainerInitializer.initialize(contextHandler);
|
||||
JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.initialize(contextHandler);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onStartup {}", container);
|
||||
}
|
||||
}
|
||||
|
@ -22,11 +22,9 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.DispatcherType;
|
||||
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
@ -39,7 +37,6 @@ import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.common.WebSocketSession;
|
||||
import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -54,22 +51,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ErrorCloseTest
|
||||
{
|
||||
private Server server = new Server();
|
||||
private WebSocketClient client = new WebSocketClient();
|
||||
private ThrowingSocket serverSocket = new ThrowingSocket();
|
||||
private CountDownLatch serverCloseListener = new CountDownLatch(1);
|
||||
private ServerConnector connector;
|
||||
private final Server server = new Server();
|
||||
private final WebSocketClient client = new WebSocketClient();
|
||||
private final ThrowingSocket serverSocket = new ThrowingSocket();
|
||||
private final CountDownLatch serverCloseListener = new CountDownLatch(1);
|
||||
private URI serverUri;
|
||||
|
||||
@BeforeEach
|
||||
public void start() throws Exception
|
||||
{
|
||||
connector = new ServerConnector(server);
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
|
||||
JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) ->
|
||||
{
|
||||
container.addMapping("/", (req, resp) -> serverSocket);
|
||||
@ -140,7 +135,7 @@ public class ErrorCloseTest
|
||||
serverSocket.methodsToThrow.add("onOpen");
|
||||
EventSocket clientSocket = new EventSocket();
|
||||
|
||||
try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class))
|
||||
try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class))
|
||||
{
|
||||
client.connect(clientSocket, serverUri).get(5, TimeUnit.SECONDS);
|
||||
assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS));
|
||||
|
@ -25,7 +25,6 @@ import java.net.ServerSocket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -33,7 +32,6 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import javax.servlet.DispatcherType;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
@ -49,7 +47,6 @@ import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint;
|
||||
import org.eclipse.jetty.websocket.tests.EchoSocket;
|
||||
import org.eclipse.jetty.websocket.tests.GetAuthHeaderEndpoint;
|
||||
import org.eclipse.jetty.websocket.tests.SimpleStatusServlet;
|
||||
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@ -73,7 +70,7 @@ public class ClientConnectTest
|
||||
{
|
||||
private Server server;
|
||||
private WebSocketClient client;
|
||||
private CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <E extends Throwable> E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher<Throwable> errorMatcher)
|
||||
@ -143,8 +140,6 @@ public class ClientConnectTest
|
||||
});
|
||||
});
|
||||
|
||||
context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
|
||||
|
||||
context.addServlet(new ServletHolder(new SimpleStatusServlet(404)), "/bogus");
|
||||
context.addServlet(new ServletHolder(new SimpleStatusServlet(200)), "/a-okay");
|
||||
context.addServlet(new ServletHolder(new InvalidUpgradeServlet()), "/invalid-upgrade/*");
|
||||
|
@ -25,12 +25,10 @@ import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.DispatcherType;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
@ -50,7 +48,6 @@ import org.eclipse.jetty.websocket.tests.ConnectMessageEndpoint;
|
||||
import org.eclipse.jetty.websocket.tests.EchoSocket;
|
||||
import org.eclipse.jetty.websocket.tests.ParamsEndpoint;
|
||||
import org.eclipse.jetty.websocket.tests.util.FutureWriteCallback;
|
||||
import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@ -104,10 +101,7 @@ public class WebSocketClientTest
|
||||
configuration.addMapping("/get-params", (req, resp) -> new ParamsEndpoint());
|
||||
});
|
||||
|
||||
context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
|
||||
|
||||
server.setHandler(context);
|
||||
|
||||
server.start();
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,6 @@ import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.websocket.core.Configuration;
|
||||
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
|
||||
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -78,34 +77,40 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebSocketUpgradeFilter.class);
|
||||
private static final AutoLock LOCK = new AutoLock();
|
||||
|
||||
/**
|
||||
* The init parameter name used to define {@link ServletContext} attribute used to share the {@link WebSocketMapping}.
|
||||
*/
|
||||
public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "jetty.websocket.WebSocketMapping";
|
||||
|
||||
/**
|
||||
* Return any {@link WebSocketUpgradeFilter} already present on the {@link ServletContext}.
|
||||
*
|
||||
* @param servletContext the {@link ServletContext} to use.
|
||||
* @return the configured default {@link WebSocketUpgradeFilter} instance.
|
||||
*/
|
||||
private static FilterHolder getFilter(ServletContext servletContext)
|
||||
{
|
||||
ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext));
|
||||
ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class);
|
||||
|
||||
for (FilterHolder holder : servletHandler.getFilters())
|
||||
{
|
||||
if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null)
|
||||
return holder;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the default WebSocketUpgradeFilter.
|
||||
*
|
||||
* <p>
|
||||
* This will return the default {@link WebSocketUpgradeFilter} on the
|
||||
* provided {@link ServletContext}, creating the filter if necessary.
|
||||
* Ensure a {@link WebSocketUpgradeFilter} is available on the provided {@link ServletContext},
|
||||
* a new filter will added if one does not already exist.
|
||||
* </p>
|
||||
* <p>
|
||||
* The default {@link WebSocketUpgradeFilter} is also available via
|
||||
* the {@link ServletContext} attribute named {@code org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter}
|
||||
* </p>
|
||||
*
|
||||
* @param servletContext the {@link ServletContext} to use
|
||||
* @return the configured default {@link WebSocketUpgradeFilter} instance
|
||||
* @param servletContext the {@link ServletContext} to use.
|
||||
* @return the configured default {@link WebSocketUpgradeFilter} instance.
|
||||
*/
|
||||
public static FilterHolder ensureFilter(ServletContext servletContext)
|
||||
{
|
||||
@ -132,8 +137,6 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
|
||||
}
|
||||
}
|
||||
|
||||
public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping.key";
|
||||
|
||||
private final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer();
|
||||
private WebSocketMapping mapping;
|
||||
|
||||
@ -174,10 +177,9 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable
|
||||
final ServletContext context = config.getServletContext();
|
||||
|
||||
String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM);
|
||||
if (mappingKey != null)
|
||||
mapping = WebSocketMapping.ensureMapping(context, mappingKey);
|
||||
else
|
||||
mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(context));
|
||||
if (mappingKey == null)
|
||||
throw new ServletException("the WebSocketMapping init param must be set");
|
||||
mapping = WebSocketMapping.ensureMapping(context, mappingKey);
|
||||
|
||||
String max = config.getInitParameter("idleTimeout");
|
||||
if (max == null)
|
||||
|
@ -63,7 +63,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
|
||||
public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey)
|
||||
{
|
||||
Object mappingObject = servletContext.getAttribute(mappingKey);
|
||||
|
||||
if (mappingObject != null)
|
||||
{
|
||||
if (mappingObject instanceof WebSocketMapping)
|
||||
@ -86,7 +85,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
|
||||
public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey)
|
||||
{
|
||||
WebSocketMapping mapping = getMapping(servletContext, mappingKey);
|
||||
|
||||
if (mapping == null)
|
||||
{
|
||||
mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(servletContext));
|
||||
@ -135,7 +133,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener
|
||||
throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]");
|
||||
}
|
||||
|
||||
public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping";
|
||||
public static final String DEFAULT_KEY = "jetty.websocket.defaultMapping";
|
||||
|
||||
private final PathMappings<Negotiator> mappings = new PathMappings<>();
|
||||
private final WebSocketComponents components;
|
||||
|
5
pom.xml
5
pom.xml
@ -28,6 +28,9 @@
|
||||
<websocket.api.version>1.1.2</websocket.api.version>
|
||||
<jsp.version>9.0.29</jsp.version>
|
||||
<infinispan.version>9.4.8.Final</infinispan.version>
|
||||
<infinispan.protostream.version>4.3.4.Final</infinispan.protostream.version>
|
||||
<gson.version>2.8.6</gson.version>
|
||||
<alpn.agent.version>2.0.10</alpn.agent.version>
|
||||
<hazelcast.version>4.0.1</hazelcast.version>
|
||||
<conscrypt.version>2.5.1</conscrypt.version>
|
||||
<asm.version>9.0</asm.version>
|
||||
@ -448,7 +451,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<version>3.0.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -36,6 +36,8 @@ import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ReadListener;
|
||||
@ -71,8 +73,11 @@ import org.eclipse.jetty.server.HttpInput.Content;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler.Context;
|
||||
import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.compression.CompressionPool;
|
||||
import org.eclipse.jetty.util.compression.InflaterPool;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
@ -87,6 +92,7 @@ import static org.eclipse.jetty.http.client.Transport.HTTP;
|
||||
import static org.eclipse.jetty.util.BufferUtil.toArray;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -776,10 +782,18 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
throw new IllegalStateException();
|
||||
if (input.read() != 'X')
|
||||
throw new IllegalStateException();
|
||||
if (!input.isReady())
|
||||
throw new IllegalStateException();
|
||||
if (input.read() != -1)
|
||||
throw new IllegalStateException();
|
||||
if (input.isReady())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (input.read() != -1)
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
@ -1204,8 +1218,8 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
{
|
||||
case 0:
|
||||
// null transform
|
||||
if (content.isEmpty())
|
||||
state++;
|
||||
content.skip(content.remaining());
|
||||
state++;
|
||||
return null;
|
||||
|
||||
case 1:
|
||||
@ -1254,7 +1268,7 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
return content;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1300,7 +1314,6 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
|
||||
String expected =
|
||||
"S0" +
|
||||
"S1" +
|
||||
"S2" +
|
||||
"S3S3" +
|
||||
@ -1345,6 +1358,316 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(TransportProvider.class)
|
||||
public void testAsyncEcho(Transport transport) throws Exception
|
||||
{
|
||||
init(transport);
|
||||
scenario.start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
System.err.println("Service " + request);
|
||||
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
ServletInputStream input = request.getInputStream();
|
||||
input.setReadListener(new ReadListener()
|
||||
{
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException
|
||||
{
|
||||
while (input.isReady())
|
||||
{
|
||||
int b = input.read();
|
||||
if (b >= 0)
|
||||
{
|
||||
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
|
||||
response.getOutputStream().write(b);
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException
|
||||
{
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable x)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
AsyncRequestContent contentProvider = new AsyncRequestContent();
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Result> resultRef = new AtomicReference<>();
|
||||
scenario.client.newRequest(scenario.newURI())
|
||||
.method(HttpMethod.POST)
|
||||
.path(scenario.servletPath)
|
||||
.body(contentProvider)
|
||||
.send(new BufferingResponseListener(16 * 1024 * 1024)
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
resultRef.set(result);
|
||||
clientLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < 1_000_000; i++)
|
||||
{
|
||||
contentProvider.offer(BufferUtil.toBuffer("S" + i));
|
||||
}
|
||||
contentProvider.close();
|
||||
|
||||
assertTrue(clientLatch.await(30, TimeUnit.SECONDS));
|
||||
assertThat(resultRef.get().isSucceeded(), Matchers.is(true));
|
||||
assertThat(resultRef.get().getResponse().getStatus(), Matchers.equalTo(HttpStatus.OK_200));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(TransportProvider.class)
|
||||
public void testAsyncInterceptedTwice(Transport transport) throws Exception
|
||||
{
|
||||
init(transport);
|
||||
scenario.start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
System.err.println("Service " + request);
|
||||
|
||||
final HttpInput httpInput = ((Request)request).getHttpInput();
|
||||
httpInput.addInterceptor(new GzipHttpInputInterceptor(new InflaterPool(-1, true), ((Request)request).getHttpChannel().getByteBufferPool(), 1024));
|
||||
httpInput.addInterceptor(content ->
|
||||
{
|
||||
ByteBuffer byteBuffer = content.getByteBuffer();
|
||||
byte[] bytes = new byte[2];
|
||||
bytes[1] = byteBuffer.get();
|
||||
bytes[0] = byteBuffer.get();
|
||||
return new Content(wrap(bytes));
|
||||
});
|
||||
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
ServletInputStream input = request.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
input.setReadListener(new ReadListener()
|
||||
{
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException
|
||||
{
|
||||
while (input.isReady())
|
||||
{
|
||||
int b = input.read();
|
||||
if (b > 0)
|
||||
{
|
||||
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
|
||||
out.write(b);
|
||||
}
|
||||
else if (b < 0)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException
|
||||
{
|
||||
response.getOutputStream().write(out.toByteArray());
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable x)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
AsyncRequestContent contentProvider = new AsyncRequestContent();
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
|
||||
String expected =
|
||||
"0S" +
|
||||
"1S" +
|
||||
"2S" +
|
||||
"3S" +
|
||||
"4S" +
|
||||
"5S" +
|
||||
"6S";
|
||||
|
||||
scenario.client.newRequest(scenario.newURI())
|
||||
.method(HttpMethod.POST)
|
||||
.path(scenario.servletPath)
|
||||
.body(contentProvider)
|
||||
.send(new BufferingResponseListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
if (result.isSucceeded())
|
||||
{
|
||||
Response response = result.getResponse();
|
||||
assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
|
||||
assertThat(getContentAsString(), Matchers.equalTo(expected));
|
||||
clientLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
contentProvider.offer(gzipToBuffer("S" + i));
|
||||
contentProvider.flush();
|
||||
}
|
||||
contentProvider.close();
|
||||
|
||||
assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(TransportProvider.class)
|
||||
public void testAsyncInterceptedTwiceWithNulls(Transport transport) throws Exception
|
||||
{
|
||||
init(transport);
|
||||
scenario.start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
System.err.println("Service " + request);
|
||||
|
||||
final HttpInput httpInput = ((Request)request).getHttpInput();
|
||||
httpInput.addInterceptor(content ->
|
||||
{
|
||||
if (content.isEmpty())
|
||||
return content;
|
||||
|
||||
// skip contents with odd numbers
|
||||
ByteBuffer duplicate = content.getByteBuffer().duplicate();
|
||||
duplicate.get();
|
||||
byte integer = duplicate.get();
|
||||
int idx = Character.getNumericValue(integer);
|
||||
Content contentCopy = new Content(content.getByteBuffer().duplicate());
|
||||
content.skip(content.remaining());
|
||||
if (idx % 2 == 0)
|
||||
return contentCopy;
|
||||
return null;
|
||||
});
|
||||
httpInput.addInterceptor(content ->
|
||||
{
|
||||
if (content.isEmpty())
|
||||
return content;
|
||||
|
||||
// reverse the bytes
|
||||
ByteBuffer byteBuffer = content.getByteBuffer();
|
||||
byte[] bytes = new byte[2];
|
||||
bytes[1] = byteBuffer.get();
|
||||
bytes[0] = byteBuffer.get();
|
||||
return new Content(wrap(bytes));
|
||||
});
|
||||
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
ServletInputStream input = request.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
input.setReadListener(new ReadListener()
|
||||
{
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException
|
||||
{
|
||||
while (input.isReady())
|
||||
{
|
||||
int b = input.read();
|
||||
if (b > 0)
|
||||
{
|
||||
// System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b));
|
||||
out.write(b);
|
||||
}
|
||||
else if (b < 0)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException
|
||||
{
|
||||
response.getOutputStream().write(out.toByteArray());
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable x)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
AsyncRequestContent contentProvider = new AsyncRequestContent();
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
|
||||
String expected =
|
||||
"0S" +
|
||||
"2S" +
|
||||
"4S" +
|
||||
"6S";
|
||||
|
||||
scenario.client.newRequest(scenario.newURI())
|
||||
.method(HttpMethod.POST)
|
||||
.path(scenario.servletPath)
|
||||
.body(contentProvider)
|
||||
.send(new BufferingResponseListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
if (result.isSucceeded())
|
||||
{
|
||||
Response response = result.getResponse();
|
||||
assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200));
|
||||
assertThat(getContentAsString(), Matchers.equalTo(expected));
|
||||
clientLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contentProvider.offer(BufferUtil.toBuffer("S0"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S1"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S2"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S3"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S4"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S5"));
|
||||
contentProvider.flush();
|
||||
contentProvider.offer(BufferUtil.toBuffer("S6"));
|
||||
contentProvider.close();
|
||||
|
||||
assertTrue(clientLatch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private ByteBuffer gzipToBuffer(String s) throws IOException
|
||||
{
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
GZIPOutputStream gzos = new GZIPOutputStream(baos);
|
||||
gzos.write(s.getBytes(StandardCharsets.ISO_8859_1));
|
||||
gzos.close();
|
||||
return BufferUtil.toBuffer(baos.toByteArray());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(TransportProvider.class)
|
||||
public void testWriteListenerFromOtherThread(Transport transport) throws Exception
|
||||
@ -1387,18 +1710,21 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
}))
|
||||
.send();
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
latch.countDown();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
failures.offer(x);
|
||||
}
|
||||
finally
|
||||
{
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assertTrue(latch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(failures.isEmpty());
|
||||
assertThat(failures, empty());
|
||||
}
|
||||
|
||||
private static class Listener implements ReadListener, WriteListener
|
||||
@ -1527,10 +1853,11 @@ public class AsyncIOServletTest extends AbstractTest<AsyncIOServletTest.AsyncTra
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopServer()
|
||||
public void stopServer() throws Exception
|
||||
{
|
||||
checkScope();
|
||||
scope.set(null);
|
||||
super.stopServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,13 @@
|
||||
<dependency>
|
||||
<groupId>org.infinispan.protostream</groupId>
|
||||
<artifactId>protostream</artifactId>
|
||||
<version>4.2.2.Final</version>
|
||||
<version>${infinispan.protostream.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@ -92,7 +92,8 @@
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>2.6.2</version>
|
||||
<version>2.7.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
Loading…
x
Reference in New Issue
Block a user