Merge branch 'jetty-12.0.x' into jetty-12.0.x-9396-websocket-jpms-review
This commit is contained in:
commit
e0133d72bf
|
@ -31,17 +31,17 @@ There is also an link:#error-handler[Error Handler] that services errors related
|
|||
|
||||
_____
|
||||
[NOTE]
|
||||
The `DefaultHandler` will also handle serving out the `flav.ico` file should a request make it through all of the other handlers without being resolved.
|
||||
The `DefaultHandler` will also handle serving out the `favicon.ico` file should a request make it through all of the other handlers without being resolved.
|
||||
_____
|
||||
|
||||
[source, java, subs="{sub-order}"]
|
||||
----
|
||||
Server server = new Server(8080);
|
||||
HandlerList handlers = new HandlerList();
|
||||
ResourceHandler resourceHandler = new ResourceHandler();
|
||||
resourceHandler.setBaseResource(Resource.newResource("."));
|
||||
handlers.setHandlers(new Handler[]
|
||||
{ resourceHandler, new DefaultHandler() });
|
||||
resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource("."));
|
||||
Handler.Sequence handlers = new Handler.Sequence(
|
||||
resourceHandler, new DefaultHandler()
|
||||
);
|
||||
server.setHandler(handlers);
|
||||
server.start();
|
||||
----
|
||||
|
|
|
@ -46,7 +46,7 @@ The Jetty Server Libraries provides a number of out-of-the-box __Handler__s that
|
|||
====== ContextHandler
|
||||
|
||||
`ContextHandler` is a `Handler` that represents a _context_ for a web application.
|
||||
It is a `HandlerWrapper` that performs some action before and after delegating to the nested `Handler`.
|
||||
It is a `Handler.Wrapper` that performs some action before and after delegating to the nested `Handler`.
|
||||
// TODO: expand on what the ContextHandler does, e.g. ServletContext.
|
||||
|
||||
The simplest use of `ContextHandler` is the following:
|
||||
|
@ -140,7 +140,7 @@ See also xref:pg-server-http-handler-use-util-default-handler[how to use] `Defau
|
|||
|
||||
`GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content.
|
||||
|
||||
`GzipHandler` is a `HandlerWrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
|
||||
`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
|
||||
The decompression/compression is not performed until the web application reads request content or writes response content.
|
||||
|
||||
`GzipHandler` can be configured at the server level in this way:
|
||||
|
@ -291,7 +291,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer
|
|||
* Sends a HTTP `404` response for any other request
|
||||
* The HTTP `404` response content nicely shows a HTML table with all the contexts deployed on the `Server` instance
|
||||
|
||||
`DefaultHandler` is best used as the last `Handler` of a `HandlerList`, for example:
|
||||
`DefaultHandler` is best used directly set on the server, for example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
|
@ -310,7 +310,7 @@ Server
|
|||
└── DefaultHandler
|
||||
----
|
||||
|
||||
In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `HandlerList` will call the next `Handler` which is `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`.
|
||||
In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `Server` will call the `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`.
|
||||
|
||||
NOTE: `DefaultHandler` just sends a nicer HTTP `404` response in case of wrong requests from clients.
|
||||
Jetty will send an HTTP `404` response anyway if `DefaultHandler` is not used.
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.docs.programming;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.content.AsyncContent;
|
||||
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.CharsetStringBuilder;
|
||||
import org.eclipse.jetty.util.FutureCallback;
|
||||
import org.eclipse.jetty.util.Utf8StringBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ContentDocs
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ContentDocs.class);
|
||||
|
||||
// tag::echo[]
|
||||
public void echo(Content.Source source, Content.Sink sink, Callback callback)
|
||||
{
|
||||
Callback echo = new Callback()
|
||||
{
|
||||
private Content.Chunk chunk;
|
||||
|
||||
public void succeeded()
|
||||
{
|
||||
// release any previous chunk
|
||||
if (chunk != null)
|
||||
{
|
||||
chunk.release();
|
||||
// complete if it was the last
|
||||
if (chunk.isLast())
|
||||
{
|
||||
callback.succeeded();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// read the next chunk
|
||||
chunk = source.read();
|
||||
|
||||
if (chunk == null)
|
||||
{
|
||||
// if no chunk, demand more and call succeeded when demand is met.
|
||||
source.demand(this::succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Content.Chunk.isFailure(chunk, true))
|
||||
{
|
||||
// if it is a persistent failure, then fail the callback
|
||||
callback.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunk.hasRemaining() || chunk.isLast())
|
||||
{
|
||||
// if chunk has content or is last, write it to the sink and resume this loop in callback
|
||||
sink.write(chunk.isLast(), chunk.getByteBuffer(), this);
|
||||
return;
|
||||
}
|
||||
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
source.fail(x);
|
||||
callback.failed(x);
|
||||
}
|
||||
};
|
||||
source.demand(echo::succeeded);
|
||||
}
|
||||
// tag::echo[]
|
||||
|
||||
public static void testEcho() throws Exception
|
||||
{
|
||||
AsyncContent source = new AsyncContent();
|
||||
AsyncContent sink = new AsyncContent();
|
||||
|
||||
Callback.Completable echoCallback = new Callback.Completable();
|
||||
new ContentDocs().echo(source, sink, echoCallback);
|
||||
|
||||
Content.Chunk chunk = sink.read();
|
||||
if (chunk != null)
|
||||
throw new IllegalStateException("No chunk expected yet");
|
||||
|
||||
FutureCallback onContentAvailable = new FutureCallback();
|
||||
sink.demand(onContentAvailable::succeeded);
|
||||
if (onContentAvailable.isDone())
|
||||
throw new IllegalStateException("No demand expected yet");
|
||||
|
||||
Callback.Completable writeCallback = new Callback.Completable();
|
||||
Content.Sink.write(source, false, "One", writeCallback);
|
||||
if (writeCallback.isDone())
|
||||
throw new IllegalStateException("Should wait until first chunk is consumed");
|
||||
|
||||
onContentAvailable.get();
|
||||
chunk = sink.read();
|
||||
if (!"One".equals(BufferUtil.toString(chunk.getByteBuffer())))
|
||||
throw new IllegalStateException("first chunk is expected");
|
||||
|
||||
if (writeCallback.isDone())
|
||||
throw new IllegalStateException("Should wait until first chunk is consumed");
|
||||
chunk.release();
|
||||
writeCallback.get();
|
||||
|
||||
|
||||
writeCallback = new Callback.Completable();
|
||||
Content.Sink.write(source, true, "Two", writeCallback);
|
||||
if (writeCallback.isDone())
|
||||
throw new IllegalStateException("Should wait until second chunk is consumed");
|
||||
|
||||
onContentAvailable = new FutureCallback();
|
||||
sink.demand(onContentAvailable::succeeded);
|
||||
if (!onContentAvailable.isDone())
|
||||
throw new IllegalStateException("Demand expected for second chunk");
|
||||
|
||||
chunk = sink.read();
|
||||
if (!"Two".equals(BufferUtil.toString(chunk.getByteBuffer())))
|
||||
throw new IllegalStateException("second chunk is expected");
|
||||
chunk.release();
|
||||
writeCallback.get();
|
||||
|
||||
onContentAvailable = new FutureCallback();
|
||||
sink.demand(onContentAvailable::succeeded);
|
||||
if (!onContentAvailable.isDone())
|
||||
throw new IllegalStateException("Demand expected for EOF");
|
||||
|
||||
chunk = sink.read();
|
||||
if (!chunk.isLast())
|
||||
throw new IllegalStateException("EOF expected");
|
||||
}
|
||||
|
||||
public static class FutureString extends CompletableFuture<String>
|
||||
{
|
||||
private final CharsetStringBuilder text;
|
||||
private final Content.Source source;
|
||||
|
||||
public FutureString(Content.Source source, Charset charset)
|
||||
{
|
||||
this.source = source;
|
||||
this.text = CharsetStringBuilder.forCharset(charset);
|
||||
source.demand(this::onContentAvailable);
|
||||
}
|
||||
|
||||
private void onContentAvailable()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = source.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
source.demand(this::onContentAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
throw chunk.getFailure();
|
||||
|
||||
if (chunk.hasRemaining())
|
||||
text.append(chunk.getByteBuffer());
|
||||
|
||||
if (chunk.isLast() && complete(text.build()))
|
||||
return;
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
completeExceptionally(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void testFutureString() throws Exception
|
||||
{
|
||||
AsyncContent source = new AsyncContent();
|
||||
FutureString future = new FutureString(source, StandardCharsets.UTF_8);
|
||||
if (future.isDone())
|
||||
throw new IllegalStateException();
|
||||
|
||||
Callback.Completable writeCallback = new Callback.Completable();
|
||||
Content.Sink.write(source, false, "One", writeCallback);
|
||||
if (!writeCallback.isDone() || future.isDone())
|
||||
throw new IllegalStateException("Should be consumed");
|
||||
Content.Sink.write(source, false, "Two", writeCallback);
|
||||
if (!writeCallback.isDone() || future.isDone())
|
||||
throw new IllegalStateException("Should be consumed");
|
||||
Content.Sink.write(source, true, "Three", writeCallback);
|
||||
if (!writeCallback.isDone() || !future.isDone())
|
||||
throw new IllegalStateException("Should be consumed");
|
||||
}
|
||||
|
||||
public static class FutureUtf8String extends ContentSourceCompletableFuture<String>
|
||||
{
|
||||
private final Utf8StringBuilder builder = new Utf8StringBuilder();
|
||||
|
||||
public FutureUtf8String(Content.Source content)
|
||||
{
|
||||
super(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parse(Content.Chunk chunk) throws Throwable
|
||||
{
|
||||
if (chunk.hasRemaining())
|
||||
builder.append(chunk.getByteBuffer());
|
||||
return chunk.isLast() ? builder.takeCompleteString(IllegalStateException::new) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception
|
||||
{
|
||||
testEcho();
|
||||
testFutureString();
|
||||
}
|
||||
}
|
|
@ -110,53 +110,6 @@ public class HTTPServerDocs
|
|||
// end::simple[]
|
||||
}
|
||||
|
||||
public void httpChannelListener() throws Exception
|
||||
{
|
||||
// tag::httpChannelListener[]
|
||||
// TODO: HttpChannelState.Listener does not exist anymore.
|
||||
/*
|
||||
class TimingHttpChannelListener implements HttpChannelState.Listener
|
||||
{
|
||||
private final ConcurrentMap<Request, Long> times = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onRequestBegin(Request request)
|
||||
{
|
||||
times.put(request, NanoTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(Request request)
|
||||
{
|
||||
long begin = times.remove(request);
|
||||
long elapsed = NanoTime.since(begin);
|
||||
System.getLogger("timing").log(INFO, "Request {0} took {1} ns", request, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
Server server = new Server();
|
||||
|
||||
Connector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Add the HttpChannel.Listener as bean to the connector.
|
||||
connector.addBean(new TimingHttpChannelListener());
|
||||
|
||||
// Set a simple Handler to handle requests/responses.
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
jettyRequest.setHandled(true);
|
||||
}
|
||||
});
|
||||
|
||||
server.start();
|
||||
*/
|
||||
// end::httpChannelListener[]
|
||||
}
|
||||
|
||||
public void serverRequestLogSLF4J()
|
||||
{
|
||||
// tag::serverRequestLogSLF4J[]
|
||||
|
@ -508,14 +461,14 @@ public class HTTPServerDocs
|
|||
// tag::handlerTree[]
|
||||
Server server = new Server();
|
||||
|
||||
GzipHandler gzipHandler = new GzipHandler();
|
||||
server.setHandler(gzipHandler);
|
||||
|
||||
Handler.Sequence sequence = new Handler.Sequence();
|
||||
gzipHandler.setHandler(sequence);
|
||||
|
||||
sequence.addHandler(new App1Handler());
|
||||
sequence.addHandler(new App2Handler());
|
||||
|
||||
GzipHandler gzipHandler = new GzipHandler(sequence);
|
||||
|
||||
server.setHandler(gzipHandler);
|
||||
|
||||
// end::handlerTree[]
|
||||
}
|
||||
|
||||
|
@ -585,6 +538,11 @@ public class HTTPServerDocs
|
|||
// tag::handlerFilter[]
|
||||
class FilterHandler extends Handler.Wrapper
|
||||
{
|
||||
public FilterHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
|
@ -617,9 +575,7 @@ public class HTTPServerDocs
|
|||
server.addConnector(connector);
|
||||
|
||||
// Link the Handlers.
|
||||
FilterHandler filter = new FilterHandler();
|
||||
filter.setHandler(new HelloWorldHandler());
|
||||
server.setHandler(filter);
|
||||
server.setHandler(new FilterHandler(new HelloWorldHandler()));
|
||||
|
||||
server.start();
|
||||
// end::handlerFilter[]
|
||||
|
@ -643,9 +599,7 @@ public class HTTPServerDocs
|
|||
server.addConnector(connector);
|
||||
|
||||
// Create a ContextHandler with contextPath.
|
||||
ContextHandler context = new ContextHandler();
|
||||
context.setContextPath("/shop");
|
||||
context.setHandler(new ShopHandler());
|
||||
ContextHandler context = new ContextHandler(new ShopHandler(), "/shop");
|
||||
|
||||
// Link the context to the server.
|
||||
server.setHandler(context);
|
||||
|
@ -683,20 +637,17 @@ public class HTTPServerDocs
|
|||
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
|
||||
// Create the context for the shop web application and add it to ContextHandlerCollection.
|
||||
contextCollection.addHandler(new ContextHandler(new ShopHandler(), "/shop"));
|
||||
|
||||
// Link the ContextHandlerCollection to the Server.
|
||||
server.setHandler(contextCollection);
|
||||
|
||||
// Create the context for the shop web application.
|
||||
ContextHandler shopContext = new ContextHandler("/shop");
|
||||
shopContext.setHandler(new ShopHandler());
|
||||
// Add it to ContextHandlerCollection.
|
||||
contextCollection.addHandler(shopContext);
|
||||
|
||||
server.start();
|
||||
|
||||
// Create the context for the API web application.
|
||||
ContextHandler apiContext = new ContextHandler("/api");
|
||||
apiContext.setHandler(new RESTHandler());
|
||||
ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");
|
||||
// Web applications can be deployed after the Server is started.
|
||||
contextCollection.deployHandler(apiContext, Callback.NOOP);
|
||||
// end::contextHandlerCollection[]
|
||||
|
@ -791,7 +742,7 @@ public class HTTPServerDocs
|
|||
// tag::multipleResourcesHandler[]
|
||||
ResourceHandler handler = new ResourceHandler();
|
||||
|
||||
// For multiple directories, use ResourceCollection.
|
||||
// For multiple directories, use ResourceFactory.combine().
|
||||
Resource resource = ResourceFactory.combine(
|
||||
ResourceFactory.of(handler).newResource("/path/to/static/resources/"),
|
||||
ResourceFactory.of(handler).newResource("/another/path/to/static/resources/")
|
||||
|
@ -822,8 +773,10 @@ public class HTTPServerDocs
|
|||
Connector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Create and configure GzipHandler.
|
||||
GzipHandler gzipHandler = new GzipHandler();
|
||||
// Create a ContextHandlerCollection to manage contexts.
|
||||
ContextHandlerCollection contexts = new ContextHandlerCollection();
|
||||
// Create and configure GzipHandler linked to the ContextHandlerCollection.
|
||||
GzipHandler gzipHandler = new GzipHandler(contexts);
|
||||
// Only compress response content larger than this.
|
||||
gzipHandler.setMinGzipSize(1024);
|
||||
// Do not compress these URI paths.
|
||||
|
@ -833,10 +786,6 @@ public class HTTPServerDocs
|
|||
// Do not compress these mime types.
|
||||
gzipHandler.addExcludedMimeTypes("font/ttf");
|
||||
|
||||
// Link a ContextHandlerCollection to manage contexts.
|
||||
ContextHandlerCollection contexts = new ContextHandlerCollection();
|
||||
gzipHandler.setHandler(contexts);
|
||||
|
||||
// Link the GzipHandler to the Server.
|
||||
server.setHandler(gzipHandler);
|
||||
|
||||
|
@ -873,26 +822,21 @@ public class HTTPServerDocs
|
|||
// tag::contextGzipHandler[]
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the Server.
|
||||
server.setHandler(contextCollection);
|
||||
|
||||
// Create the context for the shop web application.
|
||||
ContextHandler shopContext = new ContextHandler("/shop");
|
||||
shopContext.setHandler(new ShopHandler());
|
||||
|
||||
// You want to gzip the shop web application only.
|
||||
GzipHandler shopGzipHandler = new GzipHandler();
|
||||
shopGzipHandler.setHandler(shopContext);
|
||||
// Create the context for the shop web application wrapped with GzipHandler so only the shop will do gzip.
|
||||
GzipHandler shopGzipHandler = new GzipHandler(new ContextHandler(new ShopHandler(), "/shop"));
|
||||
|
||||
// Add it to ContextHandlerCollection.
|
||||
contextCollection.addHandler(shopGzipHandler);
|
||||
|
||||
// Create the context for the API web application.
|
||||
ContextHandler apiContext = new ContextHandler("/api");
|
||||
apiContext.setHandler(new RESTHandler());
|
||||
ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");
|
||||
|
||||
// Add it to ContextHandlerCollection.
|
||||
contextCollection.addHandler(apiContext);
|
||||
|
||||
// Link the ContextHandlerCollection to the Server.
|
||||
server.setHandler(contextCollection);
|
||||
// end::contextGzipHandler[]
|
||||
|
||||
server.start();
|
||||
|
@ -905,7 +849,10 @@ public class HTTPServerDocs
|
|||
ServerConnector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the RewriteHandler.
|
||||
RewriteHandler rewriteHandler = new RewriteHandler(contextCollection);
|
||||
// Compacts URI paths with double slashes, e.g. /ctx//path/to//resource.
|
||||
rewriteHandler.addRule(new CompactPathRule());
|
||||
// Rewrites */products/* to */p/*.
|
||||
|
@ -918,11 +865,6 @@ public class HTTPServerDocs
|
|||
// Link the RewriteHandler to the Server.
|
||||
server.setHandler(rewriteHandler);
|
||||
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the RewriteHandler.
|
||||
rewriteHandler.setHandler(contextCollection);
|
||||
|
||||
server.start();
|
||||
// end::rewriteHandler[]
|
||||
}
|
||||
|
@ -934,16 +876,15 @@ public class HTTPServerDocs
|
|||
ServerConnector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
StatisticsHandler statsHandler = new StatisticsHandler();
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
|
||||
// Link the ContextHandlerCollection to the StatisticsHandler.
|
||||
StatisticsHandler statsHandler = new StatisticsHandler(contextCollection);
|
||||
|
||||
// Link the StatisticsHandler to the Server.
|
||||
server.setHandler(statsHandler);
|
||||
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the StatisticsHandler.
|
||||
statsHandler.setHandler(contextCollection);
|
||||
|
||||
server.start();
|
||||
// end::statisticsHandler[]
|
||||
}
|
||||
|
@ -955,17 +896,15 @@ public class HTTPServerDocs
|
|||
ServerConnector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Create the MinimumDataRateHandler with a minimum read rate of 1KB per second and no minimum write rate.
|
||||
StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(1024L, 0L);
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
|
||||
// Create the MinimumDataRateHandler linked the ContextHandlerCollection with a minimum read rate of 1KB per second and no minimum write rate.
|
||||
StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(contextCollection, 1024L, 0L);
|
||||
|
||||
// Link the MinimumDataRateHandler to the Server.
|
||||
server.setHandler(dataRateHandler);
|
||||
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the MinimumDataRateHandler.
|
||||
dataRateHandler.setHandler(contextCollection);
|
||||
|
||||
server.start();
|
||||
// end::dataRateHandler[]
|
||||
}
|
||||
|
@ -1006,16 +945,15 @@ public class HTTPServerDocs
|
|||
secureConnector.setPort(8443);
|
||||
server.addConnector(secureConnector);
|
||||
|
||||
SecuredRedirectHandler securedHandler = new SecuredRedirectHandler();
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
|
||||
// Link the ContextHandlerCollection to the SecuredRedirectHandler.
|
||||
SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(contextCollection);
|
||||
|
||||
// Link the SecuredRedirectHandler to the Server.
|
||||
server.setHandler(securedHandler);
|
||||
|
||||
// Create a ContextHandlerCollection to hold contexts.
|
||||
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
|
||||
// Link the ContextHandlerCollection to the StatisticsHandler.
|
||||
securedHandler.setHandler(contextCollection);
|
||||
|
||||
server.start();
|
||||
// end::securedHandler[]
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SessionDocs
|
|||
//tag:schsession[]
|
||||
Server server = new Server();
|
||||
|
||||
ServletContextHandler context = new ServletContextHandler(server, "/foo", ServletContextHandler.SESSIONS);
|
||||
ServletContextHandler context = new ServletContextHandler("/foo", ServletContextHandler.SESSIONS);
|
||||
SessionHandler sessions = context.getSessionHandler();
|
||||
//make idle sessions valid for only 5mins
|
||||
sessions.setMaxInactiveInterval(300);
|
||||
|
|
|
@ -63,7 +63,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Ensure that JavaxWebSocketServletContainerInitializer is initialized,
|
||||
|
@ -82,7 +82,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Ensure that JavaxWebSocketServletContainerInitializer is initialized,
|
||||
|
@ -137,7 +137,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Setup the ServerContainer and the WebSocket endpoints for this web application context.
|
||||
|
@ -169,7 +169,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Ensure that JettyWebSocketServletContainerInitializer is initialized,
|
||||
|
@ -188,7 +188,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Ensure that JettyWebSocketServletContainerInitializer is initialized,
|
||||
|
@ -234,7 +234,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Setup the JettyWebSocketServerContainer and the WebSocket endpoints for this web application context.
|
||||
|
@ -301,7 +301,7 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// Create a ServletContextHandler with the given context path.
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Setup the JettyWebSocketServerContainer to initialize WebSocket components.
|
||||
|
@ -357,7 +357,8 @@ public class WebSocketServerDocs
|
|||
Server server = new Server(8080);
|
||||
|
||||
// tag::uriTemplatePathSpec[]
|
||||
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
|
||||
ServletContextHandler handler = new ServletContextHandler("/ctx");
|
||||
server.setHandler(handler);
|
||||
|
||||
// Configure the JettyWebSocketServerContainer.
|
||||
JettyWebSocketServletContainerInitializer.configure(handler, (servletContext, container) ->
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<name>Core :: ALPN :: Client</name>
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.alpn.client</bundle-symbolic-name>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.alpn.*</spotbugs.onlyAnalyze>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.jetty.alpn.*</spotbugs.onlyAnalyze>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<name>Core :: ALPN :: Server</name>
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.alpn.server</bundle-symbolic-name>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.alpn.*</spotbugs.onlyAnalyze>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.jetty.alpn.*</spotbugs.onlyAnalyze>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
|
||||
<jetty.test.policy.loc>target/test-policy</jetty.test.policy.loc>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.client.*</spotbugs.onlyAnalyze>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.jetty.client.*</spotbugs.onlyAnalyze>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -175,9 +175,9 @@ public interface Response
|
|||
contentSource.demand(demandCallback);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
response.abort(error.getCause());
|
||||
response.abort(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
if (chunk.isLast() && !chunk.hasRemaining())
|
||||
|
|
|
@ -554,7 +554,7 @@ public abstract class HttpReceiver
|
|||
_chunk = inputChunk;
|
||||
if (_chunk == null)
|
||||
return null;
|
||||
if (_chunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(_chunk))
|
||||
return _chunk;
|
||||
|
||||
// Retain the input chunk because its ByteBuffer will be referenced by the Inflater.
|
||||
|
@ -748,7 +748,7 @@ public abstract class HttpReceiver
|
|||
LOG.debug("Erroring {}", this);
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (currentChunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(currentChunk))
|
||||
return false;
|
||||
if (currentChunk != null)
|
||||
currentChunk.release();
|
||||
|
|
|
@ -503,8 +503,8 @@ public abstract class HttpSender
|
|||
}
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
throw error.getCause();
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
throw chunk.getFailure();
|
||||
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
contentBuffer = buffer.asReadOnlyBuffer();
|
||||
|
|
|
@ -676,7 +676,7 @@ public class ResponseListeners
|
|||
Content.Chunk currentChunk = chunk;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Content source #{} fail while current chunk is {}", index, currentChunk);
|
||||
if (currentChunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(currentChunk))
|
||||
return;
|
||||
if (currentChunk != null)
|
||||
currentChunk.release();
|
||||
|
|
|
@ -176,8 +176,8 @@ public class ConnectionPoolTest
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
throw error.getCause();
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
throw chunk.getFailure();
|
||||
|
||||
if (chunk.hasRemaining())
|
||||
{
|
||||
|
|
|
@ -30,7 +30,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.ArgumentsSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest
|
||||
|
@ -248,11 +247,11 @@ public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest
|
|||
.onResponseContentSource((response, contentSource) -> response.abort(new Throwable()).whenComplete((failed, x) ->
|
||||
{
|
||||
Content.Chunk chunk = contentSource.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
contentSource.demand(() ->
|
||||
{
|
||||
Content.Chunk c = contentSource.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, c);
|
||||
assertTrue(Content.Chunk.isFailure(c, true));
|
||||
errorContentLatch.countDown();
|
||||
});
|
||||
}))
|
||||
|
|
|
@ -439,12 +439,12 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
|
||||
assertEquals("multipart/form-data", HttpField.valueParameters(contentType, null));
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setFilesDirectory(tmpDir);
|
||||
formData.parse(request);
|
||||
|
||||
try
|
||||
{
|
||||
process(formData.join()); // May block waiting for multipart form data.
|
||||
process(formData.parse(request).join()); // May block waiting for multipart form data.
|
||||
response.write(true, BufferUtil.EMPTY_BUFFER, callback);
|
||||
}
|
||||
catch (Exception x)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.deploy</bundle-symbolic-name>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.deploy.*</spotbugs.onlyAnalyze>
|
||||
<spotbugs.onlyAnalyze>org.eclipse.jetty.deploy.*</spotbugs.onlyAnalyze>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -197,7 +197,7 @@ public class HttpStreamOverFCGI implements HttpStream
|
|||
{
|
||||
if (_chunk == null)
|
||||
_chunk = Content.Chunk.EOF;
|
||||
else if (!_chunk.isLast() && !(_chunk instanceof Content.Chunk.Error))
|
||||
else if (!_chunk.isLast() && !(Content.Chunk.isFailure(_chunk)))
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
|
|
|
@ -1204,6 +1204,167 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
// return a merged header with missing ensured values added
|
||||
return new HttpField(ensure.getHeader(), ensure.getName(), v.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper of {@link HttpFields}.
|
||||
*/
|
||||
class Wrapper implements Mutable
|
||||
{
|
||||
private final Mutable _fields;
|
||||
|
||||
public Wrapper(Mutable fields)
|
||||
{
|
||||
_fields = fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a field is added (including as part of a put).
|
||||
* @param field The field being added.
|
||||
* @return The field to add, or null if the add is to be ignored.
|
||||
*/
|
||||
public HttpField onAddField(HttpField field)
|
||||
{
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a field is removed (including as part of a put).
|
||||
* @param field The field being removed.
|
||||
* @return True if the field should be removed, false otherwise.
|
||||
*/
|
||||
public boolean onRemoveField(HttpField field)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpFields takeAsImmutable()
|
||||
{
|
||||
return Mutable.super.takeAsImmutable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
// This impl needed only as an optimization
|
||||
return _fields.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<HttpField> stream()
|
||||
{
|
||||
// This impl needed only as an optimization
|
||||
return _fields.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mutable add(HttpField field)
|
||||
{
|
||||
// This impl needed only as an optimization
|
||||
if (field != null)
|
||||
{
|
||||
field = onAddField(field);
|
||||
if (field != null)
|
||||
return _fields.add(field);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator<HttpField> listIterator()
|
||||
{
|
||||
ListIterator<HttpField> i = _fields.listIterator();
|
||||
return new ListIterator<>()
|
||||
{
|
||||
HttpField last;
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return i.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField next()
|
||||
{
|
||||
return last = i.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPrevious()
|
||||
{
|
||||
return i.hasPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField previous()
|
||||
{
|
||||
return last = i.previous();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextIndex()
|
||||
{
|
||||
return i.nextIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int previousIndex()
|
||||
{
|
||||
return i.previousIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove()
|
||||
{
|
||||
if (last != null && onRemoveField(last))
|
||||
{
|
||||
last = null;
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(HttpField field)
|
||||
{
|
||||
if (field == null)
|
||||
{
|
||||
if (last != null && onRemoveField(last))
|
||||
{
|
||||
last = null;
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (last != null && onRemoveField(last))
|
||||
{
|
||||
field = onAddField(field);
|
||||
if (field != null)
|
||||
{
|
||||
last = null;
|
||||
i.set(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(HttpField field)
|
||||
{
|
||||
if (field != null)
|
||||
{
|
||||
field = onAddField(field);
|
||||
if (field != null)
|
||||
{
|
||||
last = null;
|
||||
i.add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1391,8 +1552,12 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
public int hashCode()
|
||||
{
|
||||
int hash = 0;
|
||||
for (int i = _fields.length; i-- > 0; )
|
||||
hash ^= _fields[i].hashCode();
|
||||
for (int i = _size; i-- > 0; )
|
||||
{
|
||||
HttpField field = _fields[i];
|
||||
if (field != null)
|
||||
hash ^= field.hashCode();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ import java.io.InputStream;
|
|||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jetty.util.FileID;
|
||||
import org.eclipse.jetty.util.Index;
|
||||
|
@ -37,6 +39,12 @@ import org.slf4j.LoggerFactory;
|
|||
public class MimeTypes
|
||||
{
|
||||
static final Logger LOG = LoggerFactory.getLogger(MimeTypes.class);
|
||||
private static final Set<Locale> KNOWN_LOCALES = Set.copyOf(Arrays.asList(Locale.getAvailableLocales()));
|
||||
|
||||
public static boolean isKnownLocale(Locale locale)
|
||||
{
|
||||
return KNOWN_LOCALES.contains(locale);
|
||||
}
|
||||
|
||||
/** Enumeration of predefined MimeTypes. This is not exhaustive */
|
||||
public enum Type
|
||||
|
|
|
@ -563,7 +563,7 @@ public class MultiPart
|
|||
private State state = State.FIRST;
|
||||
private boolean closed;
|
||||
private Runnable demand;
|
||||
private Content.Chunk.Error errorChunk;
|
||||
private Content.Chunk errorChunk;
|
||||
private Part part;
|
||||
|
||||
public AbstractContentSource(String boundary)
|
||||
|
@ -759,7 +759,7 @@ public class MultiPart
|
|||
case CONTENT ->
|
||||
{
|
||||
Content.Chunk chunk = part.getContentSource().read();
|
||||
if (chunk == null || chunk instanceof Content.Chunk.Error)
|
||||
if (chunk == null || Content.Chunk.isFailure(chunk))
|
||||
yield chunk;
|
||||
if (!chunk.isLast())
|
||||
yield chunk;
|
||||
|
|
|
@ -23,12 +23,12 @@ import java.util.List;
|
|||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
|
||||
/**
|
||||
* <p>A {@link CompletableFuture} that is completed when a multipart/byteranges
|
||||
* content has been parsed asynchronously from a {@link Content.Source} via
|
||||
* {@link #parse(Content.Source)}.</p>
|
||||
* has been parsed asynchronously from a {@link Content.Source}.</p>
|
||||
* <p>Once the parsing of the multipart/byteranges content completes successfully,
|
||||
* objects of this class are completed with a {@link MultiPartByteRanges.Parts}
|
||||
* object.</p>
|
||||
|
@ -52,75 +52,10 @@ import org.eclipse.jetty.util.thread.AutoLock;
|
|||
*
|
||||
* @see Parts
|
||||
*/
|
||||
public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.Parts>
|
||||
public class MultiPartByteRanges
|
||||
{
|
||||
private final PartsListener listener = new PartsListener();
|
||||
private final MultiPart.Parser parser;
|
||||
|
||||
public MultiPartByteRanges(String boundary)
|
||||
private MultiPartByteRanges()
|
||||
{
|
||||
this.parser = new MultiPart.Parser(boundary, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return parser.getBoundary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean completeExceptionally(Throwable failure)
|
||||
{
|
||||
listener.fail(failure);
|
||||
return super.completeExceptionally(failure);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Parses the given multipart/byteranges content.</p>
|
||||
* <p>Returns this {@code MultiPartByteRanges} object,
|
||||
* so that it can be used in the typical "fluent" style
|
||||
* of {@link CompletableFuture}.</p>
|
||||
*
|
||||
* @param content the multipart/byteranges content to parse
|
||||
* @return this {@code MultiPartByteRanges} object
|
||||
*/
|
||||
public MultiPartByteRanges parse(Content.Source content)
|
||||
{
|
||||
new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = content.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
content.demand(this);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
{
|
||||
listener.onFailure(error.getCause());
|
||||
return;
|
||||
}
|
||||
parse(chunk);
|
||||
chunk.release();
|
||||
if (chunk.isLast() || isDone())
|
||||
return;
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void parse(Content.Chunk chunk)
|
||||
{
|
||||
if (listener.isFailed())
|
||||
return;
|
||||
parser.parse(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -267,76 +202,123 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
|
|||
}
|
||||
}
|
||||
|
||||
private class PartsListener extends MultiPart.AbstractPartsListener
|
||||
public static class Parser
|
||||
{
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final List<Content.Chunk> partChunks = new ArrayList<>();
|
||||
private final List<MultiPart.Part> parts = new ArrayList<>();
|
||||
private Throwable failure;
|
||||
private final PartsListener listener = new PartsListener();
|
||||
private final MultiPart.Parser parser;
|
||||
private Parts parts;
|
||||
|
||||
private boolean isFailed()
|
||||
public Parser(String boundary)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return failure != null;
|
||||
}
|
||||
parser = new MultiPart.Parser(boundary, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartContent(Content.Chunk chunk)
|
||||
public CompletableFuture<MultiPartByteRanges.Parts> parse(Content.Source content)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
ContentSourceCompletableFuture<MultiPartByteRanges.Parts> futureParts = new ContentSourceCompletableFuture<>(content)
|
||||
{
|
||||
// Retain the chunk because it is stored for later use.
|
||||
chunk.retain();
|
||||
partChunks.add(chunk);
|
||||
}
|
||||
@Override
|
||||
protected MultiPartByteRanges.Parts parse(Content.Chunk chunk) throws Throwable
|
||||
{
|
||||
if (listener.isFailed())
|
||||
throw listener.failure;
|
||||
parser.parse(chunk);
|
||||
if (listener.isFailed())
|
||||
throw listener.failure;
|
||||
return parts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean completeExceptionally(Throwable failure)
|
||||
{
|
||||
boolean failed = super.completeExceptionally(failure);
|
||||
if (failed)
|
||||
listener.fail(failure);
|
||||
return failed;
|
||||
}
|
||||
};
|
||||
futureParts.parse();
|
||||
return futureParts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
/**
|
||||
* @return the boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
return parser.getBoundary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete()
|
||||
private class PartsListener extends MultiPart.AbstractPartsListener
|
||||
{
|
||||
super.onComplete();
|
||||
List<MultiPart.Part> copy;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
copy = List.copyOf(parts);
|
||||
}
|
||||
complete(new Parts(getBoundary(), copy));
|
||||
}
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final List<Content.Chunk> partChunks = new ArrayList<>();
|
||||
private final List<MultiPart.Part> parts = new ArrayList<>();
|
||||
private Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable failure)
|
||||
{
|
||||
super.onFailure(failure);
|
||||
completeExceptionally(failure);
|
||||
}
|
||||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> partsToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
private boolean isFailed()
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
partsToFail = List.copyOf(parts);
|
||||
parts.clear();
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return failure != null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartContent(Content.Chunk chunk)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
// Retain the chunk because it is stored for later use.
|
||||
chunk.retain();
|
||||
partChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete()
|
||||
{
|
||||
super.onComplete();
|
||||
List<MultiPart.Part> copy;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
copy = List.copyOf(parts);
|
||||
Parser.this.parts = new Parts(getBoundary(), copy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable failure)
|
||||
{
|
||||
fail(failure);
|
||||
}
|
||||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> partsToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
partsToFail = List.copyOf(parts);
|
||||
parts.clear();
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
}
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,11 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -37,8 +40,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
|||
|
||||
/**
|
||||
* <p>A {@link CompletableFuture} that is completed when a multipart/form-data content
|
||||
* has been parsed asynchronously from a {@link Content.Source} via {@link #parse(Content.Source)}
|
||||
* or from one or more {@link Content.Chunk}s via {@link #parse(Content.Chunk)}.</p>
|
||||
* has been parsed asynchronously from a {@link Content.Source}.</p>
|
||||
* <p>Once the parsing of the multipart/form-data content completes successfully,
|
||||
* objects of this class are completed with a {@link Parts} object.</p>
|
||||
* <p>Objects of this class may be configured to save multipart files in a configurable
|
||||
|
@ -67,241 +69,31 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
|||
*
|
||||
* @see Parts
|
||||
*/
|
||||
public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts>
|
||||
public class MultiPartFormData
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormData.class);
|
||||
|
||||
private final PartsListener listener = new PartsListener();
|
||||
private final MultiPart.Parser parser;
|
||||
private boolean useFilesForPartsWithoutFileName;
|
||||
private Path filesDirectory;
|
||||
private long maxFileSize = -1;
|
||||
private long maxMemoryFileSize;
|
||||
private long maxLength = -1;
|
||||
private long length;
|
||||
|
||||
public MultiPartFormData(String boundary)
|
||||
private MultiPartFormData()
|
||||
{
|
||||
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
public static CompletableFuture<Parts> from(Attributes attributes, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
|
||||
{
|
||||
return parser.getBoundary();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Parses the given multipart/form-data content.</p>
|
||||
* <p>Returns this {@code MultiPartFormData} object,
|
||||
* so that it can be used in the typical "fluent"
|
||||
* style of {@link CompletableFuture}.</p>
|
||||
*
|
||||
* @param content the multipart/form-data content to parse
|
||||
* @return this {@code MultiPartFormData} object
|
||||
*/
|
||||
public MultiPartFormData parse(Content.Source content)
|
||||
{
|
||||
new Runnable()
|
||||
@SuppressWarnings("unchecked")
|
||||
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
|
||||
if (futureParts == null)
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = content.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
content.demand(this);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
{
|
||||
listener.onFailure(error.getCause());
|
||||
return;
|
||||
}
|
||||
parse(chunk);
|
||||
chunk.release();
|
||||
if (chunk.isLast() || isDone())
|
||||
return;
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Parses the given chunk containing multipart/form-data bytes.</p>
|
||||
* <p>One or more chunks may be passed to this method, until the parsing
|
||||
* of the multipart/form-data content completes.</p>
|
||||
*
|
||||
* @param chunk the {@link Content.Chunk} to parse.
|
||||
*/
|
||||
public void parse(Content.Chunk chunk)
|
||||
{
|
||||
if (listener.isFailed())
|
||||
return;
|
||||
length += chunk.getByteBuffer().remaining();
|
||||
long max = getMaxLength();
|
||||
if (max > 0 && length > max)
|
||||
listener.onFailure(new IllegalStateException("max length exceeded: %d".formatted(max)));
|
||||
else
|
||||
parser.parse(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the default charset as specified by
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
|
||||
* that is the charset specified by the part named {@code _charset_}.</p>
|
||||
* <p>If that part is not present, returns {@code null}.</p>
|
||||
*
|
||||
* @return the default charset specified by the {@code _charset_} part,
|
||||
* or null if that part is not present
|
||||
*/
|
||||
public Charset getDefaultCharset()
|
||||
{
|
||||
return listener.getDefaultCharset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
|
||||
*/
|
||||
public int getPartHeadersMaxLength()
|
||||
{
|
||||
return parser.getPartHeadersMaxLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
|
||||
*/
|
||||
public void setPartHeadersMaxLength(int partHeadersMaxLength)
|
||||
{
|
||||
parser.setPartHeadersMaxLength(partHeadersMaxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether parts without fileName may be stored as files
|
||||
*/
|
||||
public boolean isUseFilesForPartsWithoutFileName()
|
||||
{
|
||||
return useFilesForPartsWithoutFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
|
||||
*/
|
||||
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
|
||||
{
|
||||
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the directory where files are saved
|
||||
*/
|
||||
public Path getFilesDirectory()
|
||||
{
|
||||
return filesDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
|
||||
*
|
||||
* @param filesDirectory the directory where files are saved
|
||||
*/
|
||||
public void setFilesDirectory(Path filesDirectory)
|
||||
{
|
||||
this.filesDirectory = filesDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum file size in bytes, or -1 for unlimited file size
|
||||
*/
|
||||
public long getMaxFileSize()
|
||||
{
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
|
||||
*/
|
||||
public void setMaxFileSize(long maxFileSize)
|
||||
{
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
|
||||
*/
|
||||
public long getMaxMemoryFileSize()
|
||||
{
|
||||
return maxMemoryFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the maximum memory file size in bytes, after which files will be saved
|
||||
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
|
||||
* <p>Use value {@code 0} to always save the files in the directory.</p>
|
||||
* <p>Use value {@code -1} to never save the files in the directory.</p>
|
||||
*
|
||||
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
|
||||
*/
|
||||
public void setMaxMemoryFileSize(long maxMemoryFileSize)
|
||||
{
|
||||
this.maxMemoryFileSize = maxMemoryFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
|
||||
*/
|
||||
public long getMaxLength()
|
||||
{
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
|
||||
*/
|
||||
public void setMaxLength(long maxLength)
|
||||
{
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public long getMaxParts()
|
||||
{
|
||||
return parser.getMaxParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public void setMaxParts(long maxParts)
|
||||
{
|
||||
parser.setMaxParts(maxParts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean completeExceptionally(Throwable failure)
|
||||
{
|
||||
listener.fail(failure);
|
||||
return super.completeExceptionally(failure);
|
||||
}
|
||||
|
||||
// Only used for testing.
|
||||
int getPartsSize()
|
||||
{
|
||||
return listener.getPartsSize();
|
||||
futureParts = parse.apply(new Parser(boundary));
|
||||
attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
|
||||
}
|
||||
return futureParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>An ordered list of {@link MultiPart.Part}s that can
|
||||
* be accessed by index or by name, or iterated over.</p>
|
||||
*/
|
||||
public class Parts implements Iterable<MultiPart.Part>, Closeable
|
||||
public static class Parts implements Iterable<MultiPart.Part>, Closeable
|
||||
{
|
||||
private final List<MultiPart.Part> parts;
|
||||
|
||||
|
@ -310,11 +102,6 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
this.parts = parts;
|
||||
}
|
||||
|
||||
public MultiPartFormData getMultiPartFormData()
|
||||
{
|
||||
return MultiPartFormData.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the {@link MultiPart.Part} at the given index, a number
|
||||
* between {@code 0} included and the value returned by {@link #size()}
|
||||
|
@ -409,251 +196,447 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
}
|
||||
}
|
||||
|
||||
private class PartsListener extends MultiPart.AbstractPartsListener
|
||||
public static class Parser
|
||||
{
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final List<MultiPart.Part> parts = new ArrayList<>();
|
||||
private final List<Content.Chunk> partChunks = new ArrayList<>();
|
||||
private long fileSize;
|
||||
private long memoryFileSize;
|
||||
private Path filePath;
|
||||
private SeekableByteChannel fileChannel;
|
||||
private Throwable failure;
|
||||
private final PartsListener listener = new PartsListener();
|
||||
private final MultiPart.Parser parser;
|
||||
private boolean useFilesForPartsWithoutFileName;
|
||||
private Path filesDirectory;
|
||||
private long maxFileSize = -1;
|
||||
private long maxMemoryFileSize;
|
||||
private long maxLength = -1;
|
||||
private long length;
|
||||
private Parts parts;
|
||||
|
||||
@Override
|
||||
public void onPartContent(Content.Chunk chunk)
|
||||
public Parser(String boundary)
|
||||
{
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
String fileName = getFileName();
|
||||
if (fileName != null || isUseFilesForPartsWithoutFileName())
|
||||
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
|
||||
}
|
||||
|
||||
public CompletableFuture<Parts> parse(Content.Source content)
|
||||
{
|
||||
ContentSourceCompletableFuture<Parts> futureParts = new ContentSourceCompletableFuture<>(content)
|
||||
{
|
||||
long maxFileSize = getMaxFileSize();
|
||||
fileSize += buffer.remaining();
|
||||
if (maxFileSize >= 0 && fileSize > maxFileSize)
|
||||
@Override
|
||||
protected Parts parse(Content.Chunk chunk) throws Throwable
|
||||
{
|
||||
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
|
||||
return;
|
||||
if (listener.isFailed())
|
||||
throw listener.failure;
|
||||
length += chunk.getByteBuffer().remaining();
|
||||
long max = getMaxLength();
|
||||
if (max >= 0 && length > max)
|
||||
throw new IllegalStateException("max length exceeded: %d".formatted(max));
|
||||
parser.parse(chunk);
|
||||
if (listener.isFailed())
|
||||
throw listener.failure;
|
||||
return parts;
|
||||
}
|
||||
|
||||
long maxMemoryFileSize = getMaxMemoryFileSize();
|
||||
if (maxMemoryFileSize >= 0)
|
||||
@Override
|
||||
public boolean completeExceptionally(Throwable failure)
|
||||
{
|
||||
memoryFileSize += buffer.remaining();
|
||||
if (memoryFileSize > maxMemoryFileSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Must save to disk.
|
||||
if (ensureFileChannel())
|
||||
{
|
||||
// Write existing memory chunks.
|
||||
List<Content.Chunk> partChunks;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
partChunks = List.copyOf(this.partChunks);
|
||||
}
|
||||
for (Content.Chunk c : partChunks)
|
||||
{
|
||||
write(c.getByteBuffer());
|
||||
}
|
||||
}
|
||||
write(buffer);
|
||||
if (chunk.isLast())
|
||||
close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean failed = super.completeExceptionally(failure);
|
||||
if (failed)
|
||||
listener.fail(failure);
|
||||
return failed;
|
||||
}
|
||||
}
|
||||
// Retain the chunk because it is stored for later use.
|
||||
chunk.retain();
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
partChunks.add(chunk);
|
||||
}
|
||||
};
|
||||
futureParts.parse();
|
||||
return futureParts;
|
||||
}
|
||||
|
||||
private void write(ByteBuffer buffer) throws Exception
|
||||
/**
|
||||
* @return the boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
int remaining = buffer.remaining();
|
||||
while (remaining > 0)
|
||||
{
|
||||
SeekableByteChannel channel = fileChannel();
|
||||
if (channel == null)
|
||||
throw new IllegalStateException();
|
||||
int written = channel.write(buffer);
|
||||
if (written == 0)
|
||||
throw new NonWritableChannelException();
|
||||
remaining -= written;
|
||||
}
|
||||
return parser.getBoundary();
|
||||
}
|
||||
|
||||
private void close()
|
||||
/**
|
||||
* <p>Returns the default charset as specified by
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
|
||||
* that is the charset specified by the part named {@code _charset_}.</p>
|
||||
* <p>If that part is not present, returns {@code null}.</p>
|
||||
*
|
||||
* @return the default charset specified by the {@code _charset_} part,
|
||||
* or null if that part is not present
|
||||
*/
|
||||
public Charset getDefaultCharset()
|
||||
{
|
||||
try
|
||||
{
|
||||
Closeable closeable = fileChannel();
|
||||
if (closeable != null)
|
||||
closeable.close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
return listener.getDefaultCharset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
/**
|
||||
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
|
||||
*/
|
||||
public int getPartHeadersMaxLength()
|
||||
{
|
||||
fileSize = 0;
|
||||
memoryFileSize = 0;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
MultiPart.Part part;
|
||||
if (fileChannel != null)
|
||||
part = new MultiPart.PathPart(name, fileName, headers, filePath);
|
||||
else
|
||||
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
|
||||
// Reset part-related state.
|
||||
filePath = null;
|
||||
fileChannel = null;
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
// Store the new part.
|
||||
parts.add(part);
|
||||
}
|
||||
return parser.getPartHeadersMaxLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete()
|
||||
/**
|
||||
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
|
||||
*/
|
||||
public void setPartHeadersMaxLength(int partHeadersMaxLength)
|
||||
{
|
||||
super.onComplete();
|
||||
List<MultiPart.Part> result;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
result = List.copyOf(parts);
|
||||
}
|
||||
complete(new Parts(result));
|
||||
parser.setPartHeadersMaxLength(partHeadersMaxLength);
|
||||
}
|
||||
|
||||
Charset getDefaultCharset()
|
||||
/**
|
||||
* @return whether parts without fileName may be stored as files
|
||||
*/
|
||||
public boolean isUseFilesForPartsWithoutFileName()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return parts.stream()
|
||||
.filter(part -> "_charset_".equals(part.getName()))
|
||||
.map(part -> part.getContentAsString(US_ASCII))
|
||||
.map(Charset::forName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return useFilesForPartsWithoutFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
|
||||
*/
|
||||
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
|
||||
{
|
||||
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the directory where files are saved
|
||||
*/
|
||||
public Path getFilesDirectory()
|
||||
{
|
||||
return filesDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
|
||||
*
|
||||
* @param filesDirectory the directory where files are saved
|
||||
*/
|
||||
public void setFilesDirectory(Path filesDirectory)
|
||||
{
|
||||
this.filesDirectory = filesDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum file size in bytes, or -1 for unlimited file size
|
||||
*/
|
||||
public long getMaxFileSize()
|
||||
{
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
|
||||
*/
|
||||
public void setMaxFileSize(long maxFileSize)
|
||||
{
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
|
||||
*/
|
||||
public long getMaxMemoryFileSize()
|
||||
{
|
||||
return maxMemoryFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets the maximum memory file size in bytes, after which files will be saved
|
||||
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
|
||||
* <p>Use value {@code 0} to always save the files in the directory.</p>
|
||||
* <p>Use value {@code -1} to never save the files in the directory.</p>
|
||||
*
|
||||
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
|
||||
*/
|
||||
public void setMaxMemoryFileSize(long maxMemoryFileSize)
|
||||
{
|
||||
this.maxMemoryFileSize = maxMemoryFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
|
||||
*/
|
||||
public long getMaxLength()
|
||||
{
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
|
||||
*/
|
||||
public void setMaxLength(long maxLength)
|
||||
{
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public long getMaxParts()
|
||||
{
|
||||
return parser.getMaxParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public void setMaxParts(long maxParts)
|
||||
{
|
||||
parser.setMaxParts(maxParts);
|
||||
}
|
||||
|
||||
// Only used for testing.
|
||||
int getPartsSize()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return parts.size();
|
||||
}
|
||||
return listener.getPartsSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable failure)
|
||||
private class PartsListener extends MultiPart.AbstractPartsListener
|
||||
{
|
||||
super.onFailure(failure);
|
||||
completeExceptionally(failure);
|
||||
}
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final List<MultiPart.Part> parts = new ArrayList<>();
|
||||
private final List<Content.Chunk> partChunks = new ArrayList<>();
|
||||
private long fileSize;
|
||||
private long memoryFileSize;
|
||||
private Path filePath;
|
||||
private SeekableByteChannel fileChannel;
|
||||
private Throwable failure;
|
||||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> partsToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
@Override
|
||||
public void onPartContent(Content.Chunk chunk)
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
partsToFail = List.copyOf(parts);
|
||||
parts.clear();
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
close();
|
||||
delete();
|
||||
}
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
String fileName = getFileName();
|
||||
if (fileName != null || isUseFilesForPartsWithoutFileName())
|
||||
{
|
||||
long maxFileSize = getMaxFileSize();
|
||||
fileSize += buffer.remaining();
|
||||
if (maxFileSize >= 0 && fileSize > maxFileSize)
|
||||
{
|
||||
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
|
||||
return;
|
||||
}
|
||||
|
||||
private SeekableByteChannel fileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return fileChannel;
|
||||
}
|
||||
}
|
||||
long maxMemoryFileSize = getMaxMemoryFileSize();
|
||||
if (maxMemoryFileSize >= 0)
|
||||
{
|
||||
memoryFileSize += buffer.remaining();
|
||||
if (memoryFileSize > maxMemoryFileSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Must save to disk.
|
||||
if (ensureFileChannel())
|
||||
{
|
||||
// Write existing memory chunks.
|
||||
List<Content.Chunk> partChunks;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
partChunks = List.copyOf(this.partChunks);
|
||||
}
|
||||
for (Content.Chunk c : partChunks)
|
||||
{
|
||||
write(c.getByteBuffer());
|
||||
}
|
||||
}
|
||||
write(buffer);
|
||||
if (chunk.isLast())
|
||||
close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
|
||||
private void delete()
|
||||
{
|
||||
try
|
||||
{
|
||||
Path path = null;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Retain the chunk because it is stored for later use.
|
||||
chunk.retain();
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (filePath != null)
|
||||
path = filePath;
|
||||
partChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(ByteBuffer buffer) throws Exception
|
||||
{
|
||||
int remaining = buffer.remaining();
|
||||
while (remaining > 0)
|
||||
{
|
||||
SeekableByteChannel channel = fileChannel();
|
||||
if (channel == null)
|
||||
throw new IllegalStateException();
|
||||
int written = channel.write(buffer);
|
||||
if (written == 0)
|
||||
throw new NonWritableChannelException();
|
||||
remaining -= written;
|
||||
}
|
||||
}
|
||||
|
||||
private void close()
|
||||
{
|
||||
try
|
||||
{
|
||||
Closeable closeable = fileChannel();
|
||||
if (closeable != null)
|
||||
closeable.close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
{
|
||||
fileSize = 0;
|
||||
memoryFileSize = 0;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
MultiPart.Part part;
|
||||
if (fileChannel != null)
|
||||
part = new MultiPart.PathPart(name, fileName, headers, filePath);
|
||||
else
|
||||
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
|
||||
// Reset part-related state.
|
||||
filePath = null;
|
||||
fileChannel = null;
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
// Store the new part.
|
||||
parts.add(part);
|
||||
}
|
||||
if (path != null)
|
||||
Files.delete(path);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("IGNORED", x);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFailed()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
@Override
|
||||
public void onComplete()
|
||||
{
|
||||
return failure != null;
|
||||
super.onComplete();
|
||||
List<MultiPart.Part> result;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
result = List.copyOf(parts);
|
||||
Parser.this.parts = new Parts(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ensureFileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
Charset getDefaultCharset()
|
||||
{
|
||||
if (fileChannel != null)
|
||||
return false;
|
||||
createFileChannel();
|
||||
return true;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return parts.stream()
|
||||
.filter(part -> "_charset_".equals(part.getName()))
|
||||
.map(part -> part.getContentAsString(US_ASCII))
|
||||
.map(Charset::forName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createFileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
int getPartsSize()
|
||||
{
|
||||
Path directory = getFilesDirectory();
|
||||
Files.createDirectories(directory);
|
||||
String fileName = "MultiPart";
|
||||
filePath = Files.createTempFile(directory, fileName, "");
|
||||
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return parts.size();
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable failure)
|
||||
{
|
||||
onFailure(x);
|
||||
fail(failure);
|
||||
}
|
||||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> partsToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
partsToFail = List.copyOf(parts);
|
||||
parts.clear();
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
close();
|
||||
delete();
|
||||
}
|
||||
|
||||
private SeekableByteChannel fileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return fileChannel;
|
||||
}
|
||||
}
|
||||
|
||||
private void delete()
|
||||
{
|
||||
try
|
||||
{
|
||||
Path path = null;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (filePath != null)
|
||||
path = filePath;
|
||||
filePath = null;
|
||||
fileChannel = null;
|
||||
}
|
||||
if (path != null)
|
||||
Files.delete(path);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("IGNORED", x);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFailed()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
return failure != null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ensureFileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (fileChannel != null)
|
||||
return false;
|
||||
createFileChannel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void createFileChannel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
Path directory = getFilesDirectory();
|
||||
Files.createDirectories(directory);
|
||||
String fileName = "MultiPart";
|
||||
filePath = Files.createTempFile(directory, fileName, "");
|
||||
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,7 +177,7 @@ wml=text/vnd.wap.wml
|
|||
wmlc=application/vnd.wap.wmlc
|
||||
wmls=text/vnd.wap.wmlscript
|
||||
wmlsc=application/vnd.wap.wmlscriptc
|
||||
woff=application/font-woff
|
||||
woff=font/woff
|
||||
woff2=font/woff2
|
||||
wrl=model/vrml
|
||||
wtls-ca-certificate=application/vnd.wap.wtls-ca-certificate
|
||||
|
|
|
@ -54,6 +54,7 @@ public class HttpFieldsTest
|
|||
return Stream.of(
|
||||
HttpFields.build(),
|
||||
HttpFields.build(0),
|
||||
new HttpFields.Mutable.Wrapper(HttpFields.build()),
|
||||
new HttpFields.Mutable()
|
||||
{
|
||||
private final HttpFields.Mutable fields = HttpFields.build();
|
||||
|
|
|
@ -16,19 +16,20 @@ package org.eclipse.jetty.http;
|
|||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.content.AsyncContent;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
|
@ -71,32 +72,19 @@ public class MultiPartFormDataTest
|
|||
int leaks = 0;
|
||||
for (Content.Chunk chunk : _allocatedChunks)
|
||||
{
|
||||
// Any release that does not return true is a leak.
|
||||
if (!chunk.release())
|
||||
leaks++;
|
||||
// Any release that does not throw or return true is a leak.
|
||||
try
|
||||
{
|
||||
if (!chunk.release())
|
||||
leaks++;
|
||||
}
|
||||
catch (IllegalStateException ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
assertThat("Leaked " + leaks + "/" + _allocatedChunks.size() + " chunk(s)", leaks, is(0));
|
||||
}
|
||||
|
||||
Content.Chunk asChunk(String data, boolean last)
|
||||
{
|
||||
byte[] b = data.getBytes(StandardCharsets.UTF_8);
|
||||
ByteBuffer buffer = BufferUtil.allocate(b.length);
|
||||
BufferUtil.append(buffer, b);
|
||||
Content.Chunk chunk = Content.Chunk.from(buffer, last);
|
||||
_allocatedChunks.add(chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
Content.Chunk asChunk(ByteBuffer data, boolean last)
|
||||
{
|
||||
ByteBuffer buffer = BufferUtil.allocate(data.remaining());
|
||||
BufferUtil.append(buffer, data);
|
||||
Content.Chunk chunk = Content.Chunk.from(buffer, last);
|
||||
_allocatedChunks.add(chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadMultiPart() throws Exception
|
||||
{
|
||||
|
@ -109,14 +97,14 @@ public class MultiPartFormDataTest
|
|||
"Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
|
||||
"\r\n";
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(str, true));
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
Content.Sink.write(source, true, str, Callback.NOOP);
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertInstanceOf(BadMessageException.class, failure);
|
||||
|
@ -139,14 +127,14 @@ public class MultiPartFormDataTest
|
|||
eol +
|
||||
"--" + boundary + "--" + eol;
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(str, true));
|
||||
|
||||
formData.whenComplete((parts, failure) ->
|
||||
Content.Sink.write(source, true, str, Callback.NOOP);
|
||||
formData.parse(source).whenComplete((parts, failure) ->
|
||||
{
|
||||
// No errors and no parts.
|
||||
assertNull(failure);
|
||||
|
@ -165,14 +153,14 @@ public class MultiPartFormDataTest
|
|||
String str = eol +
|
||||
"--" + boundary + "--" + eol;
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(str, true));
|
||||
|
||||
formData.whenComplete((parts, failure) ->
|
||||
Content.Sink.write(source, true, str, Callback.NOOP);
|
||||
formData.parse(source).whenComplete((parts, failure) ->
|
||||
{
|
||||
// No errors and no parts.
|
||||
assertNull(failure);
|
||||
|
@ -213,14 +201,14 @@ public class MultiPartFormDataTest
|
|||
----\r
|
||||
""";
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData("");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(str, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, str, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(4));
|
||||
|
||||
|
@ -253,10 +241,10 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testNoBody() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("boundary");
|
||||
formData.parse(Content.Chunk.EOF);
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
|
||||
source.close();
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertNotNull(failure);
|
||||
|
@ -268,11 +256,11 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testBodyWithOnlyCRLF() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("boundary");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
|
||||
String body = " \n\n\n\r\n\r\n\r\n\r\n";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertNotNull(failure);
|
||||
|
@ -285,7 +273,7 @@ public class MultiPartFormDataTest
|
|||
public void testLeadingWhitespaceBodyWithCRLF() throws Exception
|
||||
{
|
||||
String body = """
|
||||
|
||||
|
||||
|
||||
\r
|
||||
\r
|
||||
|
@ -303,14 +291,14 @@ public class MultiPartFormDataTest
|
|||
--AaB03x--\r
|
||||
""";
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(2));
|
||||
MultiPart.Part part1 = parts.getFirst("field1");
|
||||
|
@ -340,14 +328,14 @@ public class MultiPartFormDataTest
|
|||
--AaB03x--\r
|
||||
""";
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(1024);
|
||||
formData.setMaxLength(3072);
|
||||
formData.setMaxMemoryFileSize(50);
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
// The first boundary must be on a new line, so the first "part" is not recognized as such.
|
||||
assertThat(parts.size(), is(1));
|
||||
|
@ -361,7 +349,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testDefaultLimits() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
String body = """
|
||||
--AaB03x\r
|
||||
|
@ -371,9 +360,8 @@ public class MultiPartFormDataTest
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
|
@ -390,7 +378,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testRequestContentTooBig() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxLength(16);
|
||||
|
||||
|
@ -402,9 +391,8 @@ public class MultiPartFormDataTest
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertNotNull(failure);
|
||||
|
@ -416,7 +404,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testFileTooBig() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxFileSize(16);
|
||||
|
||||
|
@ -428,9 +417,8 @@ public class MultiPartFormDataTest
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertNotNull(failure);
|
||||
|
@ -442,7 +430,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testTwoFilesOneInMemoryOneOnDisk() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
formData.setMaxMemoryFileSize(chunk.length() + 1);
|
||||
|
@ -460,9 +449,8 @@ public class MultiPartFormDataTest
|
|||
$C$C$C$C\r
|
||||
--AaB03x--\r
|
||||
""".replace("$C", chunk);
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertNotNull(parts);
|
||||
assertEquals(2, parts.size());
|
||||
|
@ -482,7 +470,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testPartWrite() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
formData.setMaxMemoryFileSize(chunk.length() + 1);
|
||||
|
@ -500,9 +489,8 @@ public class MultiPartFormDataTest
|
|||
$C$C$C$C\r
|
||||
--AaB03x--\r
|
||||
""".replace("$C", chunk);
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertNotNull(parts);
|
||||
assertEquals(2, parts.size());
|
||||
|
@ -528,7 +516,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testPathPartDelete() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
|
||||
String body = """
|
||||
|
@ -539,9 +528,8 @@ public class MultiPartFormDataTest
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertNotNull(parts);
|
||||
assertEquals(1, parts.size());
|
||||
|
@ -559,7 +547,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testAbort()
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxMemoryFileSize(32);
|
||||
|
||||
|
@ -575,24 +564,27 @@ public class MultiPartFormDataTest
|
|||
--AaB03x--\r
|
||||
""";
|
||||
// Parse only part of the content.
|
||||
formData.parse(asChunk(body, false));
|
||||
Content.Sink.write(source, false, body, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
assertEquals(1, formData.getPartsSize());
|
||||
|
||||
// Abort MultiPartFormData.
|
||||
formData.completeExceptionally(new IOException());
|
||||
futureParts.completeExceptionally(new IOException());
|
||||
|
||||
// Parse the rest of the content.
|
||||
formData.parse(asChunk(terminator, true));
|
||||
Content.Sink.write(source, true, terminator, Callback.NOOP);
|
||||
|
||||
// Try to get the parts, it should fail.
|
||||
assertThrows(ExecutionException.class, () -> formData.get(5, TimeUnit.SECONDS));
|
||||
assertThrows(ExecutionException.class, () -> futureParts.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(0, formData.getPartsSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxHeaderLength() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setPartHeadersMaxLength(32);
|
||||
|
||||
|
@ -604,9 +596,8 @@ public class MultiPartFormDataTest
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
formData.handle((parts, failure) ->
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
formData.parse(source).handle((parts, failure) ->
|
||||
{
|
||||
assertNull(parts);
|
||||
assertNotNull(failure);
|
||||
|
@ -618,7 +609,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testDefaultCharset() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
|
@ -645,13 +637,14 @@ public class MultiPartFormDataTest
|
|||
\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body1, false));
|
||||
formData.parse(asChunk(isoCedilla, false));
|
||||
formData.parse(asChunk(body2, false));
|
||||
formData.parse(asChunk(utfCedilla, false));
|
||||
formData.parse(asChunk(terminator, true));
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
Content.Sink.write(source, false, body1, Callback.NOOP);
|
||||
source.write(false, isoCedilla, Callback.NOOP);
|
||||
Content.Sink.write(source, false, body2, Callback.NOOP);
|
||||
source.write(false, utfCedilla, Callback.NOOP);
|
||||
Content.Sink.write(source, true, terminator, Callback.NOOP);
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
Charset defaultCharset = formData.getDefaultCharset();
|
||||
assertEquals(ISO_8859_1, defaultCharset);
|
||||
|
@ -669,7 +662,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testPartWithBackSlashInFileName() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
|
@ -681,9 +675,9 @@ public class MultiPartFormDataTest
|
|||
stuffaaa\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(contents, true));
|
||||
Content.Sink.write(source, true, contents, Callback.NOOP);
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
|
@ -694,7 +688,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testPartWithWindowsFileName() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
|
@ -706,9 +701,8 @@ public class MultiPartFormDataTest
|
|||
stuffaaa\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(contents, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, contents, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
|
@ -722,7 +716,8 @@ public class MultiPartFormDataTest
|
|||
@Disabled
|
||||
public void testCorrectlyEncodedMSFilename() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
|
@ -734,9 +729,8 @@ public class MultiPartFormDataTest
|
|||
stuffaaa\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(contents, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, contents, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
|
@ -747,7 +741,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testWriteFilesForPartWithoutFileName() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
formData.setUseFilesForPartsWithoutFileName(true);
|
||||
|
||||
|
@ -759,9 +754,8 @@ public class MultiPartFormDataTest
|
|||
sssaaa\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(body, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, body, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
|
@ -775,7 +769,8 @@ public class MultiPartFormDataTest
|
|||
@Test
|
||||
public void testPartsWithSameName() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setFilesDirectory(_tmpDir);
|
||||
|
||||
String sameNames = """
|
||||
|
@ -791,9 +786,8 @@ public class MultiPartFormDataTest
|
|||
AAAAA\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
formData.parse(asChunk(sameNames, true));
|
||||
|
||||
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
|
||||
Content.Sink.write(source, true, sameNames, Callback.NOOP);
|
||||
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(2, parts.size());
|
||||
|
||||
|
@ -810,4 +804,16 @@ public class MultiPartFormDataTest
|
|||
assertEquals("AAAAA", part2.getContentAsString(formData.getDefaultCharset()));
|
||||
}
|
||||
}
|
||||
|
||||
private class TestContent extends AsyncContent
|
||||
{
|
||||
@Override
|
||||
public Content.Chunk read()
|
||||
{
|
||||
Content.Chunk chunk = super.read();
|
||||
if (chunk != null && chunk.canRetain())
|
||||
_allocatedChunks.add(chunk);
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -607,9 +607,9 @@ public class IdleTimeoutTest extends AbstractTest
|
|||
_request.demand(this::onContentAvailable);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
_callback.failed(error.getCause());
|
||||
_callback.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
chunk.release();
|
||||
|
|
|
@ -53,8 +53,9 @@ public class Content
|
|||
/**
|
||||
* <p>Copies the given content source to the given content sink, notifying
|
||||
* the given callback when the copy is complete (either succeeded or failed).</p>
|
||||
* <p>In case of failures, the content source is {@link Source#fail(Throwable) failed}
|
||||
* too.</p>
|
||||
* <p>In case of {@link Chunk#getFailure() failure chunks},
|
||||
* the content source is {@link Source#fail(Throwable) failed} if the failure
|
||||
* chunk is {@link Chunk#isLast() last}, else the failing is transient and is ignored.</p>
|
||||
*
|
||||
* @param source the source to copy from
|
||||
* @param sink the sink to copy to
|
||||
|
@ -76,6 +77,9 @@ public class Content
|
|||
* <p>If the predicate returns {@code false}, it means that the chunk is not
|
||||
* handled, its callback will not be completed, and the implementation will
|
||||
* handle the chunk and its callback.</p>
|
||||
* <p>In case of {@link Chunk#getFailure() failure chunks} not handled by any {@code chunkHandler},
|
||||
* the content source is {@link Source#fail(Throwable) failed} if the failure
|
||||
* chunk is {@link Chunk#isLast() last}, else the failure is transient and is ignored.</p>
|
||||
*
|
||||
* @param source the source to copy from
|
||||
* @param sink the sink to copy to
|
||||
|
@ -103,10 +107,11 @@ public class Content
|
|||
* return;
|
||||
* }
|
||||
*
|
||||
* // The chunk is an error.
|
||||
* if (chunk instanceof Chunk.Error error) {
|
||||
* // Handle the error.
|
||||
* Throwable cause = error.getCause();
|
||||
* // The chunk is a failure.
|
||||
* if (Content.Chunk.isFailure(chunk)) {
|
||||
* // Handle the failure.
|
||||
* Throwable cause = chunk.getFailure();
|
||||
* boolean transient = !chunk.isLast();
|
||||
* // ...
|
||||
* return;
|
||||
* }
|
||||
|
@ -190,7 +195,7 @@ public class Content
|
|||
* @return the String obtained from the content
|
||||
* @throws IOException if reading the content fails
|
||||
*/
|
||||
public static String asString(Source source, Charset charset) throws IOException
|
||||
static String asString(Source source, Charset charset) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -227,12 +232,12 @@ public class Content
|
|||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, non-blocking, the given content source, until either an error or EOF,
|
||||
* <p>Reads, non-blocking, the given content source, until a {@link Chunk#isFailure(Chunk) failure} or EOF
|
||||
* and discards the content.</p>
|
||||
*
|
||||
* @param source the source to read from
|
||||
* @param callback the callback to notify when the whole content has been read
|
||||
* or an error occurred while reading the content
|
||||
* or a failure occurred while reading the content
|
||||
*/
|
||||
static void consumeAll(Source source, Callback callback)
|
||||
{
|
||||
|
@ -240,7 +245,7 @@ public class Content
|
|||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, blocking if necessary, the given content source, until either an error
|
||||
* <p>Reads, blocking if necessary, the given content source, until a {@link Chunk#isFailure(Chunk) failure}
|
||||
* or EOF, and discards the content.</p>
|
||||
*
|
||||
* @param source the source to read from
|
||||
|
@ -274,13 +279,15 @@ public class Content
|
|||
* <p>The returned chunk could be:</p>
|
||||
* <ul>
|
||||
* <li>{@code null}, to signal that there isn't a chunk of content available</li>
|
||||
* <li>an {@link Chunk.Error error} instance, to signal that there was an error
|
||||
* <li>an {@link Chunk} instance with non null {@link Chunk#getFailure()}, to signal that there was a failure
|
||||
* trying to produce a chunk of content, or that the content production has been
|
||||
* {@link #fail(Throwable) failed} externally</li>
|
||||
* <li>a {@link Chunk} instance, containing the chunk of content.</li>
|
||||
* </ul>
|
||||
* <p>Once a read returns an {@link Chunk.Error error} instance, further reads
|
||||
* will continue to return the same error instance.</p>
|
||||
* <p>Once a read returns an {@link Chunk} instance with non-null {@link Chunk#getFailure()}
|
||||
* then if the failure is {@link Chunk#isLast() last} further reads
|
||||
* will continue to return the same failure chunk instance, otherwise further
|
||||
* {@code read()} operations may return different non-failure chunks.</p>
|
||||
* <p>Once a read returns a {@link Chunk#isLast() last chunk}, further reads will
|
||||
* continue to return a last chunk (although the instance may be different).</p>
|
||||
* <p>The content reader code must ultimately arrange for a call to
|
||||
|
@ -296,7 +303,7 @@ public class Content
|
|||
* race condition (the thread that reads with the thread that invokes the
|
||||
* demand callback).</p>
|
||||
*
|
||||
* @return a chunk of content, possibly an error instance, or {@code null}
|
||||
* @return a chunk of content, possibly a failure instance, or {@code null}
|
||||
* @see #demand(Runnable)
|
||||
* @see Retainable
|
||||
*/
|
||||
|
@ -327,18 +334,38 @@ public class Content
|
|||
void demand(Runnable demandCallback);
|
||||
|
||||
/**
|
||||
* <p>Fails this content source, possibly failing and discarding accumulated
|
||||
* content chunks that were not yet read.</p>
|
||||
* <p>Fails this content source with a {@link Chunk#isLast() last} {@link Chunk#getFailure() failure chunk},
|
||||
* failing and discarding accumulated content chunks that were not yet read.</p>
|
||||
* <p>The failure may be notified to the content reader at a later time, when
|
||||
* the content reader reads a content chunk, via an {@link Chunk.Error} instance.</p>
|
||||
* the content reader reads a content chunk, via a {@link Chunk} instance
|
||||
* with a non null {@link Chunk#getFailure()}.</p>
|
||||
* <p>If {@link #read()} has returned a last chunk, this is a no operation.</p>
|
||||
* <p>Typical failure: the content being aborted by user code, or idle timeouts.</p>
|
||||
* <p>If this method has already been called, then it is a no operation.</p>
|
||||
*
|
||||
* @param failure the cause of the failure
|
||||
* @see Chunk#getFailure()
|
||||
*/
|
||||
void fail(Throwable failure);
|
||||
|
||||
/**
|
||||
* <p>Fails this content source with a {@link Chunk#getFailure() failure chunk}
|
||||
* that may or not may be {@link Chunk#isLast() last}.
|
||||
* If {@code last} is {@code true}, then the failure is persistent and a call to this method acts
|
||||
* as {@link #fail(Throwable)}. Otherwise the failure is transient and a
|
||||
* {@link Chunk#getFailure() failure chunk} will be {@link #read() read} in order with content chunks,
|
||||
* and subsequent calls to {@link #read() read} may produce other content.</p>
|
||||
* <p>A {@code Content.Source} or its {@link #read() reader} may treat a transient failure as persistent.</p>
|
||||
*
|
||||
* @param failure A failure.
|
||||
* @param last true if the failure is persistent, false if the failure is transient.
|
||||
* @see Chunk#getFailure()
|
||||
*/
|
||||
default void fail(Throwable failure, boolean last)
|
||||
{
|
||||
fail(failure);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Rewinds this content, if possible, so that subsequent reads return
|
||||
* chunks starting from the beginning of this content.</p>
|
||||
|
@ -555,14 +582,52 @@ public class Content
|
|||
}
|
||||
|
||||
/**
|
||||
* <p>Creates an {@link Error error chunk} with the given failure.</p>
|
||||
* <p>Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
|
||||
* and {@link Chunk#isLast()} returning true.</p>
|
||||
*
|
||||
* @param failure the cause of the failure
|
||||
* @return a new Error.Chunk
|
||||
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
|
||||
*/
|
||||
static Error from(Throwable failure)
|
||||
static Chunk from(Throwable failure)
|
||||
{
|
||||
return new Error(failure);
|
||||
return from(failure, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
|
||||
* and given {@link Chunk#isLast() last} state.</p>
|
||||
*
|
||||
* @param failure the cause of the failure
|
||||
* @param last true if the failure is terminal, else false for transient failure
|
||||
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
|
||||
*/
|
||||
static Chunk from(Throwable failure, boolean last)
|
||||
{
|
||||
return new Chunk()
|
||||
{
|
||||
public Throwable getFailure()
|
||||
{
|
||||
return failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer()
|
||||
{
|
||||
return BufferUtil.EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLast()
|
||||
{
|
||||
return last;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("Chunk@%x{c=%s,l=%b}", hashCode(), failure, last);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -581,8 +646,12 @@ public class Content
|
|||
* <td>{@code null}</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() last}</td>
|
||||
* <td>{@link Error Error}</td>
|
||||
* <td>{@link Error Error}</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() not last}</td>
|
||||
* <td>{@code null}</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #isLast()}</td>
|
||||
|
@ -597,18 +666,57 @@ public class Content
|
|||
*/
|
||||
static Chunk next(Chunk chunk)
|
||||
{
|
||||
if (chunk == null || chunk instanceof Error)
|
||||
return chunk;
|
||||
if (chunk == null)
|
||||
return null;
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
return chunk.isLast() ? chunk : null;
|
||||
if (chunk.isLast())
|
||||
return EOF;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}.
|
||||
* @return True if the chunk is non-null and {@link Chunk#getFailure() chunk.getError()} returns non-null.
|
||||
*/
|
||||
static boolean isFailure(Chunk chunk)
|
||||
{
|
||||
return chunk != null && chunk.getFailure() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}
|
||||
* @param last The {@link Chunk#isLast() last} status to test for.
|
||||
* @return True if the chunk is non-null and {@link Chunk#getFailure()} returns non-null
|
||||
* and {@link Chunk#isLast()} matches the passed status.
|
||||
*/
|
||||
static boolean isFailure(Chunk chunk, boolean last)
|
||||
{
|
||||
return chunk != null && chunk.getFailure() != null && chunk.isLast() == last;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ByteBuffer of this Chunk
|
||||
*/
|
||||
ByteBuffer getByteBuffer();
|
||||
|
||||
/**
|
||||
* Get a failure (which may be from a {@link Source#fail(Throwable) failure} or
|
||||
* a {@link Source#fail(Throwable, boolean) warning}), if any, associated with the chunk.
|
||||
* <ul>
|
||||
* <li>A {@code chunk} must not have a failure and a {@link #getByteBuffer()} with content.</li>
|
||||
* <li>A {@code chunk} with a failure may or may not be {@link #isLast() last}.</li>
|
||||
* <li>A {@code chunk} with a failure must not be {@link #canRetain() retainable}.</li>
|
||||
* </ul>
|
||||
* @return A {@link Throwable} indicating the failure or null if there is no failure or warning.
|
||||
* @see Source#fail(Throwable)
|
||||
* @see Source#fail(Throwable, boolean)
|
||||
*/
|
||||
default Throwable getFailure()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this is the last Chunk
|
||||
*/
|
||||
|
@ -674,46 +782,6 @@ public class Content
|
|||
return asChunk(getByteBuffer().asReadOnlyBuffer(), isLast(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>A chunk that wraps a failure.</p>
|
||||
* <p>Error Chunks are always last and have no bytes to read,
|
||||
* as such they are <em>terminal</em> Chunks.</p>
|
||||
*
|
||||
* @see #from(Throwable)
|
||||
*/
|
||||
final class Error implements Chunk
|
||||
{
|
||||
private final Throwable cause;
|
||||
|
||||
private Error(Throwable cause)
|
||||
{
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
public Throwable getCause()
|
||||
{
|
||||
return cause;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer()
|
||||
{
|
||||
return BufferUtil.EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLast()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x{c=%s}", getClass().getSimpleName(), hashCode(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Implementations of this interface may process {@link Chunk}s being copied by the
|
||||
* {@link Content#copy(Source, Sink, Processor, Callback)} method, so that
|
||||
|
|
|
@ -148,14 +148,14 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump
|
|||
public int getTotalKeys()
|
||||
{
|
||||
int keys = 0;
|
||||
for (final ManagedSelector selector : _selectors)
|
||||
for (ManagedSelector selector : _selectors)
|
||||
{
|
||||
keys += selector.getTotalKeys();
|
||||
if (selector != null)
|
||||
keys += selector.getTotalKeys();
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the number of selectors in use
|
||||
*/
|
||||
|
|
|
@ -51,8 +51,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
|
||||
private final AutoLock.WithCondition lock = new AutoLock.WithCondition();
|
||||
private final SerializedInvoker invoker = new SerializedInvoker();
|
||||
private final Queue<AsyncChunk> chunks = new ArrayDeque<>();
|
||||
private Content.Chunk.Error errorChunk;
|
||||
private final Queue<Content.Chunk> chunks = new ArrayDeque<>();
|
||||
private Content.Chunk persistentFailure;
|
||||
private boolean readClosed;
|
||||
private boolean writeClosed;
|
||||
private Runnable demandCallback;
|
||||
|
@ -62,7 +62,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
* {@inheritDoc}
|
||||
* <p>The write completes:</p>
|
||||
* <ul>
|
||||
* <li>immediately with a failure when this instance is closed or already in error</li>
|
||||
* <li>immediately with a failure when this instance is closed or already has a failure</li>
|
||||
* <li>successfully when a non empty {@link Content.Chunk} returned by {@link #read()} is released</li>
|
||||
* <li>successfully just before the {@link Content.Chunk} is returned by {@link #read()},
|
||||
* for any empty chunk {@link Content.Chunk}.</li>
|
||||
|
@ -79,7 +79,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
* or succeeded if and only if the chunk is terminal, as non-terminal
|
||||
* chunks have to bind the succeeding of the callback to their release.
|
||||
*/
|
||||
private void offer(AsyncChunk chunk)
|
||||
private void offer(Content.Chunk chunk)
|
||||
{
|
||||
Throwable failure = null;
|
||||
boolean wasEmpty = false;
|
||||
|
@ -89,9 +89,9 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
{
|
||||
failure = new IOException("closed");
|
||||
}
|
||||
else if (errorChunk != null)
|
||||
else if (persistentFailure != null)
|
||||
{
|
||||
failure = errorChunk.getCause();
|
||||
failure = persistentFailure.getFailure();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -105,14 +105,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
if (length == UNDETERMINED_LENGTH)
|
||||
{
|
||||
length = 0;
|
||||
for (AsyncChunk c : chunks)
|
||||
for (Content.Chunk c : chunks)
|
||||
length += c.remaining();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null)
|
||||
chunk.failed(failure);
|
||||
if (failure != null && chunk instanceof AsyncChunk asyncChunk)
|
||||
asyncChunk.failed(failure);
|
||||
if (wasEmpty)
|
||||
invoker.run(this::invokeDemandCallback);
|
||||
}
|
||||
|
@ -125,14 +125,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
{
|
||||
// Always wrap the exception to make sure
|
||||
// the stack trace comes from flush().
|
||||
if (errorChunk != null)
|
||||
throw new IOException(errorChunk.getCause());
|
||||
if (persistentFailure != null)
|
||||
throw new IOException(persistentFailure.getFailure());
|
||||
if (chunks.isEmpty())
|
||||
return;
|
||||
// Special case for a last empty chunk that may not be read.
|
||||
if (writeClosed && chunks.size() == 1)
|
||||
{
|
||||
AsyncChunk chunk = chunks.peek();
|
||||
Content.Chunk chunk = chunks.peek();
|
||||
if (chunk.isLast() && !chunk.hasRemaining())
|
||||
return;
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
@Override
|
||||
public Content.Chunk read()
|
||||
{
|
||||
AsyncChunk current;
|
||||
Content.Chunk current;
|
||||
try (AutoLock.WithCondition condition = lock.lock())
|
||||
{
|
||||
if (length == UNDETERMINED_LENGTH)
|
||||
|
@ -181,8 +181,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
{
|
||||
if (readClosed)
|
||||
return Content.Chunk.EOF;
|
||||
if (errorChunk != null)
|
||||
return errorChunk;
|
||||
if (persistentFailure != null)
|
||||
return persistentFailure;
|
||||
return null;
|
||||
}
|
||||
readClosed = current.isLast();
|
||||
|
@ -195,7 +195,12 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
return current;
|
||||
|
||||
// If the chunk is not reference counted, we can succeed it now and return a chunk with a noop release.
|
||||
current.succeeded();
|
||||
if (current instanceof AsyncChunk asyncChunk)
|
||||
asyncChunk.succeeded();
|
||||
|
||||
if (Content.Chunk.isFailure(current))
|
||||
return current;
|
||||
|
||||
return current.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
|
||||
}
|
||||
|
||||
|
@ -208,7 +213,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
if (this.demandCallback != null)
|
||||
throw new IllegalStateException("demand pending");
|
||||
this.demandCallback = Objects.requireNonNull(demandCallback);
|
||||
invoke = !chunks.isEmpty() || readClosed || errorChunk != null;
|
||||
invoke = !chunks.isEmpty() || readClosed || persistentFailure != null;
|
||||
}
|
||||
if (invoke)
|
||||
invoker.run(this::invokeDemandCallback);
|
||||
|
@ -241,22 +246,35 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
|
|||
@Override
|
||||
public void fail(Throwable failure)
|
||||
{
|
||||
List<AsyncChunk> drained;
|
||||
List<Content.Chunk> drained;
|
||||
try (AutoLock.WithCondition condition = lock.lock())
|
||||
{
|
||||
if (readClosed)
|
||||
return;
|
||||
if (errorChunk != null)
|
||||
if (persistentFailure != null)
|
||||
return;
|
||||
errorChunk = Content.Chunk.from(failure);
|
||||
persistentFailure = Content.Chunk.from(failure);
|
||||
drained = List.copyOf(chunks);
|
||||
chunks.clear();
|
||||
condition.signal();
|
||||
}
|
||||
drained.forEach(ac -> ac.failed(failure));
|
||||
drained.forEach(c ->
|
||||
{
|
||||
if (c instanceof AsyncChunk ac)
|
||||
ac.failed(failure);
|
||||
});
|
||||
invoker.run(this::invokeDemandCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(Throwable failure, boolean last)
|
||||
{
|
||||
if (last)
|
||||
fail(failure);
|
||||
else
|
||||
offer(Content.Chunk.from(failure, false));
|
||||
}
|
||||
|
||||
public int count()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.io.content;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
|
||||
/**
|
||||
* <p>A utility class to convert content from a {@link Content.Source} to an instance
|
||||
* available via a {@link CompletableFuture}.</p>
|
||||
* <p>An example usage to asynchronously read UTF-8 content is:</p>
|
||||
* <pre>{@code
|
||||
* public static class CompletableUTF8String extends ContentSourceCompletableFuture<String>;
|
||||
* {
|
||||
* private final Utf8StringBuilder builder = new Utf8StringBuilder();
|
||||
*
|
||||
* public CompletableUTF8String(Content.Source content)
|
||||
* {
|
||||
* super(content);
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected String parse(Content.Chunk chunk) throws Throwable
|
||||
* {
|
||||
* // Accumulate the chunk bytes.
|
||||
* if (chunk.hasRemaining())
|
||||
* builder.append(chunk.getByteBuffer());
|
||||
*
|
||||
* // Not the last chunk, the result is not ready yet.
|
||||
* if (!chunk.isLast())
|
||||
* return null;
|
||||
*
|
||||
* // The result is ready.
|
||||
* return builder.takeCompleteString(IllegalStateException::new);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* new CompletableUTF8String(source).thenAccept(System.err::println);
|
||||
* }</pre>
|
||||
*/
|
||||
public abstract class ContentSourceCompletableFuture<X> extends CompletableFuture<X>
|
||||
{
|
||||
private final Content.Source _content;
|
||||
|
||||
public ContentSourceCompletableFuture(Content.Source content)
|
||||
{
|
||||
_content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Initiates the parsing of the {@link Content.Source}.</p>
|
||||
* <p>For every valid chunk that is read, {@link #parse(Content.Chunk)}
|
||||
* is called, until a result is produced that is used to
|
||||
* complete this {@link CompletableFuture}.</p>
|
||||
* <p>Internally, this method is called multiple times to progress
|
||||
* the parsing in response to {@link Content.Source#demand(Runnable)}
|
||||
* calls.</p>
|
||||
* <p>Exceptions thrown during parsing result in this
|
||||
* {@link CompletableFuture} to be completed exceptionally.</p>
|
||||
*/
|
||||
public void parse()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = _content.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
_content.demand(this::parse);
|
||||
return;
|
||||
}
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
if (!chunk.isLast() && onTransientFailure(chunk.getFailure()))
|
||||
continue;
|
||||
completeExceptionally(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
X x = parse(chunk);
|
||||
if (x != null)
|
||||
{
|
||||
complete(x);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Throwable failure)
|
||||
{
|
||||
completeExceptionally(failure);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
chunk.release();
|
||||
}
|
||||
|
||||
if (chunk.isLast())
|
||||
{
|
||||
completeExceptionally(new EOFException());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Called by {@link #parse()} to parse a {@link org.eclipse.jetty.io.Content.Chunk}.</p>
|
||||
*
|
||||
* @param chunk The chunk containing content to parse. The chunk will never be {@code null} nor a
|
||||
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}.
|
||||
* If the chunk is stored away to be used later beyond the scope of this call,
|
||||
* then implementations must call {@link Content.Chunk#retain()} and
|
||||
* {@link Content.Chunk#release()} as appropriate.
|
||||
* @return The parsed {@code X} result instance or {@code null} if parsing is not yet complete
|
||||
* @throws Throwable If there is an error parsing
|
||||
*/
|
||||
protected abstract X parse(Content.Chunk chunk) throws Throwable;
|
||||
|
||||
/**
|
||||
* <p>Callback method that informs the parsing about how to handle transient failures.</p>
|
||||
*
|
||||
* @param cause A transient failure obtained by reading a {@link Content.Chunk#isLast() non-last}
|
||||
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}
|
||||
* @return {@code true} if the transient failure can be ignored, {@code false} otherwise
|
||||
*/
|
||||
protected boolean onTransientFailure(Throwable cause)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -54,8 +54,12 @@ public class ContentSourceInputStream extends InputStream
|
|||
{
|
||||
if (chunk != null)
|
||||
{
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
throw IO.rethrow(error.getCause());
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
Content.Chunk c = chunk;
|
||||
chunk = null;
|
||||
throw IO.rethrow(c.getFailure());
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = chunk.getByteBuffer();
|
||||
if (chunk.isLast() && !byteBuffer.hasRemaining())
|
||||
|
@ -96,8 +100,8 @@ public class ContentSourceInputStream extends InputStream
|
|||
@Override
|
||||
public void close()
|
||||
{
|
||||
// If we have already reached a real EOF or an error, close is a noop.
|
||||
if (chunk == Content.Chunk.EOF || chunk instanceof Content.Chunk.Error)
|
||||
// If we have already reached a real EOF or a persistent failure, close is a noop.
|
||||
if (chunk == Content.Chunk.EOF || Content.Chunk.isFailure(chunk, true))
|
||||
return;
|
||||
|
||||
// If we have a chunk here, then it needs to be released
|
||||
|
|
|
@ -125,10 +125,10 @@ public class ContentSourcePublisher implements Flow.Publisher<Content.Chunk>
|
|||
return;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
terminate();
|
||||
subscriber.onError(error.getCause());
|
||||
subscriber.onError(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,10 +60,10 @@ public abstract class ContentSourceTransformer implements Content.Source
|
|||
return null;
|
||||
}
|
||||
|
||||
if (rawChunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(rawChunk))
|
||||
return rawChunk;
|
||||
|
||||
if (transformedChunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(transformedChunk))
|
||||
return transformedChunk;
|
||||
|
||||
transformedChunk = process(rawChunk);
|
||||
|
@ -142,13 +142,14 @@ public abstract class ContentSourceTransformer implements Content.Source
|
|||
* <p>The input chunk is released as soon as this method returns, so
|
||||
* implementations that must hold onto the input chunk must arrange to call
|
||||
* {@link Content.Chunk#retain()} and its correspondent {@link Content.Chunk#release()}.</p>
|
||||
* <p>Implementations should return an {@link Content.Chunk.Error error chunk} in case
|
||||
* <p>Implementations should return an {@link Content.Chunk} with non-null
|
||||
* {@link Content.Chunk#getFailure()} in case
|
||||
* of transformation errors.</p>
|
||||
* <p>Exceptions thrown by this method are equivalent to returning an error chunk.</p>
|
||||
* <p>Implementations of this method may return:</p>
|
||||
* <ul>
|
||||
* <li>{@code null}, if more input chunks are necessary to produce an output chunk</li>
|
||||
* <li>the {@code inputChunk} itself, typically in case of {@link Content.Chunk.Error}s,
|
||||
* <li>the {@code inputChunk} itself, typically in case of non-null {@link Content.Chunk#getFailure()},
|
||||
* or when no transformation is required</li>
|
||||
* <li>a new {@link Content.Chunk} derived from {@code inputChunk}.</li>
|
||||
* </ul>
|
||||
|
|
|
@ -40,7 +40,7 @@ public class InputStreamContentSource implements Content.Source
|
|||
private final ByteBufferPool bufferPool;
|
||||
private int bufferSize = 4096;
|
||||
private Runnable demandCallback;
|
||||
private Content.Chunk.Error errorChunk;
|
||||
private Content.Chunk errorChunk;
|
||||
private boolean closed;
|
||||
|
||||
public InputStreamContentSource(InputStream inputStream)
|
||||
|
|
|
@ -46,7 +46,7 @@ public class PathContentSource implements Content.Source
|
|||
private SeekableByteChannel channel;
|
||||
private long totalRead;
|
||||
private Runnable demandCallback;
|
||||
private Content.Chunk.Error errorChunk;
|
||||
private Content.Chunk errorChunk;
|
||||
|
||||
public PathContentSource(Path path)
|
||||
{
|
||||
|
|
|
@ -16,9 +16,13 @@ package org.eclipse.jetty.io.internal;
|
|||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IteratingNestedCallback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ContentCopier extends IteratingNestedCallback
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ContentCopier.class);
|
||||
|
||||
private final Content.Source source;
|
||||
private final Content.Sink sink;
|
||||
private final Content.Chunk.Processor chunkProcessor;
|
||||
|
@ -56,8 +60,15 @@ public class ContentCopier extends IteratingNestedCallback
|
|||
if (chunkProcessor != null && chunkProcessor.process(current, this))
|
||||
return Action.SCHEDULED;
|
||||
|
||||
if (current instanceof Error error)
|
||||
throw error.getCause();
|
||||
if (Content.Chunk.isFailure(current))
|
||||
{
|
||||
if (current.isLast())
|
||||
throw current.getFailure();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("ignored transient failure", current.getFailure());
|
||||
succeeded();
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
sink.write(current.isLast(), current.getByteBuffer(), this);
|
||||
return Action.SCHEDULED;
|
||||
|
|
|
@ -44,9 +44,9 @@ public class ContentSourceByteBuffer implements Runnable
|
|||
return;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
promise.failed(error.getCause());
|
||||
promise.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,9 +41,9 @@ public class ContentSourceConsumer implements Invocable.Task
|
|||
return;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
callback.failed(error.getCause());
|
||||
callback.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,9 +42,9 @@ public class ContentSourceString
|
|||
content.demand(this::convert);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
promise.failed(error.getCause());
|
||||
promise.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
text.append(chunk.getByteBuffer());
|
||||
|
|
|
@ -33,7 +33,6 @@ import static org.hamcrest.Matchers.is;
|
|||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -101,7 +100,7 @@ public class AsyncContentTest
|
|||
|
||||
// We must read the error.
|
||||
chunk = async.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
// Offering more should fail.
|
||||
CountDownLatch failLatch = new CountDownLatch(1);
|
||||
|
@ -209,14 +208,14 @@ public class AsyncContentTest
|
|||
assertThat(chunk.release(), is(true));
|
||||
callback1.assertNoFailureWithSuccesses(1);
|
||||
|
||||
Exception error1 = new Exception("test1");
|
||||
async.fail(error1);
|
||||
Exception failure1 = new Exception("test1");
|
||||
async.fail(failure1);
|
||||
|
||||
chunk = async.read();
|
||||
assertSame(error1, ((Content.Chunk.Error)chunk).getCause());
|
||||
assertSame(failure1, chunk.getFailure());
|
||||
|
||||
callback2.assertSingleFailureSameInstanceNoSuccess(error1);
|
||||
callback3.assertSingleFailureSameInstanceNoSuccess(error1);
|
||||
callback2.assertSingleFailureSameInstanceNoSuccess(failure1);
|
||||
callback3.assertSingleFailureSameInstanceNoSuccess(failure1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ package org.eclipse.jetty.io;
|
|||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
@ -27,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedDeque;
|
|||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
@ -49,13 +52,14 @@ import org.junit.jupiter.params.provider.MethodSource;
|
|||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class ContentSourceTest
|
||||
{
|
||||
|
@ -237,7 +241,7 @@ public class ContentSourceTest
|
|||
|
||||
// We must read the error.
|
||||
chunk = source.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
@ -259,7 +263,7 @@ public class ContentSourceTest
|
|||
source.fail(new CancellationException());
|
||||
|
||||
Content.Chunk chunk = source.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
source.demand(latch::countDown);
|
||||
|
@ -285,7 +289,7 @@ public class ContentSourceTest
|
|||
});
|
||||
|
||||
chunk = source.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -560,4 +564,63 @@ public class ContentSourceTest
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentWithWarnings()
|
||||
{
|
||||
AsyncContent content = new AsyncContent();
|
||||
|
||||
Content.Sink.write(content, false, "One", Callback.NOOP);
|
||||
content.fail(new TimeoutException("test"), false);
|
||||
Content.Sink.write(content, true, "Two", Callback.NOOP);
|
||||
|
||||
Content.Chunk chunk = content.read();
|
||||
assertFalse(chunk.isLast());
|
||||
assertFalse(Content.Chunk.isFailure(chunk));
|
||||
assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("One"));
|
||||
|
||||
chunk = content.read();
|
||||
assertFalse(chunk.isLast());
|
||||
assertTrue(Content.Chunk.isFailure(chunk));
|
||||
assertThat(chunk.getFailure(), instanceOf(TimeoutException.class));
|
||||
|
||||
chunk = content.read();
|
||||
assertTrue(chunk.isLast());
|
||||
assertFalse(Content.Chunk.isFailure(chunk));
|
||||
assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("Two"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncContentWithWarningsAsInputStream() throws Exception
|
||||
{
|
||||
AsyncContent content = new AsyncContent();
|
||||
|
||||
Content.Sink.write(content, false, "One", Callback.NOOP);
|
||||
content.fail(new TimeoutException("test"), false);
|
||||
Content.Sink.write(content, true, "Two", Callback.NOOP);
|
||||
|
||||
InputStream in = Content.Source.asInputStream(content);
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int len = in.read(buffer);
|
||||
assertThat(len, is(3));
|
||||
assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("One"));
|
||||
|
||||
try
|
||||
{
|
||||
int ignored = in.read();
|
||||
fail();
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
assertThat(ioe.getCause(), instanceOf(TimeoutException.class));
|
||||
}
|
||||
|
||||
len = in.read(buffer);
|
||||
assertThat(len, is(3));
|
||||
assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("Two"));
|
||||
|
||||
len = in.read(buffer);
|
||||
assertThat(len, is(-1));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -259,11 +258,11 @@ public class ContentSourceTransformerTest
|
|||
chunk.release();
|
||||
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
// Trying to read again returns the error again.
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
// Make sure that the source is failed.
|
||||
assertEquals(0, source.count());
|
||||
|
@ -284,11 +283,11 @@ public class ContentSourceTransformerTest
|
|||
chunk.release();
|
||||
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
// Trying to read again returns the error again.
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -306,11 +305,11 @@ public class ContentSourceTransformerTest
|
|||
source.fail(new IOException());
|
||||
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
|
||||
// Trying to read again returns the error again.
|
||||
chunk = transformer.read();
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
}
|
||||
|
||||
private static class WordSplitLowCaseTransformer extends ContentSourceTransformer
|
||||
|
|
|
@ -43,11 +43,22 @@ public class RewriteHandler extends Handler.Wrapper
|
|||
|
||||
public RewriteHandler()
|
||||
{
|
||||
this(new RuleContainer());
|
||||
this(null, new RuleContainer());
|
||||
}
|
||||
|
||||
public RewriteHandler(RuleContainer rules)
|
||||
{
|
||||
this(null, rules);
|
||||
}
|
||||
|
||||
public RewriteHandler(Handler handler)
|
||||
{
|
||||
this(handler, new RuleContainer());
|
||||
}
|
||||
|
||||
public RewriteHandler(Handler handler, RuleContainer rules)
|
||||
{
|
||||
super(handler);
|
||||
_rules = rules;
|
||||
addBean(_rules);
|
||||
}
|
||||
|
|
|
@ -87,6 +87,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu
|
|||
|
||||
protected SecurityHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
protected SecurityHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
|
||||
}
|
||||
|
||||
|
@ -645,6 +651,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu
|
|||
|
||||
public PathMapped()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public PathMapped(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
public Constraint put(String pathSpec, Constraint constraint)
|
||||
|
|
|
@ -24,24 +24,15 @@
|
|||
<!-- configuration that may be set here. -->
|
||||
<!-- =============================================================== -->
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
<Arg name="threadPool"><Ref refid="threadPool"/></Arg>
|
||||
|
||||
<Call name="addBean">
|
||||
<Arg><Ref refid="byteBufferPool"/></Arg>
|
||||
</Call>
|
||||
|
||||
<!-- =========================================================== -->
|
||||
<!-- Add shared Scheduler instance -->
|
||||
<!-- =========================================================== -->
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
|
||||
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
|
||||
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
|
||||
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
<Arg name="threadPool"><Ref refid="threadPool"/></Arg>
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
|
||||
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
|
||||
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
|
||||
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
|
||||
</New>
|
||||
</Arg>
|
||||
<Arg><Ref refid="byteBufferPool"/></Arg>
|
||||
|
||||
<!-- =========================================================== -->
|
||||
<!-- Http Configuration. -->
|
||||
|
|
|
@ -22,6 +22,8 @@ import java.util.concurrent.CompletableFuture;
|
|||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.CharsetStringBuilder;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
|
@ -33,7 +35,7 @@ import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte;
|
|||
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
|
||||
* content has been parsed asynchronously from the {@link Content.Source}.
|
||||
*/
|
||||
public class FormFields extends CompletableFuture<Fields> implements Runnable
|
||||
public class FormFields extends ContentSourceCompletableFuture<Fields>
|
||||
{
|
||||
public static final String MAX_FIELDS_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormKeys";
|
||||
public static final String MAX_LENGTH_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormContentSize";
|
||||
|
@ -57,29 +59,22 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
|
|||
return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Fields> from(Request request)
|
||||
{
|
||||
// TODO make this attributes provided by the ContextRequest wrapper
|
||||
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
|
||||
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
|
||||
|
||||
return from(request, maxFields, maxLength);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Fields> from(Request request, Charset charset)
|
||||
{
|
||||
// TODO make this attributes provided by the ContextRequest wrapper
|
||||
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
|
||||
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
|
||||
|
||||
return from(request, charset, maxFields, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Fields} or related failure for the request
|
||||
* @param request The request to which to associate the fields with
|
||||
* @param fields A {@link CompletableFuture} that will provide either the fields or a failure.
|
||||
*/
|
||||
public static void set(Request request, CompletableFuture<Fields> fields)
|
||||
{
|
||||
request.setAttribute(FormFields.class.getName(), fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request The request to enquire from
|
||||
* @return A {@link CompletableFuture} that will provide either the fields or a failure, or null if none set.
|
||||
* @see #from(Request)
|
||||
*
|
||||
*/
|
||||
public static CompletableFuture<Fields> get(Request request)
|
||||
{
|
||||
Object attr = request.getAttribute(FormFields.class.getName());
|
||||
|
@ -88,26 +83,93 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
|
|||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a {@link FormFields} from a {@link Content.Source}.
|
||||
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
|
||||
* using the classname as the attribute name, else the request is used
|
||||
* as a {@link Content.Source} from which to read the fields and set the attribute.
|
||||
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
|
||||
* @see #from(Content.Source, Attributes, Charset, int, int)
|
||||
*/
|
||||
public static CompletableFuture<Fields> from(Request request)
|
||||
{
|
||||
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
|
||||
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
|
||||
return from(request, maxFields, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a {@link FormFields} from a {@link Content.Source}.
|
||||
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
|
||||
* using the classname as the attribute name, else the request is used
|
||||
* as a {@link Content.Source} from which to read the fields and set the attribute.
|
||||
* @param charset the {@link Charset} to use for byte to string conversion.
|
||||
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
|
||||
* @see #from(Content.Source, Attributes, Charset, int, int)
|
||||
*/
|
||||
public static CompletableFuture<Fields> from(Request request, Charset charset)
|
||||
{
|
||||
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
|
||||
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
|
||||
return from(request, charset, maxFields, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a {@link FormFields} from a {@link Content.Source}.
|
||||
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
|
||||
* using the classname as the attribute name, else the request is used
|
||||
* as a {@link Content.Source} from which to read the fields and set the attribute.
|
||||
* @param maxFields The maximum number of fields to be parsed
|
||||
* @param maxLength The maximum total size of the fields
|
||||
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
|
||||
* @see #from(Content.Source, Attributes, Charset, int, int)
|
||||
*/
|
||||
public static CompletableFuture<Fields> from(Request request, int maxFields, int maxLength)
|
||||
{
|
||||
Object attr = request.getAttribute(FormFields.class.getName());
|
||||
return from(request, getFormEncodedCharset(request), maxFields, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a {@link FormFields} from a {@link Content.Source}.
|
||||
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
|
||||
* using the classname as the attribute name, else the request is used
|
||||
* as a {@link Content.Source} from which to read the fields and set the attribute.
|
||||
* @param charset the {@link Charset} to use for byte to string conversion.
|
||||
* @param maxFields The maximum number of fields to be parsed
|
||||
* @param maxLength The maximum total size of the fields
|
||||
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
|
||||
* @see #from(Content.Source, Attributes, Charset, int, int)
|
||||
*/
|
||||
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
|
||||
{
|
||||
return from(request, request, charset, maxFields, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create a {@link FormFields} from a {@link Content.Source}.
|
||||
* @param source The {@link Content.Source} from which to read the fields.
|
||||
* @param attributes The {@link Attributes} in which to look for an existing {@link CompletableFuture} of
|
||||
* {@link FormFields}, using the classname as the attribute name. If not found the attribute
|
||||
* is set with the created {@link CompletableFuture} of {@link FormFields}.
|
||||
* @param charset the {@link Charset} to use for byte to string conversion.
|
||||
* @param maxFields The maximum number of fields to be parsed
|
||||
* @param maxLength The maximum total size of the fields
|
||||
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
|
||||
*/
|
||||
static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
|
||||
{
|
||||
Object attr = attributes.getAttribute(FormFields.class.getName());
|
||||
if (attr instanceof FormFields futureFormFields)
|
||||
return futureFormFields;
|
||||
else if (attr instanceof Fields fields)
|
||||
return CompletableFuture.completedFuture(fields);
|
||||
|
||||
Charset charset = getFormEncodedCharset(request);
|
||||
if (charset == null)
|
||||
return EMPTY;
|
||||
|
||||
return from(request, charset, maxFields, maxLength);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
|
||||
{
|
||||
FormFields futureFormFields = new FormFields(request, charset, maxFields, maxLength);
|
||||
request.setAttribute(FormFields.class.getName(), futureFormFields);
|
||||
futureFormFields.run();
|
||||
FormFields futureFormFields = new FormFields(source, charset, maxFields, maxLength);
|
||||
attributes.setAttribute(FormFields.class.getName(), futureFormFields);
|
||||
futureFormFields.parse();
|
||||
return futureFormFields;
|
||||
}
|
||||
|
||||
|
@ -126,7 +188,6 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
|
|||
}
|
||||
}
|
||||
|
||||
private final Content.Source _source;
|
||||
private final Fields _fields;
|
||||
private final CharsetStringBuilder _builder;
|
||||
private final int _maxFields;
|
||||
|
@ -136,9 +197,9 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
|
|||
private int _percent = 0;
|
||||
private byte _percentCode;
|
||||
|
||||
public FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
|
||||
private FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
|
||||
{
|
||||
_source = source;
|
||||
super(source);
|
||||
_maxFields = maxFields;
|
||||
_maxLength = maxSize;
|
||||
_builder = CharsetStringBuilder.forCharset(charset);
|
||||
|
@ -146,137 +207,91 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
|
|||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
Content.Chunk chunk = null;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
chunk = _source.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
_source.demand(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
{
|
||||
completeExceptionally(error.getCause());
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
Fields.Field field = parse(chunk);
|
||||
if (field == null)
|
||||
break;
|
||||
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
|
||||
{
|
||||
chunk.release();
|
||||
// Do not double release if completeExceptionally() throws.
|
||||
chunk = null;
|
||||
completeExceptionally(new IllegalStateException("form with too many fields"));
|
||||
return;
|
||||
}
|
||||
_fields.add(field);
|
||||
}
|
||||
|
||||
chunk.release();
|
||||
if (chunk.isLast())
|
||||
{
|
||||
// Do not double release if complete() throws.
|
||||
chunk = null;
|
||||
complete(_fields);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (chunk != null)
|
||||
chunk.release();
|
||||
completeExceptionally(x);
|
||||
}
|
||||
}
|
||||
|
||||
protected Fields.Field parse(Content.Chunk chunk) throws CharacterCodingException
|
||||
protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
|
||||
{
|
||||
String value = null;
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
loop:
|
||||
while (BufferUtil.hasContent(buffer))
|
||||
|
||||
do
|
||||
{
|
||||
byte b = buffer.get();
|
||||
switch (_percent)
|
||||
loop:
|
||||
while (BufferUtil.hasContent(buffer))
|
||||
{
|
||||
case 1 ->
|
||||
byte b = buffer.get();
|
||||
switch (_percent)
|
||||
{
|
||||
_percentCode = b;
|
||||
_percent++;
|
||||
continue;
|
||||
case 1 ->
|
||||
{
|
||||
_percentCode = b;
|
||||
_percent++;
|
||||
continue;
|
||||
}
|
||||
case 2 ->
|
||||
{
|
||||
_builder.append(decodeHexByte((char)_percentCode, (char)b));
|
||||
_percent = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
case 2 ->
|
||||
|
||||
if (_name == null)
|
||||
{
|
||||
_builder.append(decodeHexByte((char)_percentCode, (char)b));
|
||||
_percent = 0;
|
||||
continue;
|
||||
switch (b)
|
||||
{
|
||||
case '=' ->
|
||||
{
|
||||
_name = _builder.build();
|
||||
checkLength(_name);
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (b)
|
||||
{
|
||||
case '&' ->
|
||||
{
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
break loop;
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_name == null)
|
||||
if (_name != null)
|
||||
{
|
||||
switch (b)
|
||||
if (value == null && chunk.isLast())
|
||||
{
|
||||
case '=' ->
|
||||
if (_percent > 0)
|
||||
{
|
||||
_name = _builder.build();
|
||||
checkLength(_name);
|
||||
_builder.append((byte)'%');
|
||||
_builder.append(_percentCode);
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (b)
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
case '&' ->
|
||||
{
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
break loop;
|
||||
}
|
||||
case '+' -> _builder.append((byte)' ');
|
||||
case '%' -> _percent++;
|
||||
default -> _builder.append(b);
|
||||
Fields.Field field = new Fields.Field(_name, value);
|
||||
_name = null;
|
||||
value = null;
|
||||
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
|
||||
throw new IllegalStateException("form with too many fields > " + _maxFields);
|
||||
_fields.add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (BufferUtil.hasContent(buffer));
|
||||
|
||||
if (_name != null)
|
||||
{
|
||||
if (value == null && chunk.isLast())
|
||||
{
|
||||
if (_percent > 0)
|
||||
{
|
||||
_builder.append((byte)'%');
|
||||
_builder.append(_percentCode);
|
||||
}
|
||||
value = _builder.build();
|
||||
checkLength(value);
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
Fields.Field field = new Fields.Field(_name, value);
|
||||
_name = null;
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return chunk.isLast() ? _fields : null;
|
||||
}
|
||||
|
||||
private void checkLength(String nameOrValue)
|
||||
|
|
|
@ -216,21 +216,6 @@ public interface Handler extends LifeCycle, Destroyable, Request.Handler
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Make a {@link Container} the parent of a {@link Handler}</p>
|
||||
* @param parent The {@link Container} that will be the parent
|
||||
* @param handler The {@link Handler} that will be the child
|
||||
*/
|
||||
static void setAsParent(Container parent, Handler handler)
|
||||
{
|
||||
if (parent instanceof Collection collection)
|
||||
collection.addHandler(handler);
|
||||
else if (parent instanceof Singleton wrapper)
|
||||
wrapper.setHandler(handler);
|
||||
else if (parent != null)
|
||||
throw new IllegalArgumentException("Unknown parent type: " + parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.eclipse.jetty.util.StaticException;
|
|||
/**
|
||||
* A HttpStream is an abstraction that together with {@link MetaData.Request}, represents the
|
||||
* flow of data from and to a single request and response cycle. It is roughly analogous to the
|
||||
* Stream within a HTTP/2 connection, in that a connection can have many streams, each used once
|
||||
* Stream within an HTTP/2 connection, in that a connection can have many streams, each used once
|
||||
* and each representing a single request and response exchange.
|
||||
*/
|
||||
public interface HttpStream extends Callback
|
||||
|
@ -42,7 +42,7 @@ public interface HttpStream extends Callback
|
|||
|
||||
/**
|
||||
* @return an ID unique within the lifetime scope of the associated protocol connection.
|
||||
* This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol.
|
||||
* This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
|
@ -50,7 +50,7 @@ public interface HttpStream extends Callback
|
|||
* <p>Reads a chunk of content, with the same semantic as {@link Content.Source#read()}.</p>
|
||||
* <p>This method is called from the implementation of {@link Request#read()}.</p>
|
||||
*
|
||||
* @return a chunk of content, possibly an {@link Chunk.Error error} or {@code null}.
|
||||
* @return a chunk of content, possibly with non-null {@link Chunk#getFailure()} or {@code null}.
|
||||
*/
|
||||
Content.Chunk read();
|
||||
|
||||
|
@ -125,8 +125,8 @@ public interface HttpStream extends Callback
|
|||
content.release();
|
||||
|
||||
// if the input failed, then fail the stream for same reason
|
||||
if (content instanceof Chunk.Error error)
|
||||
return error.getCause();
|
||||
if (Content.Chunk.isFailure(content))
|
||||
return content.getFailure();
|
||||
|
||||
if (content.isLast())
|
||||
return null;
|
||||
|
|
|
@ -20,22 +20,22 @@ import java.net.SocketAddress;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.http.CookieCache;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.Trailers;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.internal.HttpChannelState;
|
||||
|
@ -69,12 +69,14 @@ import org.eclipse.jetty.util.thread.Invocable;
|
|||
* return true;
|
||||
* }
|
||||
*
|
||||
* if (chunk instanceof Content.Chunk.Error error)
|
||||
* if (Content.Chunk.isError(chunk))
|
||||
* {
|
||||
* Throwable failure = error.getCause();
|
||||
*
|
||||
* // Handle errors.
|
||||
* // Mark the handling as complete, either generating a custom
|
||||
* // If the chunk is not last, then the error can be ignored and reading can be tried again.
|
||||
* // Otherwise, if the chunk is last, or we do not wish to ignore a non-last error, then
|
||||
* // mark the handling as complete, either generating a custom
|
||||
* // response and succeeding the callback, or failing the callback.
|
||||
* callback.failed(failure);
|
||||
* return true;
|
||||
|
@ -117,10 +119,11 @@ public interface Request extends Attributes, Content.Source
|
|||
{
|
||||
String CACHE_ATTRIBUTE = Request.class.getCanonicalName() + ".CookieCache";
|
||||
String COOKIE_ATTRIBUTE = Request.class.getCanonicalName() + ".Cookies";
|
||||
List<Locale> DEFAULT_LOCALES = List.of(Locale.getDefault());
|
||||
|
||||
/**
|
||||
* an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}).
|
||||
* This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol.
|
||||
* This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol.
|
||||
*
|
||||
* @see HttpStream#getId()
|
||||
*/
|
||||
|
@ -241,7 +244,7 @@ public interface Request extends Attributes, Content.Source
|
|||
/**
|
||||
* Consume any available content. This bypasses any request wrappers to process the content in
|
||||
* {@link Request#read()} and reads directly from the {@link HttpStream}. This reads until
|
||||
* there is no content currently available or it reaches EOF.
|
||||
* there is no content currently available, or it reaches EOF.
|
||||
* The {@link HttpConfiguration#setMaxUnconsumedRequestContentReads(int)} configuration can be used
|
||||
* to configure how many reads will be attempted by this method.
|
||||
* @return true if the content was fully consumed.
|
||||
|
@ -358,7 +361,7 @@ public interface Request extends Attributes, Content.Source
|
|||
: address.getHostAddress();
|
||||
return HostPort.normalizeHost(result);
|
||||
}
|
||||
return local.toString();
|
||||
return local == null ? null : local.toString();
|
||||
}
|
||||
|
||||
static int getLocalPort(Request request)
|
||||
|
@ -387,7 +390,7 @@ public interface Request extends Attributes, Content.Source
|
|||
: address.getHostAddress();
|
||||
return HostPort.normalizeHost(result);
|
||||
}
|
||||
return remote.toString();
|
||||
return remote == null ? null : remote.toString();
|
||||
}
|
||||
|
||||
static int getRemotePort(Request request)
|
||||
|
@ -417,7 +420,7 @@ public interface Request extends Attributes, Content.Source
|
|||
if (local instanceof InetSocketAddress)
|
||||
return HostPort.normalizeHost(((InetSocketAddress)local).getHostString());
|
||||
|
||||
return local.toString();
|
||||
return local == null ? null : local.toString();
|
||||
}
|
||||
|
||||
static int getServerPort(Request request)
|
||||
|
@ -450,26 +453,27 @@ public interface Request extends Attributes, Content.Source
|
|||
{
|
||||
HttpFields fields = request.getHeaders();
|
||||
if (fields == null)
|
||||
return List.of(Locale.getDefault());
|
||||
return DEFAULT_LOCALES;
|
||||
|
||||
List<String> acceptable = fields.getQualityCSV(HttpHeader.ACCEPT_LANGUAGE);
|
||||
|
||||
// handle no locale
|
||||
if (acceptable.isEmpty())
|
||||
return List.of(Locale.getDefault());
|
||||
|
||||
return acceptable.stream().map(language ->
|
||||
// return sorted list of locals, with known locales in quality order before unknown locales in quality order
|
||||
return switch (acceptable.size())
|
||||
{
|
||||
language = HttpField.stripParameters(language);
|
||||
String country = "";
|
||||
int dash = language.indexOf('-');
|
||||
if (dash > -1)
|
||||
case 0 -> DEFAULT_LOCALES;
|
||||
case 1 -> List.of(Locale.forLanguageTag(acceptable.get(0)));
|
||||
default ->
|
||||
{
|
||||
country = language.substring(dash + 1).trim();
|
||||
language = language.substring(0, dash).trim();
|
||||
List<Locale> locales = acceptable.stream().map(Locale::forLanguageTag).toList();
|
||||
List<Locale> known = locales.stream().filter(MimeTypes::isKnownLocale).toList();
|
||||
if (known.size() == locales.size())
|
||||
yield locales; // All locales are known
|
||||
List<Locale> unknown = locales.stream().filter(l -> !MimeTypes.isKnownLocale(l)).toList();
|
||||
locales = new ArrayList<>(known);
|
||||
locales.addAll(unknown);
|
||||
yield locales; // List of known locales before unknown locales
|
||||
}
|
||||
return new Locale(language, country);
|
||||
}).collect(Collectors.toList());
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: consider inline and remove.
|
||||
|
@ -605,7 +609,7 @@ public interface Request extends Attributes, Content.Source
|
|||
* @param request the HTTP request to handle
|
||||
* @param response the HTTP response to handle
|
||||
* @param callback the callback to complete when the handling is complete
|
||||
* @return True if an only if the request will be handled, a response generated and the callback eventually called.
|
||||
* @return True if and only if the request will be handled, a response generated and the callback eventually called.
|
||||
* This may occur within the scope of the call to this method, or asynchronously some time later. If false
|
||||
* is returned, then this method must not generate a response, nor complete the callback.
|
||||
* @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be
|
||||
|
|
|
@ -51,7 +51,7 @@ public class ResourceListing
|
|||
* @param base The base URL
|
||||
* @param parent True if the parent directory should be included
|
||||
* @param query query params
|
||||
* @return the HTML as String
|
||||
* @return the XHTML as String
|
||||
*/
|
||||
public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
|
||||
{
|
||||
|
@ -108,7 +108,8 @@ public class ResourceListing
|
|||
|
||||
StringBuilder buf = new StringBuilder(4096);
|
||||
|
||||
// Doctype Declaration + XHTML
|
||||
// Doctype Declaration + XHTML. The spec says the encoding MUST be "utf-8" in all cases at it is ignored;
|
||||
// see: https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset
|
||||
buf.append("""
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
|
|
|
@ -16,6 +16,7 @@ package org.eclipse.jetty.server;
|
|||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -440,11 +441,12 @@ public class ResourceService
|
|||
|
||||
protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
if (!Objects.requireNonNull(content).getResource().isDirectory())
|
||||
throw new IllegalArgumentException("content must be a directory");
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("sendWelcome(content={}, pathInContext={}, endsWithSlash={}, req={}, resp={}, callback={})",
|
||||
content, pathInContext, endsWithSlash, request, response, callback);
|
||||
}
|
||||
|
||||
// Redirect to directory
|
||||
if (!endsWithSlash)
|
||||
|
@ -460,7 +462,7 @@ public class ResourceService
|
|||
}
|
||||
|
||||
// process optional Welcome behaviors
|
||||
if (welcome(request, response, callback))
|
||||
if (welcome(content, request, response, callback))
|
||||
return;
|
||||
|
||||
if (!passConditionalHeaders(request, response, content, callback))
|
||||
|
@ -499,9 +501,9 @@ public class ResourceService
|
|||
{
|
||||
}
|
||||
|
||||
private boolean welcome(Request request, Response response, Callback callback) throws Exception
|
||||
private boolean welcome(HttpContent content, Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
WelcomeAction welcomeAction = processWelcome(request);
|
||||
WelcomeAction welcomeAction = processWelcome(content, request);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("welcome(req={}, rsp={}, cbk={}) welcomeAction={}", request, response, callback, welcomeAction);
|
||||
|
||||
|
@ -581,18 +583,15 @@ public class ResourceService
|
|||
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
|
||||
}
|
||||
|
||||
private WelcomeAction processWelcome(Request request) throws IOException
|
||||
private WelcomeAction processWelcome(HttpContent content, Request request) throws IOException
|
||||
{
|
||||
String welcomeTarget = getWelcomeFactory().getWelcomeTarget(request);
|
||||
String welcomeTarget = getWelcomeFactory().getWelcomeTarget(content, request);
|
||||
if (welcomeTarget == null)
|
||||
return null;
|
||||
|
||||
String contextPath = request.getContext().getContextPath();
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("welcome={}", welcomeTarget);
|
||||
|
||||
WelcomeMode welcomeMode = getWelcomeMode();
|
||||
|
||||
welcomeTarget = switch (welcomeMode)
|
||||
{
|
||||
case REDIRECT, REHANDLE -> HttpURI.build(request.getHttpURI())
|
||||
|
@ -601,6 +600,9 @@ public class ResourceService
|
|||
case SERVE -> welcomeTarget;
|
||||
};
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("welcome {} {}", welcomeMode, welcomeTarget);
|
||||
|
||||
return new WelcomeAction(welcomeTarget, welcomeMode);
|
||||
}
|
||||
|
||||
|
@ -625,8 +627,10 @@ public class ResourceService
|
|||
return;
|
||||
}
|
||||
|
||||
byte[] data = listing.getBytes(StandardCharsets.UTF_8);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=utf-8");
|
||||
String characterEncoding = httpContent.getCharacterEncoding();
|
||||
Charset charset = characterEncoding == null ? StandardCharsets.UTF_8 : Charset.forName(characterEncoding);
|
||||
byte[] data = listing.getBytes(charset);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=" + charset.name());
|
||||
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, data.length);
|
||||
response.write(true, ByteBuffer.wrap(data), callback);
|
||||
}
|
||||
|
@ -892,7 +896,7 @@ public class ResourceService
|
|||
* @return The URI path of the matching welcome target in context or null
|
||||
* if no welcome target was found
|
||||
*/
|
||||
String getWelcomeTarget(Request request) throws IOException;
|
||||
String getWelcomeTarget(HttpContent content, Request request) throws IOException;
|
||||
}
|
||||
|
||||
private static class ContentWriterIteratingCallback extends IteratingCallback
|
||||
|
|
|
@ -65,6 +65,12 @@ public class BufferedResponseHandler extends Handler.Wrapper
|
|||
|
||||
public BufferedResponseHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public BufferedResponseHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
_methods.include(HttpMethod.GET.asString());
|
||||
for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
|
||||
{
|
||||
|
|
|
@ -77,7 +77,7 @@ public class ConnectHandler extends Handler.Wrapper
|
|||
|
||||
public ConnectHandler(Handler handler)
|
||||
{
|
||||
setHandler(handler);
|
||||
super(handler);
|
||||
}
|
||||
|
||||
public Executor getExecutor()
|
||||
|
|
|
@ -159,11 +159,22 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
|
|||
|
||||
public ContextHandler()
|
||||
{
|
||||
this(null);
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public ContextHandler(Handler handler)
|
||||
{
|
||||
this(handler, null);
|
||||
}
|
||||
|
||||
public ContextHandler(String contextPath)
|
||||
{
|
||||
this(null, contextPath);
|
||||
}
|
||||
|
||||
public ContextHandler(Handler handler, String contextPath)
|
||||
{
|
||||
super(handler);
|
||||
_context = newContext();
|
||||
if (contextPath != null)
|
||||
setContextPath(contextPath);
|
||||
|
@ -180,14 +191,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
|
|||
_classLoader = classLoader;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public ContextHandler(Handler.Container parent, String contextPath)
|
||||
{
|
||||
this(contextPath);
|
||||
Container.setAsParent(parent, this);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServer(Server server)
|
||||
{
|
||||
|
|
|
@ -41,6 +41,16 @@ public class DebugHandler extends Handler.Wrapper implements Connection.Listener
|
|||
private OutputStream _out;
|
||||
private PrintStream _print;
|
||||
|
||||
public DebugHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DebugHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
@ -25,8 +24,6 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.MultiPart;
|
||||
import org.eclipse.jetty.http.MultiPartFormData;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.FormFields;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
|
@ -38,6 +35,16 @@ import org.eclipse.jetty.util.StringUtil;
|
|||
|
||||
public class DelayedHandler extends Handler.Wrapper
|
||||
{
|
||||
public DelayedHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DelayedHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
|
@ -105,7 +112,6 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
return switch (mimeType)
|
||||
{
|
||||
case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType);
|
||||
case MULTIPART_FORM_DATA -> new UntilMultiPartDelayedProcess(handler, request, response, callback, contentType);
|
||||
default -> new UntilContentDelayedProcess(handler, request, response, callback);
|
||||
};
|
||||
}
|
||||
|
@ -260,102 +266,4 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
Response.writeError(getRequest(), getResponse(), getCallback(), x);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class UntilMultiPartDelayedProcess extends DelayedProcess
|
||||
{
|
||||
private final MultiPartFormData _formData;
|
||||
|
||||
public UntilMultiPartDelayedProcess(Handler handler, Request wrapped, Response response, Callback callback, String contentType)
|
||||
{
|
||||
super(handler, wrapped, response, callback);
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
_formData = boundary == null ? null : new MultiPartFormData(boundary);
|
||||
}
|
||||
|
||||
private void process(MultiPartFormData.Parts parts, Throwable x)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
getRequest().setAttribute(MultiPartFormData.Parts.class.getName(), parts);
|
||||
super.process();
|
||||
}
|
||||
else
|
||||
{
|
||||
Response.writeError(getRequest(), getResponse(), getCallback(), x);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeProcess(MultiPartFormData.Parts parts, Throwable x)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
// We must execute here as even though we have consumed all the input, we are probably
|
||||
// invoked in a demand runnable that is serialized with any write callbacks that might be done in process
|
||||
getRequest().getContext().execute(() -> process(parts, x));
|
||||
}
|
||||
else
|
||||
{
|
||||
Response.writeError(getRequest(), getResponse(), getCallback(), x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delay()
|
||||
{
|
||||
if (_formData == null)
|
||||
{
|
||||
this.process();
|
||||
}
|
||||
else
|
||||
{
|
||||
_formData.setFilesDirectory(getRequest().getContext().getTempDirectory().toPath());
|
||||
readAndParse();
|
||||
// if we are done already, then we are still in the scope of the original process call and can
|
||||
// process directly, otherwise we must execute a call to process as we are within a serialized
|
||||
// demand callback.
|
||||
if (_formData.isDone())
|
||||
{
|
||||
try
|
||||
{
|
||||
MultiPartFormData.Parts parts = _formData.join();
|
||||
process(parts, null);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
process(null, t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_formData.whenComplete(this::executeProcess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readAndParse()
|
||||
{
|
||||
while (!_formData.isDone())
|
||||
{
|
||||
Content.Chunk chunk = getRequest().read();
|
||||
if (chunk == null)
|
||||
{
|
||||
getRequest().demand(this::readAndParse);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
{
|
||||
_formData.completeExceptionally(error.getCause());
|
||||
return;
|
||||
}
|
||||
_formData.parse(chunk);
|
||||
chunk.release();
|
||||
if (chunk.isLast())
|
||||
{
|
||||
if (!_formData.isDone())
|
||||
process(null, new IOException("Incomplete multipart"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ public abstract class EventsHandler extends Handler.Wrapper
|
|||
|
||||
public EventsHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public EventsHandler(Handler handler)
|
||||
|
@ -160,7 +161,7 @@ public abstract class EventsHandler extends Handler.Wrapper
|
|||
{
|
||||
try
|
||||
{
|
||||
onResponseWrite(request, last, content.asReadOnlyBuffer());
|
||||
onResponseWrite(request, last, content == null ? null : content.asReadOnlyBuffer());
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
|
@ -229,7 +230,7 @@ public abstract class EventsHandler extends Handler.Wrapper
|
|||
* {@link Request#read()}).
|
||||
*
|
||||
* @param request the request object. The {@code read()}, {@code demand(Runnable)} and {@code fail(Throwable)} methods must not be called by the listener.
|
||||
* @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk.Error}
|
||||
* @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) error}
|
||||
* and {@link org.eclipse.jetty.http.Trailers} chunks.
|
||||
* If a reference to the chunk (or its {@link ByteBuffer}) is kept,
|
||||
* then {@link Content.Chunk#retain()} must be called.
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A Handler that can apply a
|
||||
* mechanism to buffer the entire response content until the output is closed.
|
||||
* This allows the commit to be delayed until the response is complete and thus
|
||||
* headers and response status can be changed while writing the body.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that the decision to buffer is influenced by the headers and status at the
|
||||
* first write, and thus subsequent changes to those headers will not influence the
|
||||
* decision to buffer or not.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note also that there are no memory limits to the size of the buffer, thus
|
||||
* this handler can represent an unbounded memory commitment if the content
|
||||
* generated can also be unbounded.
|
||||
* </p>
|
||||
*/
|
||||
public class FileBufferedResponseHandler extends BufferedResponseHandler
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandler.class);
|
||||
|
||||
private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath();
|
||||
|
||||
public Path getTempDir()
|
||||
{
|
||||
return _tempDir;
|
||||
}
|
||||
|
||||
public void setTempDir(Path tempDir)
|
||||
{
|
||||
_tempDir = Objects.requireNonNull(tempDir);
|
||||
}
|
||||
|
||||
// TODO
|
||||
/*
|
||||
@Override
|
||||
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
|
||||
{
|
||||
return new FileBufferedInterceptor(httpChannel, interceptor);
|
||||
}
|
||||
|
||||
class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor
|
||||
{
|
||||
private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2;
|
||||
|
||||
private final Interceptor _next;
|
||||
private final HttpChannel _channel;
|
||||
private Boolean _aggregating;
|
||||
private Path _filePath;
|
||||
private OutputStream _fileOutputStream;
|
||||
|
||||
public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
|
||||
{
|
||||
_next = interceptor;
|
||||
_channel = httpChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interceptor getNextInterceptor()
|
||||
{
|
||||
return _next;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetBuffer()
|
||||
{
|
||||
dispose();
|
||||
BufferedInterceptor.super.resetBuffer();
|
||||
}
|
||||
|
||||
protected void dispose()
|
||||
{
|
||||
IO.close(_fileOutputStream);
|
||||
_fileOutputStream = null;
|
||||
_aggregating = null;
|
||||
|
||||
if (_filePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.delete(_filePath);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not immediately delete file (delaying to jvm exit) {}", _filePath, t);
|
||||
_filePath.toFile().deleteOnExit();
|
||||
}
|
||||
_filePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer content, boolean last, Callback callback)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
|
||||
|
||||
// If we are not committed, must decide if we should aggregate or not.
|
||||
if (_aggregating == null)
|
||||
_aggregating = shouldBuffer(_channel, last);
|
||||
|
||||
// If we are not aggregating, then handle normally.
|
||||
if (!_aggregating)
|
||||
{
|
||||
getNextInterceptor().write(content, last, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} aggregating", this);
|
||||
|
||||
try
|
||||
{
|
||||
if (BufferUtil.hasContent(content))
|
||||
aggregate(content);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
dispose();
|
||||
callback.failed(t);
|
||||
return;
|
||||
}
|
||||
|
||||
if (last)
|
||||
commit(callback);
|
||||
else
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
private void aggregate(ByteBuffer content) throws IOException
|
||||
{
|
||||
if (_fileOutputStream == null)
|
||||
{
|
||||
// Create a new OutputStream to a file.
|
||||
_filePath = Files.createTempFile(_tempDir, "BufferedResponse", "");
|
||||
_fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE);
|
||||
}
|
||||
|
||||
BufferUtil.writeTo(content, _fileOutputStream);
|
||||
}
|
||||
|
||||
private void commit(Callback callback)
|
||||
{
|
||||
if (_fileOutputStream == null)
|
||||
{
|
||||
// We have no content to write, signal next interceptor that we are finished.
|
||||
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_fileOutputStream.close();
|
||||
_fileOutputStream = null;
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
dispose();
|
||||
callback.failed(t);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an iterating callback to do the writing
|
||||
IteratingCallback icb = new IteratingCallback()
|
||||
{
|
||||
private final long fileLength = _filePath.toFile().length();
|
||||
private long _pos = 0;
|
||||
private boolean _last = false;
|
||||
|
||||
@Override
|
||||
protected Action process() throws Exception
|
||||
{
|
||||
if (_last)
|
||||
return Action.SUCCEEDED;
|
||||
|
||||
long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos);
|
||||
_last = (_pos + len == fileLength);
|
||||
ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len);
|
||||
getNextInterceptor().write(buffer, _last, this);
|
||||
_pos += len;
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteSuccess()
|
||||
{
|
||||
dispose();
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteFailure(Throwable cause)
|
||||
{
|
||||
dispose();
|
||||
callback.failed(cause);
|
||||
}
|
||||
};
|
||||
icb.iterate();
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -39,6 +39,12 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
|
|||
|
||||
public GracefulHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public GracefulHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
_shutdown = new Shutdown(this)
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -36,6 +36,16 @@ public class IdleTimeoutHandler extends Handler.Wrapper
|
|||
private long _idleTimeoutMs = 1000;
|
||||
private boolean _applyToAsync = false;
|
||||
|
||||
public IdleTimeoutHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public IdleTimeoutHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
public boolean isApplyToAsync()
|
||||
{
|
||||
return _applyToAsync;
|
||||
|
|
|
@ -47,6 +47,16 @@ public class InetAccessHandler extends Handler.Wrapper
|
|||
|
||||
private final IncludeExcludeSet<PatternTuple, AccessTuple> _set = new IncludeExcludeSet<>(InetAccessSet.class);
|
||||
|
||||
public InetAccessHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public InetAccessHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the includes, excludes, included connector names and excluded
|
||||
* connector names.
|
||||
|
|
|
@ -18,7 +18,6 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
@ -42,13 +41,6 @@ public class MovedContextHandler extends ContextHandler
|
|||
setAllowNullPathInContext(true);
|
||||
}
|
||||
|
||||
public MovedContextHandler(Handler.Container parent, String contextPath, String redirectURI)
|
||||
{
|
||||
Handler.Container.setAsParent(parent, this);
|
||||
setContextPath(contextPath);
|
||||
setRedirectURI(redirectURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the redirect status code, by default 303
|
||||
*/
|
||||
|
|
|
@ -63,6 +63,16 @@ public class ResourceHandler extends Handler.Wrapper
|
|||
private MimeTypes _mimeTypes;
|
||||
private List<String> _welcomes = List.of("index.html");
|
||||
|
||||
public ResourceHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ResourceHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
protected ResourceService newResourceService()
|
||||
{
|
||||
return new HandlerResourceService();
|
||||
|
@ -122,7 +132,7 @@ public class ResourceHandler extends Handler.Wrapper
|
|||
|
||||
protected ResourceService.WelcomeFactory setupWelcomeFactory()
|
||||
{
|
||||
return request ->
|
||||
return (content, request) ->
|
||||
{
|
||||
if (_welcomes == null)
|
||||
return null;
|
||||
|
@ -131,7 +141,7 @@ public class ResourceHandler extends Handler.Wrapper
|
|||
{
|
||||
String pathInContext = Request.getPathInContext(request);
|
||||
String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
|
||||
Resource welcomePath = _baseResource.resolve(pathInContext).resolve(welcome);
|
||||
Resource welcomePath = content.getResource().resolve(welcome);
|
||||
if (Resources.isReadableFile(welcomePath))
|
||||
return welcomeInContext;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ public class SecuredRedirectHandler extends Handler.Wrapper
|
|||
*/
|
||||
public SecuredRedirectHandler()
|
||||
{
|
||||
this(HttpStatus.MOVED_TEMPORARILY_302);
|
||||
this(null, HttpStatus.MOVED_TEMPORARILY_302);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,8 +56,29 @@ public class SecuredRedirectHandler extends Handler.Wrapper
|
|||
* @param code the redirect code to use in the response
|
||||
* @throws IllegalArgumentException if parameter is an invalid redirect code
|
||||
*/
|
||||
public SecuredRedirectHandler(final int code)
|
||||
public SecuredRedirectHandler(int code)
|
||||
{
|
||||
this(null, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses moved temporarily code (302) as the redirect code.
|
||||
*/
|
||||
public SecuredRedirectHandler(Handler handler)
|
||||
{
|
||||
this(handler, HttpStatus.MOVED_TEMPORARILY_302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use supplied code as the redirect code.
|
||||
*
|
||||
* @param handler the handler to wrap
|
||||
* @param code the redirect code to use in the response
|
||||
* @throws IllegalArgumentException if parameter is an invalid redirect code
|
||||
*/
|
||||
public SecuredRedirectHandler(Handler handler, int code)
|
||||
{
|
||||
super(handler);
|
||||
if (!HttpStatus.isRedirection(code))
|
||||
throw new IllegalArgumentException("Not a 3xx redirect code");
|
||||
_redirectCode = code;
|
||||
|
|
|
@ -91,7 +91,7 @@ public class ShutdownHandler extends Handler.Wrapper
|
|||
*/
|
||||
public ShutdownHandler(String shutdownToken)
|
||||
{
|
||||
this(null, shutdownToken, false);
|
||||
this(null, null, shutdownToken, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,18 +102,20 @@ public class ShutdownHandler extends Handler.Wrapper
|
|||
*/
|
||||
public ShutdownHandler(String shutdownToken, boolean exitJVM)
|
||||
{
|
||||
this(null, shutdownToken, exitJVM);
|
||||
this(null, null, shutdownToken, exitJVM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Handler that lets the server be shut down remotely (but only from localhost).
|
||||
*
|
||||
* @param handler the handler to wrap
|
||||
* @param shutdownPath the path to respond to shutdown requests against (default is "{@code /shutdown}")
|
||||
* @param shutdownToken a secret password to avoid unauthorized shutdown attempts
|
||||
* @param exitJVM If true, when the shutdown is executed, the handler class System.exit()
|
||||
*/
|
||||
public ShutdownHandler(String shutdownPath, String shutdownToken, boolean exitJVM)
|
||||
public ShutdownHandler(Handler handler, String shutdownPath, String shutdownToken, boolean exitJVM)
|
||||
{
|
||||
super(handler);
|
||||
this._shutdownPath = StringUtil.isBlank(shutdownPath) ? "/shutdown" : shutdownPath;
|
||||
this._shutdownToken = shutdownToken;
|
||||
this._exitJvm = exitJVM;
|
||||
|
|
|
@ -241,17 +241,17 @@ public class StatisticsHandler extends EventsHandler
|
|||
*/
|
||||
public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate)
|
||||
{
|
||||
_minimumReadRate = minimumReadRate;
|
||||
_minimumWriteRate = minimumWriteRate;
|
||||
this(null, minimumReadRate, minimumWriteRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code MinimumDataRateHandler} with the specified read and write rates.
|
||||
*
|
||||
* @param handler the handler to wrap.
|
||||
* @param minimumReadRate the minimum number of bytes to be read per second, or 0 for not checking the read rate.
|
||||
* @param minimumWriteRate the minimum number of bytes to be written per second, or 0 for not checking the write rate.
|
||||
* @param handler the handler to wrap.
|
||||
*/
|
||||
public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate, Handler handler)
|
||||
public MinimumDataRateHandler(Handler handler, long minimumReadRate, long minimumWriteRate)
|
||||
{
|
||||
super(handler);
|
||||
_minimumReadRate = minimumReadRate;
|
||||
|
@ -268,7 +268,7 @@ public class StatisticsHandler extends EventsHandler
|
|||
|
||||
protected class MinimumDataRateRequest extends Request.Wrapper
|
||||
{
|
||||
private Content.Chunk.Error _errorContent;
|
||||
private Content.Chunk _errorContent;
|
||||
|
||||
private MinimumDataRateRequest(Request request)
|
||||
{
|
||||
|
|
|
@ -80,17 +80,22 @@ public class ThreadLimitHandler extends Handler.Wrapper
|
|||
|
||||
public ThreadLimitHandler()
|
||||
{
|
||||
this(null, true);
|
||||
this(null, null, true);
|
||||
}
|
||||
|
||||
public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader)
|
||||
{
|
||||
this(forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader));
|
||||
this(null, forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader));
|
||||
}
|
||||
|
||||
public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239)
|
||||
{
|
||||
super();
|
||||
this(null, forwardedHeader, rfc7239);
|
||||
}
|
||||
|
||||
public ThreadLimitHandler(@Name("handler") Handler handler, @Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239)
|
||||
{
|
||||
super(handler);
|
||||
_rfc7239 = rfc7239;
|
||||
_forwardedHeader = forwardedHeader;
|
||||
_enabled = true;
|
||||
|
|
|
@ -71,6 +71,16 @@ public class TryPathsHandler extends Handler.Wrapper
|
|||
private String originalQueryAttribute;
|
||||
private List<String> paths;
|
||||
|
||||
public TryPathsHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TryPathsHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the attribute name of the original request path
|
||||
*/
|
||||
|
|
|
@ -63,6 +63,16 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
|
|||
*/
|
||||
public GzipHandler()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new GzipHandler.
|
||||
* @param handler the handler to wrap
|
||||
*/
|
||||
public GzipHandler(Handler handler)
|
||||
{
|
||||
super(handler);
|
||||
_methods.include(HttpMethod.GET.asString());
|
||||
_methods.include(HttpMethod.POST.asString());
|
||||
for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
|
||||
|
@ -526,9 +536,9 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
|
|||
if (Request.as(request, GzipRequest.class) != null)
|
||||
return next.handle(request, response, callback);
|
||||
|
||||
String path = Request.getPathInContext(request);
|
||||
boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(path);
|
||||
boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(path) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), path);
|
||||
String pathInContext = Request.getPathInContext(request);
|
||||
boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(pathInContext);
|
||||
boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(pathInContext) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), pathInContext);
|
||||
|
||||
// Can we skip looking at the request and wrapping request or response?
|
||||
if (!tryInflate && !tryDeflate)
|
||||
|
@ -624,15 +634,15 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
|
|||
/**
|
||||
* Test if the provided Request URI is allowed to be inflated based on the Path Specs filters.
|
||||
*
|
||||
* @param requestURI the request uri
|
||||
* @param pathInContext the request path in context
|
||||
* @return whether decompressing is allowed for the given the path.
|
||||
*/
|
||||
protected boolean isPathInflatable(String requestURI)
|
||||
protected boolean isPathInflatable(String pathInContext)
|
||||
{
|
||||
if (requestURI == null)
|
||||
if (pathInContext == null)
|
||||
return true;
|
||||
|
||||
return _inflatePaths.test(requestURI);
|
||||
return _inflatePaths.test(pathInContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -159,7 +159,7 @@ public class GzipRequest extends Request.Wrapper
|
|||
_chunk = inputChunk;
|
||||
if (_chunk == null)
|
||||
return null;
|
||||
if (_chunk instanceof Content.Chunk.Error)
|
||||
if (Content.Chunk.isFailure(_chunk))
|
||||
return _chunk;
|
||||
if (_chunk.isLast() && !_chunk.hasRemaining())
|
||||
return Content.Chunk.EOF;
|
||||
|
|
|
@ -104,7 +104,6 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
private final HandlerInvoker _handlerInvoker = new HandlerInvoker();
|
||||
private final ConnectionMetaData _connectionMetaData;
|
||||
private final SerializedInvoker _serializedInvoker;
|
||||
private final Attributes _requestAttributes = new Attributes.Lazy();
|
||||
private final ResponseHttpFields _responseHeaders = new ResponseHttpFields();
|
||||
private Thread _handling;
|
||||
private boolean _handled;
|
||||
|
@ -120,7 +119,7 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
/**
|
||||
* Failure passed to {@link #onFailure(Throwable)}
|
||||
*/
|
||||
private Content.Chunk.Error _failure;
|
||||
private Content.Chunk _failure;
|
||||
/**
|
||||
* Listener for {@link #onFailure(Throwable)} events
|
||||
*/
|
||||
|
@ -157,7 +156,6 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
_streamSendState = StreamSendState.SENDING;
|
||||
|
||||
// Recycle.
|
||||
_requestAttributes.clearAttributes();
|
||||
_responseHeaders.reset();
|
||||
_handling = null;
|
||||
_handled = false;
|
||||
|
@ -402,9 +400,9 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
{
|
||||
_failure = Content.Chunk.from(x);
|
||||
}
|
||||
else if (ExceptionUtil.areNotAssociated(_failure.getCause(), x) && _failure.getCause().getClass() != x.getClass())
|
||||
else if (ExceptionUtil.areNotAssociated(_failure.getFailure(), x) && _failure.getFailure().getClass() != x.getClass())
|
||||
{
|
||||
_failure.getCause().addSuppressed(x);
|
||||
_failure.getFailure().addSuppressed(x);
|
||||
}
|
||||
|
||||
// If not handled, then we just fail the request callback
|
||||
|
@ -741,6 +739,7 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
private final MetaData.Request _metaData;
|
||||
private final AutoLock _lock;
|
||||
private final LongAdder _contentBytesRead = new LongAdder();
|
||||
private final Attributes _attributes = new Attributes.Lazy();
|
||||
private HttpChannelState _httpChannelState;
|
||||
private Request _loggedRequest;
|
||||
private HttpFields _trailers;
|
||||
|
@ -777,26 +776,25 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
@Override
|
||||
public Object getAttribute(String name)
|
||||
{
|
||||
HttpChannelState httpChannel = getHttpChannelState();
|
||||
if (name.startsWith("org.eclipse.jetty"))
|
||||
{
|
||||
if (Server.class.getName().equals(name))
|
||||
return httpChannel.getConnectionMetaData().getConnector().getServer();
|
||||
return getConnectionMetaData().getConnector().getServer();
|
||||
if (HttpChannelState.class.getName().equals(name))
|
||||
return httpChannel;
|
||||
return getHttpChannelState();
|
||||
// TODO: is the instanceof needed?
|
||||
// TODO: possibly remove this if statement or move to Servlet.
|
||||
if (HttpConnection.class.getName().equals(name) &&
|
||||
getConnectionMetaData().getConnection() instanceof HttpConnection)
|
||||
return getConnectionMetaData().getConnection();
|
||||
}
|
||||
return httpChannel._requestAttributes.getAttribute(name);
|
||||
return _attributes.getAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object removeAttribute(String name)
|
||||
{
|
||||
return getHttpChannelState()._requestAttributes.removeAttribute(name);
|
||||
return _attributes.removeAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -804,19 +802,19 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
{
|
||||
if (Server.class.getName().equals(name) || HttpChannelState.class.getName().equals(name) || HttpConnection.class.getName().equals(name))
|
||||
return null;
|
||||
return getHttpChannelState()._requestAttributes.setAttribute(name, attribute);
|
||||
return _attributes.setAttribute(name, attribute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNameSet()
|
||||
{
|
||||
return getHttpChannelState()._requestAttributes.getAttributeNameSet();
|
||||
return _attributes.getAttributeNameSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAttributes()
|
||||
{
|
||||
getHttpChannelState()._requestAttributes.clearAttributes();
|
||||
_attributes.clearAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -837,7 +835,7 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
return _connectionMetaData;
|
||||
}
|
||||
|
||||
HttpChannelState getHttpChannelState()
|
||||
private HttpChannelState getHttpChannelState()
|
||||
{
|
||||
try (AutoLock ignore = _lock.lock())
|
||||
{
|
||||
|
@ -1178,16 +1176,15 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
{
|
||||
long length = BufferUtil.length(content);
|
||||
|
||||
long totalWritten;
|
||||
HttpChannelState httpChannelState;
|
||||
HttpStream stream = null;
|
||||
Throwable failure = null;
|
||||
Throwable failure;
|
||||
MetaData.Response responseMetaData = null;
|
||||
try (AutoLock ignored = _request._lock.lock())
|
||||
{
|
||||
httpChannelState = _request.lockedGetHttpChannelState();
|
||||
long committedContentLength = httpChannelState._committedContentLength;
|
||||
totalWritten = _contentBytesWritten + length;
|
||||
long totalWritten = _contentBytesWritten + length;
|
||||
long contentLength = committedContentLength >= 0 ? committedContentLength : getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
|
||||
|
||||
if (_writeCallback != null)
|
||||
|
@ -1195,11 +1192,14 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
else
|
||||
{
|
||||
failure = getFailure(httpChannelState);
|
||||
if (failure == null && contentLength >= 0)
|
||||
if (failure == null && contentLength >= 0 && totalWritten != contentLength)
|
||||
{
|
||||
// If the content length were not compatible with what was written, then we need to abort.
|
||||
String lengthError = (totalWritten > contentLength) ? "written %d > %d content-length"
|
||||
: (last && totalWritten < contentLength) ? "written %d < %d content-length" : null;
|
||||
String lengthError = null;
|
||||
if (totalWritten > contentLength)
|
||||
lengthError = "written %d > %d content-length";
|
||||
else if (last && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
|
||||
lengthError = "written %d < %d content-length";
|
||||
if (lengthError != null)
|
||||
{
|
||||
String message = lengthError.formatted(totalWritten, contentLength);
|
||||
|
@ -1245,8 +1245,8 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
|
||||
protected Throwable getFailure(HttpChannelState httpChannelState)
|
||||
{
|
||||
Content.Chunk.Error failure = httpChannelState._failure;
|
||||
return failure == null ? null : failure.getCause();
|
||||
Content.Chunk failure = httpChannelState._failure;
|
||||
return failure == null ? null : failure.getFailure();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1441,7 +1441,7 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
long totalWritten = response._contentBytesWritten;
|
||||
long committedContentLength = httpChannelState._committedContentLength;
|
||||
|
||||
if (committedContentLength >= 0 && committedContentLength != totalWritten)
|
||||
if (committedContentLength >= 0 && committedContentLength != totalWritten && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
|
||||
failure = new IOException("content-length %d != %d written".formatted(committedContentLength, totalWritten));
|
||||
|
||||
// is the request fully consumed?
|
||||
|
@ -1696,7 +1696,7 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
protected void onError(Runnable task, Throwable failure)
|
||||
{
|
||||
ChannelRequest request;
|
||||
Content.Chunk.Error error;
|
||||
Content.Chunk error;
|
||||
boolean callbackCompleted;
|
||||
try (AutoLock ignore = _lock.lock())
|
||||
{
|
||||
|
@ -1728,9 +1728,9 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
{
|
||||
// We are already in error, so we will not handle this one,
|
||||
// but we will add as suppressed if we have not seen it already.
|
||||
Throwable cause = error.getCause();
|
||||
Throwable cause = error.getFailure();
|
||||
if (ExceptionUtil.areNotAssociated(cause, failure))
|
||||
error.getCause().addSuppressed(failure);
|
||||
error.getFailure().addSuppressed(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1098,8 +1098,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
|
|||
{
|
||||
BadMessageException bad = new BadMessageException("Early EOF");
|
||||
|
||||
if (stream._chunk instanceof Error error)
|
||||
error.getCause().addSuppressed(bad);
|
||||
if (Content.Chunk.isFailure(stream._chunk))
|
||||
stream._chunk.getFailure().addSuppressed(bad);
|
||||
else
|
||||
{
|
||||
if (stream._chunk != null)
|
||||
|
|
|
@ -657,9 +657,9 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
|
|||
request.demand(this);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
callback.failed(error.getCause());
|
||||
callback.failed(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
// copy buffer
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.io.content.AsyncContent;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.FutureCallback;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class FormFieldsTest
|
||||
{
|
||||
public static Stream<Arguments> tests()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(List.of("name=value"), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("name=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("name", "=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("n", "ame", "=", "value"), UTF_8, -1, -1, Map.of("name", "value")),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, 2, 4, Map.of("n", "v", "X", "Y")),
|
||||
Arguments.of(List.of("name=f¤¤&X=Y"), UTF_8, -1, -1, Map.of("name", "f¤¤", "X", "Y")),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, null),
|
||||
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, null)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tests")
|
||||
public void testFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
|
||||
throws Exception
|
||||
{
|
||||
AsyncContent source = new AsyncContent();
|
||||
Attributes attributes = new Attributes.Mapped();
|
||||
CompletableFuture<Fields> futureFields = FormFields.from(source, attributes, charset, maxFields, maxLength);
|
||||
assertFalse(futureFields.isDone());
|
||||
|
||||
int last = chunks.size() - 1;
|
||||
FutureCallback eof = new FutureCallback();
|
||||
for (int i = 0; i <= last; i++)
|
||||
source.write(i == last, BufferUtil.toBuffer(chunks.get(i), charset), i == last ? eof : Callback.NOOP);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
eof.get(10, TimeUnit.SECONDS);
|
||||
assertTrue(futureFields.isDone());
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Fields.Field f : futureFields.get())
|
||||
result.put(f.getName(), f.getValue());
|
||||
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
catch (AssertionError e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
assertNull(expected);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -710,9 +710,9 @@ public class GracefulHandlerTest
|
|||
}
|
||||
}
|
||||
LOG.debug("chunk = {}", chunk);
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
Response.writeError(request, response, callback, error.getCause());
|
||||
Response.writeError(request, response, callback, chunk.getFailure());
|
||||
return true;
|
||||
}
|
||||
bytesRead += chunk.remaining();
|
||||
|
|
|
@ -66,7 +66,6 @@ import static org.hamcrest.Matchers.sameInstance;
|
|||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -1214,8 +1213,8 @@ public class HttpChannelTest
|
|||
Request rq = handling.get().getRequest();
|
||||
Content.Chunk chunk = rq.read();
|
||||
assertTrue(chunk.isLast());
|
||||
assertInstanceOf(Content.Chunk.Error.class, chunk);
|
||||
assertThat(((Content.Chunk.Error)chunk).getCause(), sameInstance(failure));
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
assertThat(chunk.getFailure(), sameInstance(failure));
|
||||
|
||||
CountDownLatch demand = new CountDownLatch(1);
|
||||
// Callback serialized until after onError task
|
||||
|
|
|
@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
|
@ -542,10 +543,10 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
|
|||
continue;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
earlyEOFException.countDown();
|
||||
throw IO.rethrow(error.getCause());
|
||||
throw IO.rethrow(chunk.getFailure());
|
||||
}
|
||||
|
||||
if (chunk.hasRemaining())
|
||||
|
@ -1334,6 +1335,75 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadHandled() throws Exception
|
||||
{
|
||||
startServer(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 10);
|
||||
if (HttpMethod.HEAD.is(request.getMethod()))
|
||||
{
|
||||
if (request.getHttpURI().getCanonicalPath().equals("/writeNull"))
|
||||
response.write(true, null, callback);
|
||||
else
|
||||
callback.succeeded();
|
||||
}
|
||||
else
|
||||
{
|
||||
Content.Sink.write(response, true, "123456789\n", callback);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
_httpConfiguration.setSendDateHeader(false);
|
||||
_httpConfiguration.setSendServerVersion(false);
|
||||
_httpConfiguration.setSendXPoweredBy(false);
|
||||
|
||||
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
|
||||
{
|
||||
OutputStream os = client.getOutputStream();
|
||||
InputStream is = client.getInputStream();
|
||||
|
||||
os.write("""
|
||||
GET / HTTP/1.1
|
||||
Host: localhost
|
||||
|
||||
HEAD / HTTP/1.1
|
||||
Host: localhost
|
||||
|
||||
HEAD /writeNull HTTP/1.1
|
||||
Host: localhost
|
||||
|
||||
GET / HTTP/1.1
|
||||
Host: localhost
|
||||
Connection: close
|
||||
|
||||
""".getBytes(StandardCharsets.ISO_8859_1));
|
||||
|
||||
String in = IO.toString(is);
|
||||
assertThat(in.replace("\r", ""), is("""
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 10
|
||||
|
||||
123456789
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 10
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 10
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 10
|
||||
Connection: close
|
||||
|
||||
123456789
|
||||
"""));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockedClient() throws Exception
|
||||
{
|
||||
|
|
|
@ -119,9 +119,8 @@ public class MultiPartByteRangesTest
|
|||
assertNotNull(contentType);
|
||||
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
|
||||
byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
|
||||
MultiPartByteRanges.Parts parts = byteRanges.join();
|
||||
MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary);
|
||||
MultiPartByteRanges.Parts parts = byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))).join();
|
||||
|
||||
assertEquals(3, parts.size());
|
||||
MultiPart.Part part1 = parts.get(0);
|
||||
|
|
|
@ -17,7 +17,9 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
@ -30,6 +32,9 @@ import org.eclipse.jetty.util.component.LifeCycle;
|
|||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
@ -372,6 +377,39 @@ public class RequestTest
|
|||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
}
|
||||
|
||||
public static Stream<Arguments> localeTests()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of(null, List.of(Locale.getDefault().toLanguageTag()).toString()),
|
||||
Arguments.of("zz", "[zz]"),
|
||||
Arguments.of("en", "[en]"),
|
||||
Arguments.of("en-gb", List.of(Locale.UK.toLanguageTag()).toString()),
|
||||
Arguments.of("en-us", List.of(Locale.US.toLanguageTag()).toString()),
|
||||
Arguments.of("EN-US", List.of(Locale.US.toLanguageTag()).toString()),
|
||||
Arguments.of("en-us,en-gb", List.of(Locale.US.toLanguageTag(), Locale.UK.toLanguageTag()).toString()),
|
||||
Arguments.of("en-us;q=0.5,fr;q=0.0,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag()).toString()),
|
||||
Arguments.of("en-us;q=0.5,zz-yy;q=0.7,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag(), "zz-YY").toString())
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("localeTests")
|
||||
public void testAcceptableLocales(String acceptLanguage, String expectedLocales) throws Exception
|
||||
{
|
||||
acceptLanguage = acceptLanguage == null ? "" : (HttpHeader.ACCEPT_LANGUAGE.asString() + ": " + acceptLanguage + "\n");
|
||||
String rawRequest = """
|
||||
GET / HTTP/1.1
|
||||
Host: tester
|
||||
Connection: close
|
||||
%s
|
||||
""".formatted(acceptLanguage);
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest));
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(response.getContent(), containsString("locales=" + expectedLocales));
|
||||
}
|
||||
|
||||
private static void checkCookieResult(String containedCookie, String[] notContainedCookies, String response)
|
||||
{
|
||||
assertNotNull(containedCookie);
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
|
@ -39,11 +40,11 @@ import org.eclipse.jetty.util.NanoTime;
|
|||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
|
@ -57,7 +58,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
@Disabled // TODO
|
||||
public class StopTest
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StopTest.class);
|
||||
|
@ -93,7 +93,7 @@ public class StopTest
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
stopper.start();
|
||||
|
@ -108,10 +108,7 @@ public class StopTest
|
|||
).getBytes());
|
||||
client.getOutputStream().flush();
|
||||
|
||||
while (!connector.isShutdown())
|
||||
{
|
||||
Thread.sleep(10);
|
||||
}
|
||||
await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown);
|
||||
|
||||
handler.latchB.countDown();
|
||||
|
||||
|
@ -281,89 +278,83 @@ public class StopTest
|
|||
LocalConnector connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
StatisticsHandler stats = new StatisticsHandler();
|
||||
ContextHandler context = new ContextHandler("/");
|
||||
StatisticsHandler stats = new StatisticsHandler(context);
|
||||
server.setHandler(stats);
|
||||
|
||||
ContextHandler context = new ContextHandler(stats, "/");
|
||||
|
||||
Exchanger<Void> exchanger0 = new Exchanger<>();
|
||||
Exchanger<Void> exchanger1 = new Exchanger<>();
|
||||
/* TODO
|
||||
context.setHandler(new AbstractHandler()
|
||||
context.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
response.setStatus(200);
|
||||
Content.Sink.write(response, true, "The Response", callback);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
throw new ServletException(x);
|
||||
callback.failed(x);
|
||||
}
|
||||
|
||||
baseRequest.setHandled(true);
|
||||
response.setStatus(200);
|
||||
response.getWriter().println("The Response");
|
||||
response.getWriter().close();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
server.setStopTimeout(1000);
|
||||
server.start();
|
||||
|
||||
LocalEndPoint endp = connector.executeRequest(
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"\r\n"
|
||||
);
|
||||
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
String response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(containsString("Connection: close")));
|
||||
|
||||
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
|
||||
exchanger0.exchange(null);
|
||||
|
||||
FutureCallback stopped = new FutureCallback();
|
||||
new Thread(() ->
|
||||
try (LocalEndPoint endp = connector.executeRequest(
|
||||
"""
|
||||
GET / HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
\r
|
||||
"""
|
||||
))
|
||||
{
|
||||
try
|
||||
{
|
||||
server.stop();
|
||||
stopped.succeeded();
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
stopped.failed(e);
|
||||
}
|
||||
}).start();
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
long start = NanoTime.now();
|
||||
while (!connector.isShutdown())
|
||||
{
|
||||
assertThat(NanoTime.secondsSince(start), lessThan(10L));
|
||||
Thread.sleep(10);
|
||||
String response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(containsString("Connection: close")));
|
||||
|
||||
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
|
||||
exchanger0.exchange(null);
|
||||
|
||||
FutureCallback stopped = new FutureCallback();
|
||||
new Thread(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
server.stop();
|
||||
stopped.succeeded();
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
stopped.failed(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown);
|
||||
|
||||
// Check new connections rejected!
|
||||
assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
|
||||
// Check completed 200 has close
|
||||
exchanger1.exchange(null);
|
||||
response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.containsString("Connection: close"));
|
||||
stopped.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// Check new connections rejected!
|
||||
assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
|
||||
// Check completed 200 has close
|
||||
exchanger1.exchange(null);
|
||||
response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.containsString("Connection: close"));
|
||||
stopped.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -374,85 +365,81 @@ public class StopTest
|
|||
LocalConnector connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ContextHandler context = new ContextHandler(server, "/");
|
||||
|
||||
ContextHandler context = new ContextHandler("/");
|
||||
server.setHandler(context);
|
||||
StatisticsHandler stats = new StatisticsHandler();
|
||||
context.setHandler(stats);
|
||||
|
||||
Exchanger<Void> exchanger0 = new Exchanger<>();
|
||||
Exchanger<Void> exchanger1 = new Exchanger<>();
|
||||
/* TODO
|
||||
stats.setHandler(new AbstractHandler()
|
||||
stats.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
response.setStatus(200);
|
||||
Content.Sink.write(response, true, "The Response", callback);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
throw new ServletException(x);
|
||||
callback.failed(x);
|
||||
}
|
||||
|
||||
baseRequest.setHandled(true);
|
||||
response.setStatus(200);
|
||||
response.getWriter().println("The Response");
|
||||
response.getWriter().close();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
server.start();
|
||||
|
||||
LocalEndPoint endp = connector.executeRequest(
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"\r\n"
|
||||
);
|
||||
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
String response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(containsString("Connection: close")));
|
||||
|
||||
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
exchanger0.exchange(null);
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread(() ->
|
||||
try (LocalEndPoint endp = connector.executeRequest(
|
||||
"""
|
||||
GET / HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
\r
|
||||
"""
|
||||
))
|
||||
{
|
||||
try
|
||||
exchanger0.exchange(null);
|
||||
exchanger1.exchange(null);
|
||||
|
||||
String response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(containsString("Connection: close")));
|
||||
|
||||
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
|
||||
exchanger0.exchange(null);
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread(() ->
|
||||
{
|
||||
context.stop();
|
||||
latch.countDown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
while (context.isStarted())
|
||||
{
|
||||
Thread.sleep(10);
|
||||
try
|
||||
{
|
||||
context.stop();
|
||||
latch.countDown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
|
||||
await().atMost(10, TimeUnit.SECONDS).until(context::isStopped);
|
||||
|
||||
// Check new connections accepted, but don't find context!
|
||||
String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
|
||||
assertThat(unavailable, containsString(" 404 Not Found"));
|
||||
|
||||
// Check completed 200 does not have close
|
||||
exchanger1.exchange(null);
|
||||
response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(Matchers.containsString("Connection: close")));
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
// Check new connections accepted, but don't find context!
|
||||
String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
|
||||
assertThat(unavailable, containsString(" 404 Not Found"));
|
||||
|
||||
// Check completed 200 does not have close
|
||||
exchanger1.exchange(null);
|
||||
response = endp.getResponse();
|
||||
assertThat(response, containsString("200 OK"));
|
||||
assertThat(response, Matchers.not(Matchers.containsString("Connection: close")));
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -486,12 +473,12 @@ public class StopTest
|
|||
ContextHandler context2 = new ContextHandler("/two")
|
||||
{
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
protected void doStart()
|
||||
{
|
||||
context2Started.set(true);
|
||||
}
|
||||
};
|
||||
contexts.setHandlers(new Handler[]{context0, context1, context2});
|
||||
contexts.setHandlers(context0, context1, context2);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -535,30 +522,19 @@ public class StopTest
|
|||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 2);
|
||||
response.write(true, ByteBuffer.wrap("a".getBytes()), new Callback()
|
||||
request.getContext().run(() ->
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
latchA.countDown();
|
||||
latchB.await();
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
response.write(true, ByteBuffer.wrap("b".getBytes()), callback);
|
||||
latchA.countDown();
|
||||
latchB.await();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
callback.failed(x);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
response.write(true, ByteBuffer.wrap("ab".getBytes()), callback);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
@ -105,9 +106,9 @@ public class DumpHandler extends Handler.Abstract
|
|||
}
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
callback.failed(error.getCause());
|
||||
callback.failed(chunk.getFailure());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -151,6 +152,7 @@ public class DumpHandler extends Handler.Abstract
|
|||
writer.write("<pre>httpURI.path=" + httpURI.getPath() + "</pre><br/>\n");
|
||||
writer.write("<pre>httpURI.query=" + httpURI.getQuery() + "</pre><br/>\n");
|
||||
writer.write("<pre>httpURI.pathQuery=" + httpURI.getPathQuery() + "</pre><br/>\n");
|
||||
writer.write("<pre>locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "</pre><br/>\n");
|
||||
writer.write("<pre>pathInContext=" + Request.getPathInContext(request) + "</pre><br/>\n");
|
||||
writer.write("<pre>contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "</pre><br/>\n");
|
||||
writer.write("<pre>servername=" + Request.getServerName(request) + "</pre><br/>\n");
|
||||
|
|
|
@ -1,645 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Disabled // TODO
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class FileBufferedResponseHandlerTest
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class);
|
||||
|
||||
public WorkDir _workDir;
|
||||
|
||||
private final CountDownLatch _disposeLatch = new CountDownLatch(1);
|
||||
private Server _server;
|
||||
private LocalConnector _localConnector;
|
||||
private ServerConnector _serverConnector;
|
||||
private Path _testDir;
|
||||
private FileBufferedResponseHandler _bufferedHandler;
|
||||
|
||||
/* TODO
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
{
|
||||
_testDir = _workDir.getEmptyPathDir();
|
||||
|
||||
_server = new Server();
|
||||
HttpConfiguration config = new HttpConfiguration();
|
||||
config.setOutputBufferSize(1024);
|
||||
config.setOutputAggregationSize(256);
|
||||
|
||||
_localConnector = new LocalConnector(_server, new HttpConnectionFactory(config));
|
||||
_localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis());
|
||||
_server.addConnector(_localConnector);
|
||||
_serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config));
|
||||
_server.addConnector(_serverConnector);
|
||||
|
||||
_bufferedHandler = new FileBufferedResponseHandler()
|
||||
{
|
||||
@Override
|
||||
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, HttpOutput.Interceptor interceptor)
|
||||
{
|
||||
return new FileBufferedInterceptor(httpChannel, interceptor)
|
||||
{
|
||||
@Override
|
||||
protected void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
_disposeLatch.countDown();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
_bufferedHandler.setTempDir(_testDir);
|
||||
_bufferedHandler.getPathIncludeExclude().include("/include/*");
|
||||
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
|
||||
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
|
||||
_server.setHandler(_bufferedHandler);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() throws Exception
|
||||
{
|
||||
_server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathNotIncluded() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(10);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The response was committed after the first write and we never created a file to buffer the response into.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("Committed: true"));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncludedByPath() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(10);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The response was not committed after the first write and a file was created to buffer the response.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("Committed: false"));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludedByPath() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(10);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The response was committed after the first write and we never created a file to buffer the response into.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("Committed: true"));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludedByMime() throws Exception
|
||||
{
|
||||
String excludedMimeType = "text/excluded";
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setContentType(excludedMimeType);
|
||||
response.setBufferSize(10);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The response was committed after the first write and we never created a file to buffer the response into.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("Committed: true"));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlushed() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(1024);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string smaller than the buffer size");
|
||||
writer.println("NumFilesBeforeFlush: " + getNumFiles());
|
||||
writer.flush();
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The response was not committed after the buffer was flushed and a file was created to buffer the response.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("NumFilesBeforeFlush: 0"));
|
||||
assertThat(responseContent, containsString("Committed: false"));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClosed() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(10);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
writer.close();
|
||||
writer.println("writtenAfterClose");
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The content written after close was not sent.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, not(containsString("writtenAfterClose")));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBufferSizeBig() throws Exception
|
||||
{
|
||||
int bufferSize = 4096;
|
||||
String largeContent = generateContent(bufferSize - 64);
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(bufferSize);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println(largeContent);
|
||||
writer.println("Committed: " + response.isCommitted());
|
||||
writer.println("NumFiles: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The content written was not buffered as a file as it was less than the buffer size.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, not(containsString("writtenAfterClose")));
|
||||
assertThat(responseContent, containsString("Committed: false"));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlushEmpty() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(1024);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.flush();
|
||||
int numFiles = getNumFiles();
|
||||
writer.println("NumFiles: " + numFiles);
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// The flush should not create the file unless there is content to write.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReset() throws Exception
|
||||
{
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.setBufferSize(8);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("THIS WILL BE RESET");
|
||||
writer.flush();
|
||||
writer.println("THIS WILL BE RESET");
|
||||
int numFilesBeforeReset = getNumFiles();
|
||||
response.resetBuffer();
|
||||
int numFilesAfterReset = getNumFiles();
|
||||
|
||||
writer.println("NumFilesBeforeReset: " + numFilesBeforeReset);
|
||||
writer.println("NumFilesAfterReset: " + numFilesAfterReset);
|
||||
writer.println("a string larger than the buffer size");
|
||||
writer.println("NumFilesAfterWrite: " + getNumFiles());
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
String responseContent = response.getContent();
|
||||
|
||||
// Resetting the response buffer will delete the file.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
|
||||
|
||||
assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
|
||||
assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
|
||||
assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileLargerThanMaxInteger() throws Exception
|
||||
{
|
||||
long fileSize = Integer.MAX_VALUE + 1234L;
|
||||
byte[] bytes = randomBytes(1024 * 1024);
|
||||
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
long written = 0;
|
||||
while (written < fileSize)
|
||||
{
|
||||
int length = Math.toIntExact(Math.min(bytes.length, fileSize - written));
|
||||
outputStream.write(bytes, 0, length);
|
||||
written += length;
|
||||
}
|
||||
outputStream.flush();
|
||||
|
||||
response.getHeaders().put("NumFiles", Integer.toString(getNumFiles()));
|
||||
response.getHeaders().put("FileSize", Long.toString(getFileSize()));
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
|
||||
AtomicLong received = new AtomicLong();
|
||||
HttpTester.Response response = new HttpTester.Response()
|
||||
{
|
||||
@Override
|
||||
public boolean content(ByteBuffer ref)
|
||||
{
|
||||
// Verify the content is what was sent.
|
||||
while (ref.hasRemaining())
|
||||
{
|
||||
byte byteFromBuffer = ref.get();
|
||||
long totalReceived = received.getAndIncrement();
|
||||
int bytesIndex = (int)(totalReceived % bytes.length);
|
||||
byte byteFromArray = bytes[bytesIndex];
|
||||
|
||||
if (byteFromBuffer != byteFromArray)
|
||||
{
|
||||
LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
HttpTester.Input input = HttpTester.from(socket.getInputStream());
|
||||
HttpTester.parseResponse(input, response);
|
||||
}
|
||||
|
||||
assertTrue(response.isComplete());
|
||||
assertThat(response.get("NumFiles"), is("1"));
|
||||
assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
|
||||
assertThat(received.get(), is(fileSize));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNextInterceptorFailed() throws Exception
|
||||
{
|
||||
AbstractHandler failingInterceptorHandler = new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
|
||||
HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor();
|
||||
httpOutput.setInterceptor(new HttpOutput.Interceptor()
|
||||
{
|
||||
@Override
|
||||
public void write(ByteBuffer content, boolean last, Callback callback)
|
||||
{
|
||||
callback.failed(new Throwable("intentionally throwing from interceptor"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpOutput.Interceptor getNextInterceptor()
|
||||
{
|
||||
return nextInterceptor;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler()));
|
||||
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
byte[] chunk1 = "this content will ".getBytes();
|
||||
byte[] chunk2 = "be buffered in a file".getBytes();
|
||||
response.setContentLength(chunk1.length + chunk2.length);
|
||||
ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
// Write chunk1 and then flush so it is written to the file.
|
||||
outputStream.write(chunk1);
|
||||
outputStream.flush();
|
||||
assertThat(getNumFiles(), is(1));
|
||||
|
||||
try
|
||||
{
|
||||
// ContentLength is set so it knows this is the last write.
|
||||
// This will cause the file to be written to the next interceptor which will fail.
|
||||
outputStream.write(chunk2);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
errorFuture.complete(t);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
|
||||
// Response was aborted.
|
||||
assertThat(response.getStatus(), is(0));
|
||||
|
||||
// We failed because of the next interceptor.
|
||||
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
|
||||
assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
|
||||
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
// All files were deleted.
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileWriteFailed() throws Exception
|
||||
{
|
||||
// Set the temp directory to an empty directory so that the file cannot be created.
|
||||
File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName());
|
||||
FS.ensureDeleted(tempDir);
|
||||
_bufferedHandler.setTempDir(tempDir.toPath());
|
||||
|
||||
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
|
||||
_bufferedHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public void handle(request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
ServletOutputStream outputStream = response.getOutputStream();
|
||||
byte[] content = "this content will be buffered in a file".getBytes();
|
||||
|
||||
try
|
||||
{
|
||||
// Write the content and flush it to the file.
|
||||
// This should throw as it cannot create the file to aggregate into.
|
||||
outputStream.write(content);
|
||||
outputStream.flush();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
errorFuture.complete(t);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_server.start();
|
||||
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
|
||||
|
||||
// Response was aborted.
|
||||
assertThat(response.getStatus(), is(0));
|
||||
|
||||
// We failed because cannot create the file.
|
||||
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
|
||||
assertThat(error, instanceOf(NoSuchFileException.class));
|
||||
|
||||
// No files were created.
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
private int getNumFiles()
|
||||
{
|
||||
File[] files = _testDir.toFile().listFiles();
|
||||
if (files == null)
|
||||
return 0;
|
||||
|
||||
return files.length;
|
||||
}
|
||||
|
||||
private long getFileSize()
|
||||
{
|
||||
File[] files = _testDir.toFile().listFiles();
|
||||
assertNotNull(files);
|
||||
assertThat(files.length, is(1));
|
||||
return files[0].length();
|
||||
}
|
||||
|
||||
private static String generateContent(int size)
|
||||
{
|
||||
Random random = new Random();
|
||||
StringBuilder stringBuilder = new StringBuilder(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
stringBuilder.append((char)Math.abs(random.nextInt(0x7F)));
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private byte[] randomBytes(int size)
|
||||
{
|
||||
byte[] data = new byte[size];
|
||||
new Random().nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,6 @@ import java.net.InetSocketAddress;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
|
@ -44,7 +42,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
@ -79,7 +76,8 @@ public class MultiPartFormDataHandlerTest
|
|||
public boolean handle(Request request, Response response, Callback callback)
|
||||
{
|
||||
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
new MultiPartFormData(boundary).parse(request)
|
||||
new MultiPartFormData.Parser(boundary)
|
||||
.parse(request)
|
||||
.whenComplete((parts, failure) ->
|
||||
{
|
||||
if (parts != null)
|
||||
|
@ -118,72 +116,6 @@ public class MultiPartFormDataHandlerTest
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelayedUntilFormData() throws Exception
|
||||
{
|
||||
DelayedHandler delayedHandler = new DelayedHandler();
|
||||
CountDownLatch processLatch = new CountDownLatch(1);
|
||||
delayedHandler.setHandler(new Handler.Abstract.NonBlocking()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
processLatch.countDown();
|
||||
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
|
||||
assertNotNull(parts);
|
||||
MultiPart.Part part = parts.get(0);
|
||||
Content.copy(part.getContentSource(), response, callback);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
start(delayedHandler);
|
||||
|
||||
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
|
||||
{
|
||||
String contentBegin = """
|
||||
--A1B2C3
|
||||
Content-Disposition: form-data; name="part"
|
||||
|
||||
""";
|
||||
String contentMiddle = """
|
||||
0123456789\
|
||||
""";
|
||||
String contentEnd = """
|
||||
ABCDEF
|
||||
--A1B2C3--
|
||||
""";
|
||||
String header = """
|
||||
POST / HTTP/1.1
|
||||
Host: localhost
|
||||
Content-Type: multipart/form-data; boundary=A1B2C3
|
||||
Content-Length: $L
|
||||
|
||||
""".replace("$L", String.valueOf(contentBegin.length() + contentMiddle.length() + contentEnd.length()));
|
||||
|
||||
client.write(UTF_8.encode(header));
|
||||
client.write(UTF_8.encode(contentBegin));
|
||||
|
||||
// Verify that the handler has not been called yet.
|
||||
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
|
||||
|
||||
client.write(UTF_8.encode(contentMiddle));
|
||||
|
||||
// Verify that the handler has not been called yet.
|
||||
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
|
||||
|
||||
// Finish to send the content.
|
||||
client.write(UTF_8.encode(contentEnd));
|
||||
|
||||
// Verify that the handler has been called.
|
||||
assertTrue(processLatch.await(5, TimeUnit.SECONDS));
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client));
|
||||
assertNotNull(response);
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
assertEquals("0123456789ABCDEF", response.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEchoMultiPart() throws Exception
|
||||
{
|
||||
|
@ -193,13 +125,15 @@ public class MultiPartFormDataHandlerTest
|
|||
public boolean handle(Request request, Response response, Callback callback)
|
||||
{
|
||||
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
new MultiPartFormData(boundary).parse(request)
|
||||
|
||||
new MultiPartFormData.Parser(boundary)
|
||||
.parse(request)
|
||||
.whenComplete((parts, failure) ->
|
||||
{
|
||||
if (parts != null)
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getMultiPartFormData().getBoundary()));
|
||||
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getMultiPartFormData().getBoundary());
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(boundary));
|
||||
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(boundary);
|
||||
source.setPartHeadersMaxLength(1024);
|
||||
parts.forEach(source::addPart);
|
||||
source.close();
|
||||
|
@ -310,22 +244,22 @@ public class MultiPartFormDataHandlerTest
|
|||
String boundary = MultiPart.extractBoundary(value);
|
||||
assertNotNull(boundary);
|
||||
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
ByteBufferContentSource byteBufferContentSource = new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()));
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setFilesDirectory(tempDir);
|
||||
formData.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
|
||||
MultiPartFormData.Parts parts = formData.join();
|
||||
|
||||
assertEquals(2, parts.size());
|
||||
MultiPart.Part part1 = parts.get(0);
|
||||
assertEquals("part1", part1.getName());
|
||||
assertEquals("hello", part1.getContentAsString(UTF_8));
|
||||
MultiPart.Part part2 = parts.get(1);
|
||||
assertEquals("part2", part2.getName());
|
||||
assertEquals("file2.bin", part2.getFileName());
|
||||
HttpFields headers2 = part2.getHeaders();
|
||||
assertEquals(2, headers2.size());
|
||||
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(32, part2.getContentSource().getLength());
|
||||
try (MultiPartFormData.Parts parts = formData.parse(byteBufferContentSource).join())
|
||||
{
|
||||
assertEquals(2, parts.size());
|
||||
MultiPart.Part part1 = parts.get(0);
|
||||
assertEquals("part1", part1.getName());
|
||||
assertEquals("hello", part1.getContentAsString(UTF_8));
|
||||
MultiPart.Part part2 = parts.get(1);
|
||||
assertEquals("part2", part2.getName());
|
||||
assertEquals("file2.bin", part2.getFileName());
|
||||
HttpFields headers2 = part2.getHeaders();
|
||||
assertEquals(2, headers2.size());
|
||||
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ public class ResourceHandlerByteRangesTest
|
|||
String contentType = response.get(HttpHeader.CONTENT_TYPE);
|
||||
assertThat(contentType, startsWith(responseContentType));
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
MultiPartByteRanges.Parts parts = new MultiPartByteRanges(boundary)
|
||||
MultiPartByteRanges.Parts parts = new MultiPartByteRanges.Parser(boundary)
|
||||
.parse(new ByteBufferContentSource(response.getContentByteBuffer()))
|
||||
.join();
|
||||
assertEquals(2, parts.size());
|
||||
|
|
|
@ -100,9 +100,9 @@ public class StatisticsHandlerTest
|
|||
return true;
|
||||
}
|
||||
|
||||
if (chunk instanceof Content.Chunk.Error errorContent)
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
callback.failed(errorContent.getCause());
|
||||
callback.failed(chunk.getFailure());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,7 @@ public class StatisticsHandlerTest
|
|||
AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
int expectedContentLength = 1000;
|
||||
StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(0, 1000, new Handler.Abstract.NonBlocking()
|
||||
StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(new Handler.Abstract.NonBlocking()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback)
|
||||
|
@ -217,7 +217,7 @@ public class StatisticsHandlerTest
|
|||
response.write(true, ByteBuffer.allocate(1), finalCallback);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 0, 1000);
|
||||
|
||||
_latchHandler.setHandler(mdrh);
|
||||
_server.start();
|
||||
|
|
|
@ -284,8 +284,8 @@ public class ThreadLimitHandlerTest
|
|||
request.demand(this);
|
||||
return;
|
||||
}
|
||||
if (chunk instanceof Error error)
|
||||
throw error.getCause();
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
throw chunk.getFailure();
|
||||
|
||||
if (chunk.hasRemaining())
|
||||
read.addAndGet(chunk.remaining());
|
||||
|
|
|
@ -1984,11 +1984,8 @@ public class GzipHandlerTest
|
|||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
|
||||
|
||||
Fields queryParameters = Request.extractQueryParameters(request);
|
||||
FormFields futureFormFields = new FormFields(request, StandardCharsets.UTF_8, -1, -1);
|
||||
futureFormFields.run();
|
||||
Fields formParameters = futureFormFields.get();
|
||||
Fields formParameters = FormFields.from(request, UTF_8, -1, -1).get();
|
||||
Fields parameters = Fields.combine(queryParameters, formParameters);
|
||||
|
||||
String dump = parameters.stream().map(f -> "%s: %s\n".formatted(f.getName(), f.getValue())).collect(Collectors.joining());
|
||||
|
|
|
@ -51,9 +51,10 @@ class Timestamp
|
|||
|
||||
public Timestamp(TimeZone timeZone)
|
||||
{
|
||||
tzFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
zoneId = timeZone.toZoneId();
|
||||
tzFormatter.withZone(zoneId);
|
||||
tzFormatter = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
.withZone(zoneId);
|
||||
tick = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,6 @@ import org.junit.jupiter.params.provider.MethodSource;
|
|||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -256,8 +255,8 @@ public class HttpClientTest extends AbstractTest
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (chunk instanceof Content.Chunk.Error error)
|
||||
throw IO.rethrow(error.getCause());
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
throw IO.rethrow(chunk.getFailure());
|
||||
|
||||
total += chunk.remaining();
|
||||
if (total >= sleep)
|
||||
|
@ -941,7 +940,7 @@ public class HttpClientTest extends AbstractTest
|
|||
assertThat(chunks2.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(totalBytes));
|
||||
assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0));
|
||||
assertThat(chunks3.size(), is(1));
|
||||
assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class));
|
||||
assertTrue(Content.Chunk.isFailure(chunks3.get(0), true));
|
||||
|
||||
chunks1.forEach(Content.Chunk::release);
|
||||
chunks2.forEach(Content.Chunk::release);
|
||||
|
@ -983,7 +982,7 @@ public class HttpClientTest extends AbstractTest
|
|||
assertThat(chunks3Latch.await(5, TimeUnit.SECONDS), is(true));
|
||||
assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0));
|
||||
assertThat(chunks3.size(), is(1));
|
||||
assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class));
|
||||
assertTrue(Content.Chunk.isFailure(chunks3.get(0), true));
|
||||
|
||||
chunks1.forEach(Content.Chunk::release);
|
||||
chunks2.forEach(Content.Chunk::release);
|
||||
|
|
|
@ -130,8 +130,8 @@ public class ServerTimeoutsTest extends AbstractTest
|
|||
|
||||
// Reads should yield the idle timeout.
|
||||
Content.Chunk chunk = requestRef.get().read();
|
||||
assertThat(chunk, instanceOf(Content.Chunk.Error.class));
|
||||
Throwable cause = ((Content.Chunk.Error)chunk).getCause();
|
||||
assertTrue(Content.Chunk.isFailure(chunk, true));
|
||||
Throwable cause = chunk.getFailure();
|
||||
assertThat(cause, instanceOf(TimeoutException.class));
|
||||
|
||||
// Complete the callback as the error listener promised.
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -23,7 +22,6 @@ import java.util.List;
|
|||
* Utility methods for Array manipulation
|
||||
*/
|
||||
public class ArrayUtil
|
||||
implements Cloneable, Serializable
|
||||
{
|
||||
|
||||
public static <T> T[] removeFromArray(T[] array, Object item)
|
||||
|
@ -34,7 +32,7 @@ public class ArrayUtil
|
|||
{
|
||||
if (item.equals(array[i]))
|
||||
{
|
||||
Class<?> c = array == null ? item.getClass() : array.getClass().getComponentType();
|
||||
Class<?> c = array.getClass().getComponentType();
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] na = (T[])Array.newInstance(c, Array.getLength(array) - 1);
|
||||
if (i > 0)
|
||||
|
@ -155,5 +153,10 @@ public class ArrayUtil
|
|||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private ArrayUtil()
|
||||
{
|
||||
// prevents instantiation
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -208,7 +208,6 @@ public class Blocker
|
|||
|
||||
/**
|
||||
* A shared reusable Blocking source.
|
||||
* TODO Review need for this, as it is currently unused.
|
||||
*/
|
||||
public static class Shared
|
||||
{
|
||||
|
|
|
@ -132,16 +132,19 @@ public class DateCache
|
|||
else
|
||||
_tzFormatString = _formatString;
|
||||
|
||||
_zoneId = tz.toZoneId();
|
||||
if (_locale != null)
|
||||
{
|
||||
_tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale);
|
||||
_tzFormat = DateTimeFormatter
|
||||
.ofPattern(_tzFormatString, _locale)
|
||||
.withZone(_zoneId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tzFormat = DateTimeFormatter.ofPattern(_tzFormatString);
|
||||
_tzFormat = DateTimeFormatter
|
||||
.ofPattern(_tzFormatString)
|
||||
.withZone(_zoneId);
|
||||
}
|
||||
_zoneId = tz.toZoneId();
|
||||
_tzFormat.withZone(_zoneId);
|
||||
_tick = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -536,6 +536,11 @@ public class IO
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IO()
|
||||
{
|
||||
// prevent instantiation
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -400,6 +400,7 @@ public abstract class IteratingCallback implements Callback
|
|||
break;
|
||||
case PENDING:
|
||||
{
|
||||
_state = State.FAILED;
|
||||
failure = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1254,6 +1254,11 @@ public class StringUtil
|
|||
.collect(StringBuilder::new, (b, c) -> b.append((char)c), StringBuilder::append)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private StringUtil()
|
||||
{
|
||||
// prevent instantiation
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.lang.reflect.InvocationTargetException;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.CodeSource;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
|
@ -406,7 +407,7 @@ public class TypeUtil
|
|||
digit = 10 + c - 'a';
|
||||
}
|
||||
if (digit < 0 || digit >= base)
|
||||
throw new NumberFormatException(new String(b, offset, length));
|
||||
throw new NumberFormatException(new String(b, offset, length, StandardCharsets.US_ASCII));
|
||||
value = value * base + digit;
|
||||
}
|
||||
return value;
|
||||
|
@ -671,14 +672,11 @@ public class TypeUtil
|
|||
try
|
||||
{
|
||||
String resourceName = TypeUtil.toClassReference(clazz);
|
||||
if (loader != null)
|
||||
URL url = loader.getResource(resourceName);
|
||||
if (url != null)
|
||||
{
|
||||
URL url = loader.getResource(resourceName);
|
||||
if (url != null)
|
||||
{
|
||||
URI uri = url.toURI();
|
||||
return URIUtil.unwrapContainer(uri);
|
||||
}
|
||||
URI uri = url.toURI();
|
||||
return URIUtil.unwrapContainer(uri);
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ignored)
|
||||
|
@ -819,4 +817,9 @@ public class TypeUtil
|
|||
{
|
||||
return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false);
|
||||
}
|
||||
|
||||
private TypeUtil()
|
||||
{
|
||||
// prevents instantiation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,6 @@ public class UrlEncoded
|
|||
charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
|
||||
if (charset == null)
|
||||
{
|
||||
charset = StandardCharsets.UTF_8.toString();
|
||||
encoding = StandardCharsets.UTF_8;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -19,11 +19,13 @@ import java.net.MalformedURLException;
|
|||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jetty.util.FileID;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
|
||||
/**
|
||||
* {@link ResourceFactory} for {@link java.net.URL} based resources.
|
||||
|
@ -109,7 +111,7 @@ public class URLResourceFactory implements ResourceFactory
|
|||
@Override
|
||||
public boolean isDirectory()
|
||||
{
|
||||
return uri.getPath().endsWith("/");
|
||||
return uri.getSchemeSpecificPart().endsWith("/");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -121,7 +123,7 @@ public class URLResourceFactory implements ResourceFactory
|
|||
@Override
|
||||
public URI getURI()
|
||||
{
|
||||
return uri;
|
||||
return URIUtil.correctFileURI(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -139,7 +141,7 @@ public class URLResourceFactory implements ResourceFactory
|
|||
@Override
|
||||
public Resource resolve(String subUriPath)
|
||||
{
|
||||
URI newURI = uri.resolve(subUriPath);
|
||||
URI newURI = resolve(uri, subUriPath);
|
||||
try
|
||||
{
|
||||
return new URLResource(newURI, this.connectTimeout, this.useCaches);
|
||||
|
@ -150,6 +152,25 @@ public class URLResourceFactory implements ResourceFactory
|
|||
}
|
||||
}
|
||||
|
||||
// This could probably live in URIUtil, but it's awefully specific to URLResourceFactory.
|
||||
private static URI resolve(URI parent, String path)
|
||||
{
|
||||
if (parent.isOpaque() && parent.getPath() == null)
|
||||
{
|
||||
URI resolved = resolve(URI.create(parent.getRawSchemeSpecificPart()), path);
|
||||
return URI.create(parent.getScheme() + ":" + resolved.toASCIIString());
|
||||
}
|
||||
else if (parent.getPath() != null)
|
||||
{
|
||||
return parent.resolve(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not possible to use URLs that without a path in Jetty.
|
||||
throw new RuntimeException("URL without path not supported by Jetty: " + parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists()
|
||||
{
|
||||
|
@ -200,10 +221,9 @@ public class URLResourceFactory implements ResourceFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public ReadableByteChannel newReadableByteChannel()
|
||||
public ReadableByteChannel newReadableByteChannel() throws IOException
|
||||
{
|
||||
// not really possible with the URL interface
|
||||
return null;
|
||||
return Channels.newChannel(newInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue