---
.../eclipse/jetty/ee9/nested/ContextHandler.java | 7 -------
.../eclipse/jetty/ee9/nested/ErrorHandler.java | 4 +---
.../eclipse/jetty/ee9/nested/HttpChannel.java | 12 ++----------
.../jetty/ee9/nested/HttpChannelState.java | 3 +--
.../org/eclipse/jetty/ee9/nested/Request.java | 16 ++++++++++++++++
.../jetty/ee9/servlet/ServletHandler.java | 5 -----
6 files changed, 20 insertions(+), 27 deletions(-)
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java
index b45ea51b149e..c270128d6cb8 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java
@@ -97,9 +97,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_CONTEXT_ATTRIBUTE;
-import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_PATH_IN_CONTEXT_ATTRIBUTE;
-
/**
* ContextHandler.
*
@@ -905,10 +902,6 @@ else if (contextPath.length() == 1)
(DispatcherType.INCLUDE.equals(dispatch) || !target.startsWith("/")) ? oldPathInContext : pathInContext);
org.eclipse.jetty.server.handler.ContextHandler.ScopedContext context = getCoreContextHandler().getContext();
-
- baseRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, _apiContext);
- baseRequest.setAttribute(SERVLET_PATH_IN_CONTEXT_ATTRIBUTE, baseRequest.getPathInContext());
-
if (context == org.eclipse.jetty.server.handler.ContextHandler.getCurrentContext())
{
nextScope(target, baseRequest, request, response);
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ErrorHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ErrorHandler.java
index 141fc78fce5d..eb86c025fd85 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ErrorHandler.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ErrorHandler.java
@@ -45,8 +45,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_CONTEXT_ATTRIBUTE;
-
/**
* Handler for Error pages
*/
@@ -91,7 +89,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
// This logic really should be in ErrorPageErrorHandler, but some implementations extend ErrorHandler
// and implement ErrorPageMapper directly, so we do this here in the base class.
String errorPage = (this instanceof ErrorPageMapper) ? ((ErrorPageMapper)this).getErrorPage(request) : null;
- ServletContext context = (ServletContext)request.getAttribute(SERVLET_CONTEXT_ATTRIBUTE);
+ ServletContext context = baseRequest.getLastContext();
Dispatcher errorDispatcher = (errorPage != null && context != null)
? (Dispatcher)context.getRequestDispatcher(errorPage) : null;
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
index 38ed7c9144f8..843745510c48 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
@@ -32,7 +32,6 @@
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
-import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpException;
@@ -73,9 +72,6 @@
public class HttpChannel implements Runnable, HttpOutput.Interceptor
{
private static final Logger LOG = LoggerFactory.getLogger(HttpChannel.class);
- public static final String SERVLET_CONTEXT_ATTRIBUTE = "jetty.servletContext.attribute";
- public static final String SERVLET_NAME_ATTRIBUTE = "jetty.servletName.attribute";
- public static final String SERVLET_PATH_IN_CONTEXT_ATTRIBUTE = "jetty.servletPathInContext.attribute";
private final ContextHandler _contextHandler;
private final ConnectionMetaData _connectionMetaData;
@@ -1014,15 +1010,11 @@ public void onCompleted()
if (idleTO >= 0 && getIdleTimeout() != _oldIdleTimeout)
setIdleTimeout(_oldIdleTimeout);
- // TODO: We're not in scope for the servlet context so this won't always work.
if (getServer().getRequestLog() instanceof CustomRequestLog)
{
- String servletName = (String)_request.getAttribute(SERVLET_NAME_ATTRIBUTE);
- String pathInContext = (String)_request.getAttribute(SERVLET_PATH_IN_CONTEXT_ATTRIBUTE);
- ServletContext servletContext = (ServletContext)_request.getAttribute(SERVLET_CONTEXT_ATTRIBUTE);
CustomRequestLog.LogDetail logDetail = new CustomRequestLog.LogDetail(
- servletName,
- servletContext.getRealPath(pathInContext)
+ _request.getServletName(),
+ _request.getLastContext().getRealPath(_request.getLastPathInContext())
);
_request.setAttribute(CustomRequestLog.LOG_DETAIL, logDetail);
}
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannelState.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannelState.java
index 75c57d36dcab..ce17bdea6566 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannelState.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannelState.java
@@ -35,7 +35,6 @@
import static jakarta.servlet.RequestDispatcher.ERROR_REQUEST_URI;
import static jakarta.servlet.RequestDispatcher.ERROR_SERVLET_NAME;
import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE;
-import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_CONTEXT_ATTRIBUTE;
/**
* Implementation of AsyncContext interface that holds the state of request-response cycle.
@@ -923,7 +922,7 @@ public void sendError(int code, String message)
response.setStatus(code);
response.errorClose();
- ServletContext errorContext = (ServletContext)request.getAttribute(SERVLET_CONTEXT_ATTRIBUTE);
+ ServletContext errorContext = request.getLastContext();
request.setAttribute(ErrorHandler.ERROR_CONTEXT, errorContext);
request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(ERROR_SERVLET_NAME, request.getServletName());
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
index 294f9a655de6..8bf16a94abeb 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
@@ -188,6 +188,8 @@ public static Request getBaseRequest(ServletRequest request)
private long _timeStamp;
private MultiPartFormInputStream _multiParts; //if the request is a multi-part mime
private AsyncContextState _async;
+ private String _lastPathInContext;
+ private ContextHandler.APIContext _lastContext;
public Request(HttpChannel channel, HttpInput input)
{
@@ -700,8 +702,22 @@ public void setContext(ContextHandler.APIContext context, String pathInContext)
{
_context = context;
_pathInContext = pathInContext;
+ if (context != null)
+ {
+ _lastContext = context;
+ _lastPathInContext = pathInContext;
+ }
}
+ public ContextHandler.APIContext getLastContext()
+ {
+ return _lastContext;
+ }
+
+ public String getLastPathInContext()
+ {
+ return _lastPathInContext;
+ }
@Override
public String getContextPath()
{
diff --git a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletHandler.java b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletHandler.java
index 6ee23353b9fe..c192fce8108f 100644
--- a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletHandler.java
+++ b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletHandler.java
@@ -70,8 +70,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static org.eclipse.jetty.ee9.nested.HttpChannel.SERVLET_NAME_ATTRIBUTE;
-
/**
* Servlet HttpHandler.
*
@@ -470,10 +468,7 @@ public void doScope(String target, Request baseRequest, HttpServletRequest reque
ServletPathMapping servletPathMapping = mappedServlet.getServletPathMapping(target, matched.getMatchedPath());
if (servletPathMapping != null)
- {
baseRequest.setServletPathMapping(servletPathMapping);
- baseRequest.setAttribute(SERVLET_NAME_ATTRIBUTE, servletPathMapping.getServletName());
- }
}
if (LOG.isDebugEnabled())
From d5c81f9f93fafab99fc4938aa22f67c0eefbfca3 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Thu, 22 Jun 2023 21:07:09 +1000
Subject: [PATCH 25/63] fix checkstyle error
Signed-off-by: Lachlan Roberts
---
.../src/main/java/org/eclipse/jetty/ee9/nested/Request.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
index 8bf16a94abeb..b978e2178494 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java
@@ -718,6 +718,7 @@ public String getLastPathInContext()
{
return _lastPathInContext;
}
+
@Override
public String getContextPath()
{
From 0b1c28a8883bb70421a228d1a72644d6125f2baa Mon Sep 17 00:00:00 2001
From: Greg Wilkins
Date: Thu, 22 Jun 2023 17:04:49 +0200
Subject: [PATCH 26/63] Jetty 12 inserted handler in ee10 servlet context
(#9927)
This PR refactors the ee10 handing of servlet API request and response objects:
+ The ServletContextHandler matches the request to a servlet and creates a one time only ServletContextRequest and a ServletContextResponse
+ A reusable ServletChannel object with all the heavy weight HttpInput and HttpOutput object is associated with the ServletContextRequest and ServletContextResponse.
+ Once the handling reaches the ServletHandler, the possibly wrapped request, response and callback are associated with the ServletChannel before handling.
+ Were possible the ServletApiRequest and ServletApiResponse use the possibly wrapped request/response
Added tests to check that GzipHandler can now be nested inside of an EE10 context.
---------
Signed-off-by: Ludovic Orban
Signed-off-by: gregw
Co-authored-by: Ludovic Orban
---
.../org/eclipse/jetty/http/HttpFields.java | 169 +++++++++-
.../eclipse/jetty/http/HttpFieldsTest.java | 1 +
.../eclipse/jetty/server/ResourceService.java | 8 +-
.../server/handler/gzip/GzipHandler.java | 14 +-
.../server/internal/HttpChannelState.java | 20 +-
.../java/org/eclipse/jetty/util/Blocker.java | 1 -
.../core/server/ServerUpgradeResponse.java | 2 +
.../server/internal/HttpFieldsWrapper.java | 166 ----------
.../internal/ServerUpgradeResponseImpl.java | 11 +-
.../internal/WebSocketHttpFieldsWrapper.java | 114 ++++---
.../ee10/servlet/AsyncContentProducer.java | 19 +-
.../jetty/ee10/servlet/AsyncContextState.java | 2 +-
.../jetty/ee10/servlet/DebugListener.java | 26 +-
.../jetty/ee10/servlet/DefaultServlet.java | 15 +-
.../jetty/ee10/servlet/Dispatcher.java | 2 +-
.../jetty/ee10/servlet/ErrorHandler.java | 10 +-
.../eclipse/jetty/ee10/servlet/HttpInput.java | 4 +-
.../jetty/ee10/servlet/HttpOutput.java | 102 +++---
.../jetty/ee10/servlet/PushBuilderImpl.java | 2 +-
.../jetty/ee10/servlet/ServletApiRequest.java | 255 +++++++--------
.../ee10/servlet/ServletApiResponse.java | 304 ++++++++----------
.../jetty/ee10/servlet/ServletChannel.java | 180 +++++++----
.../ee10/servlet/ServletContextHandler.java | 121 ++++++-
.../ee10/servlet/ServletContextRequest.java | 99 +++---
.../ee10/servlet/ServletContextResponse.java | 283 ++++++++++++----
.../jetty/ee10/servlet/ServletHandler.java | 13 +-
.../servlet/ServletMultiPartFormData.java | 4 +-
.../ee10/servlet/ServletRequestState.java | 10 +-
.../jetty/ee10/servlet/SessionHandler.java | 2 +-
.../jetty/ee10/servlet/AsyncServletTest.java | 2 +-
.../jetty/ee10/servlet/GzipHandlerTest.java | 175 ++++++----
.../servlet/ServletContextHandlerTest.java | 7 +-
.../ee10/servlet/ServletHandlerTest.java | 1 -
.../ee10/test/GzipWithSendErrorTest.java | 3 +-
.../test/resources/add-jetty-test-webapp.xml | 18 +-
.../jetty/ee10/webapp/WebAppContext.java | 6 +-
.../JakartaWebSocketServerContainer.java | 2 +-
.../server/JettyWebSocketServerContainer.java | 2 +-
.../server/JettyWebSocketServlet.java | 2 +-
.../servlet/WebSocketUpgradeFilter.java | 2 +-
.../eclipse/jetty/ee9/nested/HttpOutput.java | 3 +-
.../ee9/servlets/CloseableDoSFilterTest.java | 2 -
.../src/main/resources/modules/demo-root.mod | 1 +
.../resources/modules/demo.d/root/favicon.ico | Bin 0 -> 1150 bytes
.../tests/distribution/DemoModulesTests.java | 62 ++++
45 files changed, 1310 insertions(+), 937 deletions(-)
delete mode 100644 jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/HttpFieldsWrapper.java
create mode 100644 jetty-home/src/main/resources/modules/demo.d/root/favicon.ico
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
index 393732e4c21b..26a2b4969c52 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
@@ -1204,6 +1204,167 @@ else if (ensured > 0)
// 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 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 listIterator()
+ {
+ ListIterator 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 HttpField getField(int index)
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;
}
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
index 8e1b6f1fbb77..6d848fca80c6 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
@@ -54,6 +54,7 @@ public static Stream mutables()
return Stream.of(
HttpFields.build(),
HttpFields.build(0),
+ new HttpFields.Mutable.Wrapper(HttpFields.build()),
new HttpFields.Mutable()
{
private final HttpFields.Mutable fields = HttpFields.build();
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java
index bdd623f78299..48d998ddf6ae 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java
@@ -589,11 +589,8 @@ private WelcomeAction processWelcome(HttpContent content, Request request) throw
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())
@@ -602,6 +599,9 @@ private WelcomeAction processWelcome(HttpContent content, Request request) throw
case SERVE -> welcomeTarget;
};
+ if (LOG.isDebugEnabled())
+ LOG.debug("welcome {} {}", welcomeMode, welcomeTarget);
+
return new WelcomeAction(welcomeTarget, welcomeMode);
}
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java
index e74dd4893612..f9e10e33dfe8 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java
@@ -526,9 +526,9 @@ public boolean handle(Request request, Response response, Callback callback) thr
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 +624,15 @@ protected boolean isPathDeflatable(String requestURI)
/**
* 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);
}
/**
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java
index 31173e8cf4bd..f4315df47da5 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java
@@ -104,7 +104,6 @@ private enum StreamSendState
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;
@@ -157,7 +156,6 @@ public void recycle()
_streamSendState = StreamSendState.SENDING;
// Recycle.
- _requestAttributes.clearAttributes();
_responseHeaders.reset();
_handling = null;
_handled = false;
@@ -741,6 +739,7 @@ public static class ChannelRequest implements Attributes, Request
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 long getContentBytesRead()
@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 Object setAttribute(String name, Object attribute)
{
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 getAttributeNameSet()
{
- return getHttpChannelState()._requestAttributes.getAttributeNameSet();
+ return _attributes.getAttributeNameSet();
}
@Override
public void clearAttributes()
{
- getHttpChannelState()._requestAttributes.clearAttributes();
+ _attributes.clearAttributes();
}
@Override
@@ -837,7 +835,7 @@ public ConnectionMetaData getConnectionMetaData()
return _connectionMetaData;
}
- HttpChannelState getHttpChannelState()
+ private HttpChannelState getHttpChannelState()
{
try (AutoLock ignore = _lock.lock())
{
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Blocker.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Blocker.java
index 529f612c3d37..2aa2c1814b78 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Blocker.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Blocker.java
@@ -208,7 +208,6 @@ public void close()
/**
* A shared reusable Blocking source.
- * TODO Review need for this, as it is currently unused.
*/
public static class Shared
{
diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/ServerUpgradeResponse.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/ServerUpgradeResponse.java
index 1341f0147c5c..5d4b2c58ff36 100644
--- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/ServerUpgradeResponse.java
+++ b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/ServerUpgradeResponse.java
@@ -28,5 +28,7 @@ public interface ServerUpgradeResponse extends Response
void addExtensions(List configs);
+ void removeExtensions(List configs);
+
void setExtensions(List configs);
}
diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/HttpFieldsWrapper.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/HttpFieldsWrapper.java
deleted file mode 100644
index 24ed32ad4074..000000000000
--- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/HttpFieldsWrapper.java
+++ /dev/null
@@ -1,166 +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.websocket.core.server.internal;
-
-import java.util.EnumSet;
-import java.util.Iterator;
-import java.util.ListIterator;
-
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpHeaderValue;
-
-public class HttpFieldsWrapper implements HttpFields.Mutable
-{
- private final HttpFields.Mutable _fields;
-
- public HttpFieldsWrapper(HttpFields.Mutable fields)
- {
- _fields = fields;
- }
-
- public boolean onPutField(String name, String value)
- {
- return true;
- }
-
- public boolean onAddField(String name, String value)
- {
- return true;
- }
-
- public boolean onRemoveField(String name)
- {
- return true;
- }
-
- @Override
- public Mutable add(String name, String value)
- {
- if (onAddField(name, value))
- return _fields.add(name, value);
- return this;
- }
-
- @Override
- public Mutable add(HttpHeader header, HttpHeaderValue value)
- {
- if (onAddField(header.asString(), value.asString()))
- return _fields.add(header, value);
- return this;
- }
-
- @Override
- public Mutable add(HttpHeader header, String value)
- {
- if (onAddField(header.asString(), value))
- return _fields.add(header, value);
- return this;
- }
-
- @Override
- public Mutable add(HttpField field)
- {
- if (onAddField(field.getName(), field.getValue()))
- return _fields.add(field);
- return this;
- }
-
- @Override
- public Mutable add(HttpFields fields)
- {
- for (HttpField field : fields)
- {
- add(field);
- }
- return this;
- }
-
- @Override
- public Mutable clear()
- {
- return _fields.clear();
- }
-
- @Override
- public Iterator iterator()
- {
- return _fields.iterator();
- }
-
- @Override
- public ListIterator listIterator()
- {
- return _fields.listIterator();
- }
-
- @Override
- public Mutable put(HttpField field)
- {
- if (onPutField(field.getName(), field.getValue()))
- return _fields.put(field);
- return this;
- }
-
- @Override
- public Mutable put(String name, String value)
- {
- if (onPutField(name, value))
- return _fields.put(name, value);
- return this;
- }
-
- @Override
- public Mutable put(HttpHeader header, HttpHeaderValue value)
- {
- if (onPutField(header.asString(), value.asString()))
- return _fields.put(header, value);
- return this;
- }
-
- @Override
- public Mutable put(HttpHeader header, String value)
- {
- if (onPutField(header.asString(), value))
- return _fields.put(header, value);
- return this;
- }
-
- @Override
- public Mutable remove(HttpHeader header)
- {
- if (onRemoveField(header.asString()))
- return _fields.remove(header);
- return this;
- }
-
- @Override
- public Mutable remove(EnumSet fields)
- {
- for (HttpHeader header : fields)
- {
- remove(header);
- }
- return this;
- }
-
- @Override
- public Mutable remove(String name)
- {
- if (onRemoveField(name))
- return _fields.remove(name);
- return this;
- }
-}
diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/ServerUpgradeResponseImpl.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/ServerUpgradeResponseImpl.java
index 3a024c959022..41c7106dc5a7 100644
--- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/ServerUpgradeResponseImpl.java
+++ b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/ServerUpgradeResponseImpl.java
@@ -69,12 +69,19 @@ public List getExtensions()
@Override
public void addExtensions(List configs)
{
- ArrayList combinedConfig = new ArrayList<>();
- combinedConfig.addAll(getExtensions());
+ ArrayList combinedConfig = new ArrayList<>(getExtensions());
combinedConfig.addAll(configs);
setExtensions(combinedConfig);
}
+ @Override
+ public void removeExtensions(List configs)
+ {
+ ArrayList trimmedExtensions = new ArrayList<>(getExtensions());
+ trimmedExtensions.removeAll(configs);
+ setExtensions(trimmedExtensions);
+ }
+
@Override
public void setExtensions(List configs)
{
diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/WebSocketHttpFieldsWrapper.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/WebSocketHttpFieldsWrapper.java
index 07dbae333129..22b5b377f6ee 100644
--- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/WebSocketHttpFieldsWrapper.java
+++ b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/WebSocketHttpFieldsWrapper.java
@@ -14,75 +14,117 @@
package org.eclipse.jetty.websocket.core.server.internal;
import java.util.Collections;
+import java.util.List;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.websocket.core.ExtensionConfig;
import org.eclipse.jetty.websocket.core.server.ServerUpgradeResponse;
-public class WebSocketHttpFieldsWrapper extends HttpFieldsWrapper
+public class WebSocketHttpFieldsWrapper extends HttpFields.Mutable.Wrapper
{
- private final WebSocketNegotiation _negotiation;
private final ServerUpgradeResponse _response;
public WebSocketHttpFieldsWrapper(Mutable fields, ServerUpgradeResponse response, WebSocketNegotiation negotiation)
{
super(fields);
- _negotiation = negotiation;
_response = response;
}
@Override
- public boolean onPutField(String name, String value)
+ public HttpField onAddField(HttpField field)
{
- if (HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.is(name))
+ if (field.getHeader() != null)
{
- _response.setAcceptedSubProtocol(value);
- return false;
- }
+ return switch (field.getHeader())
+ {
+ case SEC_WEBSOCKET_SUBPROTOCOL ->
+ {
+ _response.setAcceptedSubProtocol(field.getValue());
+ yield null;
+ }
- if (HttpHeader.SEC_WEBSOCKET_EXTENSIONS.is(name))
- {
- _response.setExtensions(ExtensionConfig.parseList(value));
- return false;
- }
+ case SEC_WEBSOCKET_EXTENSIONS ->
+ {
+ _response.addExtensions(ExtensionConfig.parseList(field.getValue()));
+ yield null;
+ }
- return super.onPutField(name, value);
+ default -> super.onAddField(field);
+ };
+ }
+ return super.onAddField(field);
}
@Override
- public boolean onAddField(String name, String value)
+ public boolean onRemoveField(HttpField field)
{
- if (HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.is(name))
+ if (field.getHeader() != null)
{
- _response.setAcceptedSubProtocol(value);
- return false;
+ return switch (field.getHeader())
+ {
+ case SEC_WEBSOCKET_SUBPROTOCOL ->
+ {
+ _response.setAcceptedSubProtocol(null);
+ yield true;
+ }
+
+ case SEC_WEBSOCKET_EXTENSIONS ->
+ {
+ _response.removeExtensions(ExtensionConfig.parseList(field.getValue()));
+ yield true;
+ }
+
+ default -> super.onRemoveField(field);
+ };
}
+ return super.onRemoveField(field);
+ }
+ @Override
+ public Mutable put(HttpField field)
+ {
+ // Need to override put methods as putting extensions clears them, even if field does not exist.
+ if (field.getHeader() == HttpHeader.SEC_WEBSOCKET_EXTENSIONS)
+ _response.setExtensions(Collections.emptyList());
+ return super.put(field);
+ }
+
+ @Override
+ public Mutable put(String name, String value)
+ {
+ // Need to override put methods as putting extensions clears them, even if field does not exist.
if (HttpHeader.SEC_WEBSOCKET_EXTENSIONS.is(name))
- {
- _response.addExtensions(ExtensionConfig.parseList(value));
- return false;
- }
+ _response.setExtensions(Collections.emptyList());
+ return super.put(name, value);
+ }
- return super.onAddField(name, value);
+ @Override
+ public Mutable put(HttpHeader header, HttpHeaderValue value)
+ {
+ // Need to override put methods as putting extensions clears them, even if field does not exist.
+ if (header == HttpHeader.SEC_WEBSOCKET_EXTENSIONS)
+ _response.setExtensions(Collections.emptyList());
+ return super.put(header, value);
}
@Override
- public boolean onRemoveField(String name)
+ public Mutable put(HttpHeader header, String value)
{
- if (HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.is(name))
- {
- _response.setAcceptedSubProtocol(null);
- return false;
- }
+ // Need to override put methods as putting extensions clears them, even if field does not exist.
+ if (header == HttpHeader.SEC_WEBSOCKET_EXTENSIONS)
+ _response.setExtensions(Collections.emptyList());
+ return super.put(header, value);
+ }
+ @Override
+ public Mutable put(String name, List list)
+ {
+ // Need to override put methods as putting extensions clears them, even if field does not exist.
if (HttpHeader.SEC_WEBSOCKET_EXTENSIONS.is(name))
- {
- // TODO: why add extensions??
- _response.addExtensions(Collections.emptyList());
- return false;
- }
-
- return super.onRemoveField(name);
+ _response.setExtensions(Collections.emptyList());
+ return super.put(name, list);
}
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContentProducer.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContentProducer.java
index f173cba11c10..b222c8ddd712 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContentProducer.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContentProducer.java
@@ -126,7 +126,7 @@ public void checkMinDataRate()
LOG.debug("checkMinDataRate check failed {}", this);
BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,
String.format("Request content data rate < %d B/s", minRequestDataRate));
- if (_servletChannel.getState().isResponseCommitted())
+ if (_servletChannel.getServletRequestState().isResponseCommitted())
{
if (LOG.isDebugEnabled())
LOG.debug("checkMinDataRate aborting channel {}", this);
@@ -180,7 +180,7 @@ private boolean consumeCurrentChunk()
private boolean consumeAvailableChunks()
{
- return _servletChannel.getServletContextRequest().consumeAvailable();
+ return _servletChannel.getRequest().consumeAvailable();
}
@Override
@@ -189,7 +189,7 @@ public boolean onContentProducible()
assertLocked();
if (LOG.isDebugEnabled())
LOG.debug("onContentProducible {}", this);
- return _servletChannel.getState().onReadReady();
+ return _servletChannel.getServletRequestState().onReadReady();
}
@Override
@@ -200,7 +200,7 @@ public Content.Chunk nextChunk()
if (LOG.isDebugEnabled())
LOG.debug("nextChunk = {} {}", chunk, this);
if (chunk != null)
- _servletChannel.getState().onReadIdle();
+ _servletChannel.getServletRequestState().onReadIdle();
return chunk;
}
@@ -227,8 +227,8 @@ public boolean isReady()
return true;
}
- _servletChannel.getState().onReadUnready();
- _servletChannel.getServletContextRequest().demand(() ->
+ _servletChannel.getServletRequestState().onReadUnready();
+ _servletChannel.getRequest().demand(() ->
{
if (_servletChannel.getHttpInput().onContentProducible())
_servletChannel.handle();
@@ -241,7 +241,7 @@ public boolean isReady()
boolean isUnready()
{
- return _servletChannel.getState().isInputUnready();
+ return _servletChannel.getServletRequestState().isInputUnready();
}
private Content.Chunk produceChunk()
@@ -280,7 +280,7 @@ private Content.Chunk produceChunk()
}
else
{
- _servletChannel.getState().onContentAdded();
+ _servletChannel.getServletRequestState().onContentAdded();
}
}
@@ -297,7 +297,7 @@ private Content.Chunk produceChunk()
private Content.Chunk readChunk()
{
- Content.Chunk chunk = _servletChannel.getServletContextRequest().read();
+ Content.Chunk chunk = _servletChannel.getRequest().read();
if (chunk != null)
{
_bytesArrived += chunk.remaining();
@@ -305,7 +305,6 @@ private Content.Chunk readChunk()
_firstByteNanoTime = NanoTime.now();
if (LOG.isDebugEnabled())
LOG.debug("readChunk() updated _bytesArrived to {} and _firstByteTimeStamp to {} {}", _bytesArrived, _firstByteNanoTime, this);
- // TODO: notify channel listeners (see ee9)?
if (chunk instanceof Trailers trailers)
_servletChannel.onTrailers(trailers.getTrailers());
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContextState.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContextState.java
index 0271a05675f7..29c65d004ecf 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContextState.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/AsyncContextState.java
@@ -117,7 +117,7 @@ public boolean hasOriginalRequestAndResponse()
ServletResponse servletResponse = getResponse();
ServletChannel servletChannel = _state.getServletChannel();
HttpServletRequest originalHttpServletRequest = servletChannel.getServletContextRequest().getServletApiRequest();
- HttpServletResponse originalHttpServletResponse = servletChannel.getResponse().getHttpServletResponse();
+ HttpServletResponse originalHttpServletResponse = servletChannel.getServletContextResponse().getServletApiResponse();
return (servletRequest == originalHttpServletRequest && servletResponse == originalHttpServletResponse);
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DebugListener.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DebugListener.java
index 20d93f2e031f..c999ad13e9d8 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DebugListener.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DebugListener.java
@@ -162,18 +162,16 @@ protected String findContextName(ServletContext context)
return n;
}
- protected String findRequestName(ServletRequest request)
+ protected String findRequestName(ServletContextRequest request)
{
if (request == null)
return null;
- HttpServletRequest r = (HttpServletRequest)request;
- String n = (String)request.getAttribute(_attr);
- if (n == null)
- {
- n = String.format("%s@%x", r.getRequestURI(), request.hashCode());
- request.setAttribute(_attr, n);
- }
- return n;
+ return request.getId();
+ }
+
+ protected String findRequestName(ServletRequest request)
+ {
+ return findRequestName(ServletContextRequest.getServletContextRequest(request));
}
protected void log(String format, Object... arg)
@@ -225,7 +223,7 @@ public void onComplete(AsyncEvent event) throws IOException
String rname = findRequestName(ace.getAsyncContext().getRequest());
ServletContextRequest request = ServletContextRequest.getServletContextRequest(ace.getAsyncContext().getRequest());
- Response response = request.getResponse();
+ Response response = request.getServletContextResponse();
String headers = _showHeaders ? ("\n" + response.getHeaders().toString()) : "";
log("! ctx=%s r=%s onComplete %s %d%s", cname, rname, ace.getServletRequestState(), response.getStatus(), headers);
@@ -280,8 +278,8 @@ public void requestDestroyed(ServletRequestEvent sre)
else
{
ServletContextRequest request = ServletContextRequest.getServletContextRequest(r);
- String headers = _showHeaders ? ("\n" + request.getResponse().getHeaders().toString()) : "";
- log("<< %s ctx=%s r=%s async=false %d%s", d, cname, rname, request.getResponse().getStatus(), headers);
+ String headers = _showHeaders ? ("\n" + request.getServletContextResponse().getHeaders().toString()) : "";
+ log("<< %s ctx=%s r=%s async=false %d%s", d, cname, rname, request.getServletContextResponse().getStatus(), headers);
}
}
};
@@ -296,7 +294,7 @@ public void enterScope(ServletContextHandler.ServletScopedContext context, Servl
log("> ctx=%s", cname);
else
{
- String rname = findRequestName(request.getServletApiRequest());
+ String rname = findRequestName(request);
if (_renameThread)
{
@@ -316,7 +314,7 @@ public void exitScope(ServletContextHandler.ServletScopedContext context, Servle
log("< ctx=%s", cname);
else
{
- String rname = findRequestName(request.getServletApiRequest());
+ String rname = findRequestName(request);
log("< ctx=%s r=%s", cname, rname);
if (_renameThread)
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java
index 1a1995f17e4d..73e880a889ff 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java
@@ -458,7 +458,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
else if (isPathInfoOnly())
encodedPathInContext = URIUtil.encodePath(req.getPathInfo());
else if (req instanceof ServletApiRequest apiRequest)
- encodedPathInContext = Context.getPathInContext(req.getContextPath(), apiRequest.getServletContextRequest().getHttpURI().getCanonicalPath());
+ encodedPathInContext = Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
else
encodedPathInContext = Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI()));
@@ -878,12 +878,7 @@ public HttpFields.Mutable getHeaders()
public ServletContextResponse getServletContextResponse()
{
- if (_response instanceof ServletApiResponse)
- {
- ServletApiResponse apiResponse = (ServletApiResponse)_response;
- return apiResponse.getResponse();
- }
- return null;
+ return ServletContextResponse.getServletContextResponse(_response);
}
@Override
@@ -1003,6 +998,12 @@ public CompletableFuture writeInterim(int status, HttpFields headers)
{
return null;
}
+
+ @Override
+ public String toString()
+ {
+ return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response);
+ }
}
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
index 4d43b9f9d3fe..d1bc1702c1f0 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
@@ -104,7 +104,7 @@ public void forward(ServletRequest request, ServletResponse response) throws Ser
HttpServletResponse httpResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response);
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(request);
- servletContextRequest.getResponse().resetForForward();
+ servletContextRequest.getServletContextResponse().resetForForward();
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse);
// If we are not async and not closed already, then close via the possibly wrapped response.
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java
index 107e2e1f3142..93dc82a1c59c 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ErrorHandler.java
@@ -91,7 +91,7 @@ public boolean handle(Request request, Response response, Callback callback) thr
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpServletRequest httpServletRequest = servletContextRequest.getServletApiRequest();
HttpServletResponse httpServletResponse = servletContextRequest.getHttpServletResponse();
- ServletContextHandler contextHandler = servletContextRequest.getContext().getServletContextHandler();
+ ServletContextHandler contextHandler = servletContextRequest.getServletContext().getServletContextHandler();
String cacheControl = getCacheControl();
if (cacheControl != null)
response.getHeaders().put(HttpHeader.CACHE_CONTROL.asString(), cacheControl);
@@ -164,7 +164,7 @@ protected void generateAcceptableResponse(ServletContextRequest baseRequest, Htt
for (String mimeType : acceptable)
{
generateAcceptableResponse(baseRequest, request, response, code, message, mimeType);
- if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming())
+ if (response.isCommitted() || baseRequest.getServletContextResponse().isWritingOrStreaming())
break;
}
}
@@ -300,7 +300,7 @@ protected void generateAcceptableResponse(ServletContextRequest baseRequest, Htt
// TODO error page may cause a BufferOverflow. In which case we try
// TODO again with stacks disabled. If it still overflows, it is
// TODO written without a body.
- ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().getByteBuffer();
+ ByteBuffer buffer = baseRequest.getServletContextResponse().getHttpOutput().getByteBuffer();
ByteBufferOutputStream out = new ByteBufferOutputStream(buffer);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset));
@@ -334,7 +334,7 @@ protected void generateAcceptableResponse(ServletContextRequest baseRequest, Htt
LOG.warn("Error page too large: {} {} {}", code, message, request, e);
else
LOG.warn("Error page too large: {} {} {}", code, message, request);
- baseRequest.getResponse().resetContent();
+ baseRequest.getServletContextResponse().resetContent();
if (!_disableStacks)
{
LOG.info("Disabling showsStacks for {}", this);
@@ -395,7 +395,7 @@ protected void writeErrorPageBody(HttpServletRequest request, Writer writer, int
if (showStacks && !_disableStacks)
writeErrorPageStacks(request, writer);
- ((ServletApiRequest)request).getServletContextRequest().getServletChannel().getHttpConfiguration()
+ ((ServletApiRequest)request).getServletRequestInfo().getServletChannel().getHttpConfiguration()
.writePoweredBy(writer, "
", "
\n");
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpInput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpInput.java
index 531291982af0..4d0951257008 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpInput.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpInput.java
@@ -52,7 +52,7 @@ public class HttpInput extends ServletInputStream implements Runnable
public HttpInput(ServletChannel channel)
{
_servletChannel = channel;
- _channelState = _servletChannel.getState();
+ _channelState = _servletChannel.getServletRequestState();
_asyncContentProducer = new AsyncContentProducer(_servletChannel, _lock);
_blockingContentProducer = new BlockingContentProducer(_asyncContentProducer);
_contentProducer = _blockingContentProducer;
@@ -349,7 +349,7 @@ public void run()
if (LOG.isDebugEnabled())
LOG.debug("running error={} {}", error, this);
// TODO is this necessary to add here?
- _servletChannel.getResponse().getHeaders().add(HttpFields.CONNECTION_CLOSE);
+ _servletChannel.getServletContextResponse().getHeaders().add(HttpFields.CONNECTION_CLOSE);
_readListener.onError(error);
}
else if (chunk.isLast() && !chunk.hasRemaining())
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java
index 3d1298c5d6c0..b5d3548fb9a0 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java
@@ -35,17 +35,14 @@
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.RetainableByteBuffer;
-import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.NanoTime;
-import org.eclipse.jetty.util.SharedBlockingCallback;
-import org.eclipse.jetty.util.SharedBlockingCallback.Blocker;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -127,12 +124,9 @@ enum ApiState
private static final Logger LOG = LoggerFactory.getLogger(HttpOutput.class);
private static final ThreadLocal _encoder = new ThreadLocal<>();
- private final ConnectionMetaData _connectionMetaData;
private final ServletChannel _servletChannel;
- private final Response _response;
- private final ByteBufferPool _bufferPool;
private final ServletRequestState _channelState;
- private final SharedBlockingCallback _writeBlocker;
+ private final Blocker.Shared _writeBlocker;
private ApiState _apiState = ApiState.BLOCKING;
private State _state = State.OPEN;
private boolean _softClose = false;
@@ -146,15 +140,11 @@ enum ApiState
private volatile Throwable _onError;
private Callback _closedCallback;
- public HttpOutput(Response response, ServletChannel channel)
+ public HttpOutput(ServletChannel channel)
{
- _response = response;
_servletChannel = channel;
- _connectionMetaData = _response.getRequest().getConnectionMetaData();
- _bufferPool = _response.getRequest().getComponents().getByteBufferPool();
-
- _channelState = _servletChannel.getState();
- _writeBlocker = new WriteBlocker(_servletChannel);
+ _channelState = _servletChannel.getServletRequestState();
+ _writeBlocker = new Blocker.Shared();
HttpConfiguration config = _servletChannel.getHttpConfiguration();
_bufferSize = config.getOutputBufferSize();
_commitSize = config.getOutputAggregationSize();
@@ -164,11 +154,6 @@ public HttpOutput(Response response, ServletChannel channel)
_commitSize = _bufferSize;
}
}
-
- public Response getResponse()
- {
- return _response;
- }
public boolean isWritten()
{
@@ -188,14 +173,9 @@ public void reopen()
}
}
- protected Blocker acquireWriteBlockingCallback() throws IOException
- {
- return _writeBlocker.acquire();
- }
-
private void channelWrite(ByteBuffer content, boolean complete) throws IOException
{
- try (Blocker blocker = _writeBlocker.acquire())
+ try (Blocker.Callback blocker = _writeBlocker.callback())
{
channelWrite(content, complete, blocker);
blocker.block();
@@ -206,13 +186,13 @@ private void channelWrite(ByteBuffer content, boolean last, Callback callback)
{
if (_firstByteNanoTime == -1)
{
- long minDataRate = _connectionMetaData.getHttpConfiguration().getMinResponseDataRate();
+ long minDataRate = _servletChannel.getConnectionMetaData().getHttpConfiguration().getMinResponseDataRate();
if (minDataRate > 0)
_firstByteNanoTime = NanoTime.now();
else
_firstByteNanoTime = Long.MAX_VALUE;
}
- _response.write(last, content, callback);
+ _servletChannel.getResponse().write(last, content, callback);
}
private void onWriteComplete(boolean last, Throwable failure)
@@ -353,7 +333,7 @@ public void complete(Callback callback)
case PENDING: // an async write is pending and may complete at any time
// If this is not the last write, then we must abort
- if (!_servletChannel.getResponse().isContentComplete(_written))
+ if (_servletChannel.getServletContextResponse().isContentIncomplete(_written))
error = new CancellationException("Completed whilst write pending");
break;
@@ -369,7 +349,6 @@ public void complete(Callback callback)
if (error != null)
{
_servletChannel.abort(error);
- _writeBlocker.fail(error);
_state = State.CLOSED;
}
else
@@ -463,7 +442,7 @@ public void completed(Throwable failure)
public void close() throws IOException
{
ByteBuffer content = null;
- Blocker blocker = null;
+ Blocker.Callback blocker = null;
try (AutoLock l = _channelState.lock())
{
if (_onError != null)
@@ -489,7 +468,7 @@ public void close() throws IOException
case BLOCKING:
case BLOCKED:
// block until CLOSED state reached.
- blocker = _writeBlocker.acquire();
+ blocker = _writeBlocker.callback();
_closedCallback = Callback.combine(_closedCallback, blocker);
break;
@@ -506,7 +485,7 @@ public void close() throws IOException
// Output is idle blocking state, but we still do an async close
_apiState = ApiState.BLOCKED;
_state = State.CLOSING;
- blocker = _writeBlocker.acquire();
+ blocker = _writeBlocker.callback();
content = _aggregate != null && _aggregate.hasRemaining() ? _aggregate.getByteBuffer() : BufferUtil.EMPTY_BUFFER;
break;
@@ -516,7 +495,7 @@ public void close() throws IOException
// then trigger a close from onWriteComplete
_state = State.CLOSE;
// and block until it is complete
- blocker = _writeBlocker.acquire();
+ blocker = _writeBlocker.callback();
_closedCallback = Callback.combine(_closedCallback, blocker);
break;
@@ -550,9 +529,9 @@ public void close() throws IOException
return;
// Just wait for some other close to finish.
- try (Blocker b = blocker)
+ try (Blocker.Callback cb = blocker)
{
- b.block();
+ cb.block();
}
}
else
@@ -565,7 +544,7 @@ public void close() throws IOException
else
{
// Do a blocking close
- try (Blocker b = blocker)
+ try (Blocker.Callback b = blocker)
{
channelWrite(content, true, blocker);
b.block();
@@ -590,9 +569,10 @@ public ByteBuffer getByteBuffer()
private RetainableByteBuffer acquireBuffer()
{
- boolean useOutputDirectByteBuffers = _connectionMetaData.getHttpConfiguration().isUseOutputDirectByteBuffers();
+ boolean useOutputDirectByteBuffers = _servletChannel.getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers();
+ ByteBufferPool pool = _servletChannel.getRequest().getComponents().getByteBufferPool();
if (_aggregate == null)
- _aggregate = _bufferPool.acquire(getBufferSize(), useOutputDirectByteBuffers);
+ _aggregate = pool.acquire(getBufferSize(), useOutputDirectByteBuffers);
return _aggregate;
}
@@ -724,7 +704,7 @@ public void write(byte[] b, int off, int len) throws IOException
checkWritable();
long written = _written + len;
int space = maximizeAggregateSpace();
- last = _servletChannel.getResponse().isAllContentWritten(written);
+ last = _servletChannel.getServletContextResponse().isAllContentWritten(written);
// Write will be aggregated if:
// + it is smaller than the commitSize
// + is not the last one, or is last but will fit in an already allocated aggregate buffer.
@@ -858,7 +838,7 @@ public void write(ByteBuffer buffer) throws IOException
{
checkWritable();
long written = _written + len;
- last = _servletChannel.getResponse().isAllContentWritten(written);
+ last = _servletChannel.getServletContextResponse().isAllContentWritten(written);
flush = last || len > 0 || (_aggregate != null && _aggregate.hasRemaining());
if (last && _state == State.OPEN)
@@ -938,7 +918,7 @@ public void write(int b) throws IOException
checkWritable();
long written = _written + 1;
int space = maximizeAggregateSpace();
- last = _servletChannel.getResponse().isAllContentWritten(written);
+ last = _servletChannel.getServletContextResponse().isAllContentWritten(written);
flush = last || space == 1;
if (last && _state == State.OPEN)
@@ -1012,7 +992,7 @@ private void print(String s, boolean eoln) throws IOException
s = String.valueOf(s);
- String charset = _servletChannel.getResponse().getCharacterEncoding(false);
+ String charset = _servletChannel.getServletContextResponse().getCharacterEncoding(false);
CharsetEncoder encoder = _encoder.get();
if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset))
{
@@ -1025,8 +1005,8 @@ private void print(String s, boolean eoln) throws IOException
{
encoder.reset();
}
-
- RetainableByteBuffer out = _bufferPool.acquire((int)(1 + (s.length() + 2) * encoder.averageBytesPerChar()), false);
+ ByteBufferPool pool = _servletChannel.getRequest().getComponents().getByteBufferPool();
+ RetainableByteBuffer out = pool.acquire((int)(1 + (s.length() + 2) * encoder.averageBytesPerChar()), false);
try
{
CharBuffer in = CharBuffer.wrap(s);
@@ -1062,7 +1042,7 @@ else if (crlf != null && crlf.hasRemaining())
if (result.isOverflow())
{
BufferUtil.flipToFlush(byteBuffer, 0);
- RetainableByteBuffer bigger = _bufferPool.acquire(out.capacity() + s.length() + 2, out.isDirect());
+ RetainableByteBuffer bigger = pool.acquire(out.capacity() + s.length() + 2, out.isDirect());
BufferUtil.flipToFill(bigger.getByteBuffer());
bigger.getByteBuffer().put(byteBuffer);
out.release();
@@ -1107,7 +1087,7 @@ public void sendContent(ByteBuffer content) throws IOException
*/
public void sendContent(InputStream in) throws IOException
{
- try (Blocker blocker = _writeBlocker.acquire())
+ try (Blocker.Callback blocker = _writeBlocker.callback())
{
new InputStreamWritingCB(in, blocker).iterate();
blocker.block();
@@ -1122,7 +1102,7 @@ public void sendContent(InputStream in) throws IOException
*/
public void sendContent(ReadableByteChannel in) throws IOException
{
- try (Blocker blocker = _writeBlocker.acquire())
+ try (Blocker.Callback blocker = _writeBlocker.callback())
{
new ReadableByteChannelWritingCB(in, blocker).iterate();
blocker.block();
@@ -1256,7 +1236,7 @@ public void onFlushed(long bytes) throws IOException
{
if (_firstByteNanoTime == -1 || _firstByteNanoTime == Long.MAX_VALUE)
return;
- long minDataRate = _connectionMetaData.getHttpConfiguration().getMinResponseDataRate();
+ long minDataRate = _servletChannel.getConnectionMetaData().getHttpConfiguration().getMinResponseDataRate();
_flushed += bytes;
long minFlushed = minDataRate * NanoTime.millisSince(_firstByteNanoTime) / TimeUnit.SECONDS.toMillis(1);
if (LOG.isDebugEnabled())
@@ -1276,7 +1256,7 @@ public void recycle()
_state = State.OPEN;
_apiState = ApiState.BLOCKING;
_softClose = true; // Stay closed until next request
- HttpConfiguration config = _connectionMetaData.getHttpConfiguration();
+ HttpConfiguration config = _servletChannel.getConnectionMetaData().getHttpConfiguration();
_bufferSize = config.getOutputBufferSize();
_commitSize = config.getOutputAggregationSize();
if (_commitSize > _bufferSize)
@@ -1304,7 +1284,7 @@ public void resetBuffer()
@Override
public void setWriteListener(WriteListener writeListener)
{
- if (!_servletChannel.getState().isAsync())
+ if (!_servletChannel.getServletRequestState().isAsync())
throw new IllegalStateException("!ASYNC: " + stateString());
boolean wake;
try (AutoLock l = _channelState.lock())
@@ -1313,7 +1293,7 @@ public void setWriteListener(WriteListener writeListener)
throw new IllegalStateException("!OPEN" + stateString());
_apiState = ApiState.READY;
_writeListener = writeListener;
- wake = _servletChannel.getState().onWritePossible();
+ wake = _servletChannel.getServletRequestState().onWritePossible();
}
if (wake)
_servletChannel.execute(_servletChannel::handle);
@@ -1626,7 +1606,8 @@ private InputStreamWritingCB(InputStream in, Callback callback)
super(callback, true);
_in = in;
// Reading from InputStream requires byte[], don't use direct buffers.
- _buffer = _bufferPool.acquire(getBufferSize(), false);
+ ByteBufferPool pool = _servletChannel.getRequest().getComponents().getByteBufferPool();
+ _buffer = pool.acquire(getBufferSize(), false);
}
@Override
@@ -1701,8 +1682,9 @@ private ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback)
{
super(callback, true);
_in = in;
- boolean useOutputDirectByteBuffers = _connectionMetaData.getHttpConfiguration().isUseOutputDirectByteBuffers();
- _buffer = _bufferPool.acquire(getBufferSize(), useOutputDirectByteBuffers);
+ boolean useOutputDirectByteBuffers = _servletChannel.getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers();
+ ByteBufferPool pool = _servletChannel.getRequest().getComponents().getByteBufferPool();
+ _buffer = pool.acquire(getBufferSize(), useOutputDirectByteBuffers);
}
@Override
@@ -1753,16 +1735,6 @@ public void onCompleteFailure(Throwable x)
}
}
- private static class WriteBlocker extends SharedBlockingCallback
- {
- private final ServletChannel _channel;
-
- private WriteBlocker(ServletChannel channel)
- {
- _channel = channel;
- }
- }
-
private class WriteCompleteCB implements Callback
{
@Override
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PushBuilderImpl.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PushBuilderImpl.java
index 1a3de77dc740..7d8eba157126 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PushBuilderImpl.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/PushBuilderImpl.java
@@ -114,7 +114,7 @@ public void push()
}
if (!pushPath.startsWith("/"))
- pushPath = URIUtil.addPaths(_request.getContext().getContextPath(), pushPath);
+ pushPath = URIUtil.addPaths(_request.getServletContext().getContextPath(), pushPath);
String pushParam = null;
if (_sessionId != null)
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
index b12dfe156eb9..d1e1d1ab8add 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
@@ -56,6 +56,7 @@
import jakarta.servlet.http.HttpUpgradeHandler;
import jakarta.servlet.http.Part;
import jakarta.servlet.http.PushBuilder;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletRequestInfo;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.CookieCache;
import org.eclipse.jetty.http.CookieCompliance;
@@ -77,9 +78,11 @@
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.ManagedSession;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.IO;
@@ -89,16 +92,15 @@
import org.slf4j.LoggerFactory;
/**
- * The Jetty low level implementation of the ee10 {@link HttpServletRequest} object.
- *
- *
- * This provides the bridges from Servlet {@link HttpServletRequest} to the Jetty Core {@link Request} concepts (provided by the {@link ServletContextRequest})
- *
+ * The Jetty implementation of the ee10 {@link HttpServletRequest} object.
+ * This provides the bridge from Servlet {@link HttpServletRequest} to the Jetty Core {@link Request}
+ * via the {@link ServletContextRequest}.
*/
public class ServletApiRequest implements HttpServletRequest
{
private static final Logger LOG = LoggerFactory.getLogger(ServletApiRequest.class);
- private final ServletContextRequest _request;
+ private final ServletContextRequest _servletContextRequest;
+ private final ServletChannel _servletChannel;
//TODO review which fields should be in ServletContextRequest
private AsyncContextState _async;
private String _characterEncoding;
@@ -110,30 +112,18 @@ public class ServletApiRequest implements HttpServletRequest
private Fields _contentParameters;
private Fields _parameters;
private Fields _queryParameters;
- private String _method;
private ServletMultiPartFormData.Parts _parts;
private boolean _asyncSupported = true;
protected ServletApiRequest(ServletContextRequest servletContextRequest)
{
- _request = servletContextRequest;
- }
-
- public Fields getQueryParams()
- {
- extractQueryParameters();
- return _queryParameters;
- }
-
- public Fields getContentParams()
- {
- extractContentParameters();
- return _contentParameters;
+ _servletContextRequest = servletContextRequest;
+ _servletChannel = _servletContextRequest.getServletChannel();
}
public AuthenticationState getAuthentication()
{
- return AuthenticationState.getAuthenticationState(getServletContextRequest());
+ return AuthenticationState.getAuthenticationState(getRequest());
}
private AuthenticationState getUndeferredAuthentication()
@@ -141,11 +131,11 @@ private AuthenticationState getUndeferredAuthentication()
AuthenticationState authenticationState = getAuthentication();
if (authenticationState instanceof AuthenticationState.Deferred deferred)
{
- AuthenticationState undeferred = deferred.authenticate(getServletContextRequest());
+ AuthenticationState undeferred = deferred.authenticate(getRequest());
if (undeferred != null && undeferred != authenticationState)
{
authenticationState = undeferred;
- AuthenticationState.setAuthenticationState(getServletContextRequest(), authenticationState);
+ AuthenticationState.setAuthenticationState(getRequest(), authenticationState);
}
}
return authenticationState;
@@ -154,45 +144,55 @@ private AuthenticationState getUndeferredAuthentication()
@Override
public String getMethod()
{
- if (_method == null)
- return getServletContextRequest().getMethod();
- else
- return _method;
+ return getRequest().getMethod();
}
- //TODO shouldn't really be public?
- public void setMethod(String method)
+ /**
+ * @return The {@link ServletRequestInfo} view of the {@link ServletContextRequest} as wrapped
+ * by the {@link ServletContextHandler}.
+ * @see #getRequest()
+ */
+ public ServletRequestInfo getServletRequestInfo()
{
- _method = method;
+ return _servletContextRequest;
}
- public ServletContextRequest getServletContextRequest()
+ /**
+ * @return The core {@link Request} associated with the servlet API request. This may differ
+ * from {@link ServletContextRequest} as wrapped by the {@link ServletContextHandler} as it
+ * may have been further wrapped before being passed
+ * to {@link ServletChannel#associate(Request, Response, Callback)}.
+ * @see #getServletRequestInfo()
+ * @see ServletChannel#associate(Request, Response, Callback)
+ */
+ public Request getRequest()
{
- return _request;
+ ServletChannel servletChannel = _servletChannel;
+ return servletChannel == null ? _servletContextRequest : servletChannel.getRequest();
}
public HttpFields getFields()
{
- return _request.getHeaders();
+ return getRequest().getHeaders();
}
@Override
public String getRequestId()
{
- return _request.getConnectionMetaData().getId() + "#" + _request.getId();
+ return getRequest().getConnectionMetaData().getId() + "#" + getRequest().getId();
}
@Override
public String getProtocolRequestId()
{
- return _request.getId();
+ return getRequest().getId();
}
@Override
public ServletConnection getServletConnection()
{
// TODO cache the results
- final ConnectionMetaData connectionMetaData = _request.getConnectionMetaData();
+ final ConnectionMetaData connectionMetaData = getRequest().getConnectionMetaData();
return new ServletConnection()
{
@Override
@@ -236,15 +236,15 @@ public String getAuthType()
@Override
public Cookie[] getCookies()
{
- List httpCookies = Request.getCookies(getServletContextRequest());
+ List httpCookies = Request.getCookies(getRequest());
if (httpCookies.isEmpty())
return null;
if (httpCookies instanceof ServletCookieList servletCookieList)
return servletCookieList.getServletCookies();
- ServletCookieList servletCookieList = new ServletCookieList(httpCookies, getServletContextRequest().getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
- _request.setAttribute(Request.COOKIE_ATTRIBUTE, servletCookieList);
- if (_request.getComponents().getCache().getAttribute(Request.CACHE_ATTRIBUTE) instanceof CookieCache cookieCache)
+ ServletCookieList servletCookieList = new ServletCookieList(httpCookies, getRequest().getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
+ getRequest().setAttribute(Request.COOKIE_ATTRIBUTE, servletCookieList);
+ if (getRequest().getComponents().getCache().getAttribute(Request.CACHE_ATTRIBUTE) instanceof CookieCache cookieCache)
cookieCache.replaceCookieList(servletCookieList);
return servletCookieList.getServletCookies();
}
@@ -279,7 +279,7 @@ public Enumeration getHeaders(String name)
@Override
public Enumeration getHeaderNames()
{
- return getFields().getFieldNames();
+ return Collections.enumeration(getFields().getFieldNamesCollection());
}
@Override
@@ -292,28 +292,28 @@ public int getIntHeader(String name)
@Override
public String getPathInfo()
{
- return _request._matchedPath.getPathInfo();
+ return getServletRequestInfo().getMatchedResource().getMatchedPath().getPathInfo();
}
@Override
public String getPathTranslated()
{
String pathInfo = getPathInfo();
- if (pathInfo == null || _request.getContext() == null)
+ if (pathInfo == null || getServletRequestInfo().getServletContext() == null)
return null;
- return _request.getContext().getServletContext().getRealPath(pathInfo);
+ return getServletRequestInfo().getServletContext().getServletContext().getRealPath(pathInfo);
}
@Override
public String getContextPath()
{
- return _request.getContext().getServletContextHandler().getRequestContextPath();
+ return getServletRequestInfo().getServletContext().getServletContextHandler().getRequestContextPath();
}
@Override
public String getQueryString()
{
- return _request.getHttpURI().getQuery();
+ return getRequest().getHttpURI().getQuery();
}
@Override
@@ -329,7 +329,7 @@ public String getRemoteUser()
public boolean isUserInRole(String role)
{
//obtain any substituted role name from the destination servlet
- String linkedRole = _request._mappedServlet.getServletHolder().getUserRoleLink(role);
+ String linkedRole = getServletRequestInfo().getMatchedResource().getResource().getServletHolder().getUserRoleLink(role);
AuthenticationState authenticationState = getUndeferredAuthentication();
if (authenticationState instanceof AuthenticationState.Succeeded succeededAuthentication)
@@ -354,33 +354,34 @@ public Principal getUserPrincipal()
@Override
public String getRequestedSessionId()
{
- AbstractSessionManager.RequestedSession requestedSession = _request.getRequestedSession();
+ AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession == null ? null : requestedSession.sessionId();
}
@Override
public String getRequestURI()
{
- HttpURI uri = _request.getHttpURI();
+ HttpURI uri = getRequest().getHttpURI();
return uri == null ? null : uri.getPath();
}
@Override
public StringBuffer getRequestURL()
{
- return new StringBuffer(HttpURI.build(_request.getHttpURI()).query(null).asString());
+ // Use the ServletContextRequest here as even if changed in the Request, it must match the servletPath and pathInfo
+ return new StringBuffer(HttpURI.build(getRequest().getHttpURI()).query(null).asString());
}
@Override
public String getServletPath()
{
- return _request._matchedPath.getPathMatch();
+ return getServletRequestInfo().getMatchedResource().getMatchedPath().getPathMatch();
}
@Override
public HttpSession getSession(boolean create)
{
- Session session = _request.getSession(create);
+ Session session = getRequest().getSession(create);
if (session == null)
return null;
if (session.isNew() && getAuthentication() instanceof AuthenticationState.Succeeded)
@@ -397,11 +398,11 @@ public HttpSession getSession()
@Override
public String changeSessionId()
{
- Session session = _request.getSession(false);
+ Session session = getRequest().getSession(false);
if (session == null)
throw new IllegalStateException("No session");
- session.renewId(_request, _request.getResponse());
+ session.renewId(getRequest(), _servletChannel.getResponse());
if (getRemoteUser() != null)
session.setAttribute(ManagedSession.SESSION_CREATED_SECURE, Boolean.TRUE);
@@ -412,21 +413,21 @@ public String changeSessionId()
@Override
public boolean isRequestedSessionIdValid()
{
- AbstractSessionManager.RequestedSession requestedSession = _request.getRequestedSession();
+ AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie();
}
@Override
public boolean isRequestedSessionIdFromCookie()
{
- AbstractSessionManager.RequestedSession requestedSession = _request.getRequestedSession();
+ AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.sessionIdFromCookie();
}
@Override
public boolean isRequestedSessionIdFromURL()
{
- AbstractSessionManager.RequestedSession requestedSession = _request.getRequestedSession();
+ AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie();
}
@@ -462,8 +463,9 @@ public void login(String username, String password) throws ServletException
{
try
{
+ ServletRequestInfo servletRequestInfo = getServletRequestInfo();
AuthenticationState.Succeeded succeededAuthentication = AuthenticationState.login(
- username, password, getServletContextRequest(), getServletContextRequest().getResponse());
+ username, password, getRequest(), servletRequestInfo.getServletChannel().getServletContextResponse());
if (succeededAuthentication == null)
throw new QuietException.Exception("Authentication failed for username '" + username + "'");
@@ -477,7 +479,8 @@ public void login(String username, String password) throws ServletException
@Override
public void logout() throws ServletException
{
- if (!AuthenticationState.logout(getServletContextRequest(), getServletContextRequest().getResponse()))
+ ServletRequestInfo servletRequestInfo = getServletRequestInfo();
+ if (!AuthenticationState.logout(getRequest(), servletRequestInfo.getServletChannel().getServletContextResponse()))
throw new ServletException("logout failed");
}
@@ -494,7 +497,7 @@ public Collection getParts() throws IOException, ServletException
if (config == null)
throw new IllegalStateException("No multipart config for servlet");
- ServletContextHandler contextHandler = _request.getContext().getServletContextHandler();
+ ServletContextHandler contextHandler = getServletRequestInfo().getServletContext().getServletContextHandler();
int maxFormContentSize = contextHandler.getMaxFormContentSize();
int maxFormKeys = contextHandler.getMaxFormKeys();
@@ -574,10 +577,10 @@ public T upgrade(Class handlerClass) throws IO
@Override
public PushBuilder newPushBuilder()
{
- if (!_request.getConnectionMetaData().isPushSupported())
+ if (!getRequest().getConnectionMetaData().isPushSupported())
return null;
- HttpFields.Mutable pushHeaders = HttpFields.build(_request.getHeaders(), EnumSet.of(
+ HttpFields.Mutable pushHeaders = HttpFields.build(getRequest().getHeaders(), EnumSet.of(
HttpHeader.IF_MATCH,
HttpHeader.IF_RANGE,
HttpHeader.IF_UNMODIFIED_SINCE,
@@ -594,7 +597,7 @@ public PushBuilder newPushBuilder()
pushHeaders.put(HttpHeader.REFERER, referrer);
// Any Set-Cookie in the response should be present in the push.
- HttpFields.Mutable responseHeaders = _request.getResponse().getHeaders();
+ HttpFields.Mutable responseHeaders = _servletChannel.getResponse().getHeaders();
List setCookies = new ArrayList<>(responseHeaders.getValuesList(HttpHeader.SET_COOKIE));
setCookies.addAll(responseHeaders.getValuesList(HttpHeader.SET_COOKIE2));
String cookies = pushHeaders.get(HttpHeader.COOKIE);
@@ -642,7 +645,7 @@ public PushBuilder newPushBuilder()
sessionId = getRequestedSessionId();
}
- return new PushBuilderImpl(_request, pushHeaders, sessionId);
+ return new PushBuilderImpl(ServletContextRequest.getServletContextRequest(this), pushHeaders, sessionId);
}
@Override
@@ -659,17 +662,17 @@ public Object getAttribute(String name)
case AsyncContext.ASYNC_PATH_INFO -> getPathInfo();
case AsyncContext.ASYNC_QUERY_STRING -> getQueryString();
case AsyncContext.ASYNC_MAPPING -> getHttpServletMapping();
- default -> _request.getAttribute(name);
+ default -> getRequest().getAttribute(name);
};
}
- return _request.getAttribute(name);
+ return getRequest().getAttribute(name);
}
@Override
public Enumeration getAttributeNames()
{
- Set set = _request.getAttributeNameSet();
+ Set set = getRequest().getAttributeNameSet();
if (_async != null)
{
set = new HashSet<>(set);
@@ -689,8 +692,8 @@ public String getCharacterEncoding()
{
if (_characterEncoding == null)
{
- if (_request.getContext() != null)
- _characterEncoding = _request.getContext().getServletContext().getRequestCharacterEncoding();
+ if (getRequest().getContext() != null)
+ _characterEncoding = getServletRequestInfo().getServletContext().getServletContext().getRequestCharacterEncoding();
if (_characterEncoding == null)
{
@@ -765,10 +768,10 @@ public ServletInputStream getInputStream() throws IOException
throw new IllegalStateException("READER");
_inputState = ServletContextRequest.INPUT_STREAM;
- if (_request.getServletChannel().isExpecting100Continue())
- _request.getServletChannel().continue100(_request.getHttpInput().available());
+ if (getServletRequestInfo().getServletChannel().isExpecting100Continue())
+ getServletRequestInfo().getServletChannel().continue100(getServletRequestInfo().getHttpInput().available());
- return _request.getHttpInput();
+ return getServletRequestInfo().getHttpInput();
}
@Override
@@ -798,20 +801,6 @@ public Map getParameterMap()
return Collections.unmodifiableMap(getParameters().toStringArrayMap());
}
- public Fields getContentParameters()
- {
- getParameters(); // ensure extracted
- return _contentParameters;
- }
-
- public void setContentParameters(Fields params)
- {
- if (params == null || params.getSize() == 0)
- _contentParameters = ServletContextRequest.NO_PARAMS;
- else
- _contentParameters = params;
- }
-
private Fields getParameters()
{
extractContentParameters();
@@ -855,13 +844,14 @@ private void extractContentParameters() throws BadMessageException
{
String baseType = HttpField.valueParameters(getContentType(), null);
if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
- _request.getConnectionMetaData().getHttpConfiguration().isFormEncodedMethod(getMethod()))
+ getRequest().getConnectionMetaData().getHttpConfiguration().isFormEncodedMethod(getMethod()))
{
try
{
- int maxKeys = _request.getServletRequestState().getContextHandler().getMaxFormKeys();
- int maxContentSize = _request.getServletRequestState().getContextHandler().getMaxFormContentSize();
- _contentParameters = FormFields.from(getServletContextRequest(), maxKeys, maxContentSize).get();
+ ServletContextHandler contextHandler = getServletRequestInfo().getServletContextHandler();
+ int maxKeys = contextHandler.getMaxFormKeys();
+ int maxContentSize = contextHandler.getMaxFormContentSize();
+ _contentParameters = FormFields.from(getRequest(), maxKeys, maxContentSize).get();
}
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
@@ -889,7 +879,7 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
{
try
{
- _contentParameters = FormFields.get(getServletContextRequest()).get();
+ _contentParameters = FormFields.get(getRequest()).get();
}
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
@@ -918,14 +908,14 @@ private void extractQueryParameters() throws BadMessageException
// and may have already been extracted by mergeQueryParameters().
if (_queryParameters == null)
{
- HttpURI httpURI = _request.getHttpURI();
+ HttpURI httpURI = getRequest().getHttpURI();
if (httpURI == null || StringUtil.isEmpty(httpURI.getQuery()))
_queryParameters = ServletContextRequest.NO_PARAMS;
else
{
try
{
- _queryParameters = Request.extractQueryParameters(_request, _request.getQueryEncoding());
+ _queryParameters = Request.extractQueryParameters(getRequest(), getServletRequestInfo().getQueryEncoding());
}
catch (IllegalStateException | IllegalArgumentException e)
{
@@ -939,19 +929,19 @@ private void extractQueryParameters() throws BadMessageException
@Override
public String getProtocol()
{
- return _request.getConnectionMetaData().getProtocol();
+ return getRequest().getConnectionMetaData().getProtocol();
}
@Override
public String getScheme()
{
- return _request.getHttpURI().getScheme();
+ return getRequest().getHttpURI().getScheme();
}
@Override
public String getServerName()
{
- HttpURI uri = _request.getHttpURI();
+ HttpURI uri = getRequest().getHttpURI();
if ((uri != null) && StringUtil.isNotBlank(uri.getAuthority()))
return formatAddrOrHost(uri.getHost());
else
@@ -960,13 +950,13 @@ public String getServerName()
private String formatAddrOrHost(String name)
{
- ServletChannel servletChannel = _request.getServletChannel();
+ ServletChannel servletChannel = _servletChannel;
return servletChannel == null ? HostPort.normalizeHost(name) : servletChannel.formatAddrOrHost(name);
}
private String findServerName()
{
- ServletChannel servletChannel = _request.getServletChannel();
+ ServletChannel servletChannel = _servletChannel;
if (servletChannel != null)
{
HostPort serverAuthority = servletChannel.getServerAuthority();
@@ -985,9 +975,9 @@ private String findServerName()
@Override
public int getServerPort()
{
- int port = -1;
+ int port;
- HttpURI uri = _request.getHttpURI();
+ HttpURI uri = getRequest().getHttpURI();
if ((uri != null) && StringUtil.isNotBlank(uri.getAuthority()))
port = uri.getPort();
else
@@ -1003,7 +993,7 @@ public int getServerPort()
private int findServerPort()
{
- ServletChannel servletChannel = _request.getServletChannel();
+ ServletChannel servletChannel = getServletRequestInfo().getServletChannel();
if (servletChannel != null)
{
HostPort serverAuthority = servletChannel.getServerAuthority();
@@ -1043,9 +1033,9 @@ public void close() throws IOException
}
};
}
- else if (_request.getServletChannel().isExpecting100Continue())
+ else if (getServletRequestInfo().getServletChannel().isExpecting100Continue())
{
- _request.getServletChannel().continue100(_request.getHttpInput().available());
+ getServletRequestInfo().getServletChannel().continue100(getServletRequestInfo().getHttpInput().available());
}
_inputState = ServletContextRequest.INPUT_READER;
return _reader;
@@ -1054,28 +1044,28 @@ else if (_request.getServletChannel().isExpecting100Continue())
@Override
public String getRemoteAddr()
{
- return Request.getRemoteAddr(_request);
+ return Request.getRemoteAddr(getRequest());
}
@Override
public String getRemoteHost()
{
// TODO: review.
- return Request.getRemoteAddr(_request);
+ return Request.getRemoteAddr(getRequest());
}
@Override
public void setAttribute(String name, Object attribute)
{
- Object oldValue = _request.setAttribute(name, attribute);
+ Object oldValue = getRequest().setAttribute(name, attribute);
if ("org.eclipse.jetty.server.Request.queryEncoding".equals(name))
- _request.setQueryEncoding(attribute == null ? null : attribute.toString());
+ getServletRequestInfo().setQueryEncoding(attribute == null ? null : attribute.toString());
- if (!_request.getRequestAttributeListeners().isEmpty())
+ if (!getServletRequestInfo().getRequestAttributeListeners().isEmpty())
{
- final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(_request.getContext().getServletContext(), this, name, oldValue == null ? attribute : oldValue);
- for (ServletRequestAttributeListener l : _request.getRequestAttributeListeners())
+ final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(getServletRequestInfo().getServletContext().getServletContext(), this, name, oldValue == null ? attribute : oldValue);
+ for (ServletRequestAttributeListener l : getServletRequestInfo().getRequestAttributeListeners())
{
if (oldValue == null)
l.attributeAdded(event);
@@ -1090,12 +1080,12 @@ else if (attribute == null)
@Override
public void removeAttribute(String name)
{
- Object oldValue = _request.removeAttribute(name);
+ Object oldValue = getRequest().removeAttribute(name);
- if (oldValue != null && !_request.getRequestAttributeListeners().isEmpty())
+ if (oldValue != null && !getServletRequestInfo().getRequestAttributeListeners().isEmpty())
{
- final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(_request.getContext().getServletContext(), this, name, oldValue);
- for (ServletRequestAttributeListener listener : _request.getRequestAttributeListeners())
+ final ServletRequestAttributeEvent event = new ServletRequestAttributeEvent(getServletRequestInfo().getServletContext().getServletContext(), this, name, oldValue);
+ for (ServletRequestAttributeListener listener : getServletRequestInfo().getRequestAttributeListeners())
{
listener.attributeRemoved(event);
}
@@ -1105,32 +1095,32 @@ public void removeAttribute(String name)
@Override
public Locale getLocale()
{
- return Request.getLocales(_request).get(0);
+ return Request.getLocales(getRequest()).get(0);
}
@Override
public Enumeration getLocales()
{
- return Collections.enumeration(Request.getLocales(_request));
+ return Collections.enumeration(Request.getLocales(getRequest()));
}
@Override
public boolean isSecure()
{
- return _request.getConnectionMetaData().isSecure();
+ return getRequest().getConnectionMetaData().isSecure();
}
@Override
public RequestDispatcher getRequestDispatcher(String path)
{
- ServletContextHandler.ServletScopedContext context = _request.getContext();
+ ServletContextHandler.ServletScopedContext context = getServletRequestInfo().getServletContext();
if (path == null || context == null)
return null;
// handle relative path
if (!path.startsWith("/"))
{
- String relTo = _request.getDecodedPathInContext();
+ String relTo = getServletRequestInfo().getDecodedPathInContext();
int slash = relTo.lastIndexOf("/");
if (slash > 1)
relTo = relTo.substring(0, slash + 1);
@@ -1145,13 +1135,13 @@ public RequestDispatcher getRequestDispatcher(String path)
@Override
public int getRemotePort()
{
- return Request.getRemotePort(_request);
+ return Request.getRemotePort(getRequest());
}
@Override
public String getLocalName()
{
- ServletChannel servletChannel = _request.getServletChannel();
+ ServletChannel servletChannel = getServletRequestInfo().getServletChannel();
if (servletChannel != null)
{
String localName = servletChannel.getLocalName();
@@ -1164,19 +1154,19 @@ public String getLocalName()
@Override
public String getLocalAddr()
{
- return Request.getLocalAddr(_request);
+ return Request.getLocalAddr(getRequest());
}
@Override
public int getLocalPort()
{
- return Request.getLocalPort(_request);
+ return Request.getLocalPort(getRequest());
}
@Override
public ServletContext getServletContext()
{
- return _request.getServletChannel().getServletContext();
+ return getServletRequestInfo().getServletChannel().getServletContextApi();
}
@Override
@@ -1184,10 +1174,11 @@ public AsyncContext startAsync() throws IllegalStateException
{
if (!isAsyncSupported())
throw new IllegalStateException("Async Not Supported");
- ServletRequestState state = _request.getState();
+ ServletRequestState state = getServletRequestInfo().getState();
if (_async == null)
_async = new AsyncContextState(state);
- AsyncContextEvent event = new AsyncContextEvent(_request.getContext(), _async, state, this, _request.getResponse().getHttpServletResponse());
+ ServletRequestInfo servletRequestInfo = getServletRequestInfo();
+ AsyncContextEvent event = new AsyncContextEvent(getServletRequestInfo().getServletContext(), _async, state, this, servletRequestInfo.getServletChannel().getServletContextResponse().getServletApiResponse());
state.startAsync(event);
return _async;
}
@@ -1197,10 +1188,10 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se
{
if (!isAsyncSupported())
throw new IllegalStateException("Async Not Supported");
- ServletRequestState state = _request.getState();
+ ServletRequestState state = getServletRequestInfo().getState();
if (_async == null)
_async = new AsyncContextState(state);
- AsyncContextEvent event = new AsyncContextEvent(_request.getContext(), _async, state, servletRequest, servletResponse);
+ AsyncContextEvent event = new AsyncContextEvent(getServletRequestInfo().getServletContext(), _async, state, servletRequest, servletResponse);
state.startAsync(event);
return _async;
}
@@ -1208,13 +1199,13 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se
@Override
public HttpServletMapping getHttpServletMapping()
{
- return _request._mappedServlet.getServletPathMapping(_request.getDecodedPathInContext());
+ return getServletRequestInfo().getMatchedResource().getResource().getServletPathMapping(getServletRequestInfo().getDecodedPathInContext());
}
@Override
public boolean isAsyncStarted()
{
- return _request.getState().isAsyncStarted();
+ return getServletRequestInfo().getState().isAsyncStarted();
}
@Override
@@ -1231,7 +1222,7 @@ public void setAsyncSupported(boolean asyncSupported)
@Override
public AsyncContext getAsyncContext()
{
- ServletRequestState state = _request.getServletChannel().getState();
+ ServletRequestState state = getServletRequestInfo().getServletChannel().getServletRequestState();
if (_async == null || !state.isAsyncStarted())
throw new IllegalStateException(state.getStatusString());
@@ -1247,7 +1238,7 @@ public DispatcherType getDispatcherType()
@Override
public Map getTrailerFields()
{
- HttpFields trailers = _request.getTrailers();
+ HttpFields trailers = getRequest().getTrailers();
if (trailers == null)
return Map.of();
Map trailersMap = new HashMap<>();
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
index d7798dd431d5..8211dca565f7 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
@@ -15,7 +15,6 @@
import java.io.IOException;
import java.io.PrintWriter;
-import java.nio.channels.IllegalSelectorException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@@ -27,30 +26,29 @@
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletRequestInfo;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletResponseInfo;
import org.eclipse.jetty.ee10.servlet.writer.EncodingHttpWriter;
import org.eclipse.jetty.ee10.servlet.writer.Iso88591HttpWriter;
import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter;
import org.eclipse.jetty.ee10.servlet.writer.Utf8HttpWriter;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.io.RuntimeIOException;
+import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Blocker;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
/**
- * The Jetty low level implementation of the ee10 {@link HttpServletResponse} object.
- *
- *
- * This provides the bridges from Servlet {@link HttpServletResponse} to the Jetty Core {@link Response} concepts (provided by the {@link ServletContextResponse})
- *
+ * The Jetty implementation of the ee10 {@link HttpServletResponse} object.
+ * This provides the bridge from the Servlet {@link HttpServletResponse} to the Jetty Core {@link Response}
+ * via the {@link ServletContextResponse}.
*/
public class ServletApiResponse implements HttpServletResponse
{
@@ -62,16 +60,45 @@ public class ServletApiResponse implements HttpServletResponse
ServletContextResponse.EncodingFrom.SET_LOCALE
);
- private final ServletContextResponse _response;
+ private final ServletChannel _servletChannel;
+ private final ServletContextHandler.ServletRequestInfo _servletRequestInfo;
+ private final ServletResponseInfo _servletResponseInfo;
- protected ServletApiResponse(ServletContextResponse response)
+ protected ServletApiResponse(ServletContextResponse servletContextResponse)
{
- _response = response;
+ _servletChannel = servletContextResponse.getServletContextRequest().getServletChannel();
+ _servletRequestInfo = servletContextResponse.getServletContextRequest();
+ _servletResponseInfo = servletContextResponse;
}
- public ServletContextResponse getResponse()
+ public ServletChannel getServletChannel()
{
- return _response;
+ return _servletChannel;
+ }
+
+ public ServletRequestInfo getServletRequestInfo()
+ {
+ return _servletRequestInfo;
+ }
+
+ /**
+ * @return The {@link ServletResponseInfo} for the request as provided by
+ * {@link ServletContextResponse} when wrapped by the {@link ServletContextHandler}.
+ */
+ public ServletResponseInfo getServletResponseInfo()
+ {
+ return _servletResponseInfo;
+ }
+
+ /**
+ * @return The core {@link Response} associated with the API response.
+ * This may differ from the {@link ServletContextResponse} as wrapped by the
+ * {@link ServletContextHandler} as it may have subsequently been wrapped before
+ * being passed to {@link ServletChannel#associate(Request, Response, Callback)}.
+ */
+ public Response getResponse()
+ {
+ return getServletChannel().getResponse();
}
@Override
@@ -85,22 +112,23 @@ public void addCookie(Cookie cookie)
public void addCookie(HttpCookie cookie)
{
- Response.addCookie(_response, cookie);
+ Response.addCookie(getResponse(), cookie);
}
@Override
public boolean containsHeader(String name)
{
- return _response.getHeaders().contains(name);
+ return getResponse().getHeaders().contains(name);
}
@Override
public String encodeURL(String url)
{
- SessionManager sessionManager = _response.getServletContextRequest().getServletChannel().getContextHandler().getSessionHandler();
+ SessionManager sessionManager = getServletChannel().getServletContextHandler().getSessionHandler();
if (sessionManager == null)
return url;
- return sessionManager.encodeURI(_response.getServletContextRequest(), url, getResponse().getServletContextRequest().getServletApiRequest().isRequestedSessionIdFromCookie());
+ return sessionManager.encodeURI(getServletChannel().getRequest(), url,
+ getServletChannel().getServletContextRequest().getServletApiRequest().isRequestedSessionIdFromCookie());
}
@Override
@@ -114,14 +142,14 @@ public void sendError(int sc, String msg) throws IOException
{
switch (sc)
{
- case -1 -> _response.getServletContextRequest().getServletChannel().abort(new IOException(msg));
+ case -1 -> getServletChannel().abort(new IOException(msg));
case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINT_103 ->
{
if (!isCommitted())
{
try (Blocker.Callback blocker = Blocker.callback())
{
- CompletableFuture completable = _response.writeInterim(sc, _response.getHeaders().asImmutable());
+ CompletableFuture completable = getServletChannel().getServletContextResponse().writeInterim(sc, getResponse().getHeaders().asImmutable());
blocker.completeWith(completable);
blocker.block();
}
@@ -131,7 +159,7 @@ public void sendError(int sc, String msg) throws IOException
{
if (isCommitted())
throw new IllegalStateException("Committed");
- _response.getState().sendError(sc, msg);
+ getServletRequestInfo().getServletRequestState().sendError(sc, msg);
}
}
}
@@ -160,7 +188,7 @@ public void sendRedirect(int code, String location) throws IOException
resetBuffer();
try (Blocker.Callback callback = Blocker.callback())
{
- Response.sendRedirect(_response.getServletContextRequest(), _response, callback, code, location, false);
+ Response.sendRedirect(getServletRequestInfo().getRequest(), getResponse(), callback, code, location, false);
callback.block();
}
}
@@ -168,25 +196,25 @@ public void sendRedirect(int code, String location) throws IOException
@Override
public void setDateHeader(String name, long date)
{
- _response.getHeaders().putDate(name, date);
+ getResponse().getHeaders().putDate(name, date);
}
@Override
public void addDateHeader(String name, long date)
{
- _response.getHeaders().addDateField(name, date);
+ getResponse().getHeaders().addDateField(name, date);
}
@Override
public void setHeader(String name, String value)
{
- _response.getHeaders().put(name, value);
+ getResponse().getHeaders().put(name, value);
}
@Override
public void addHeader(String name, String value)
{
- _response.getHeaders().add(name, value);
+ getResponse().getHeaders().add(name, value);
}
@Override
@@ -194,7 +222,7 @@ public void setIntHeader(String name, int value)
{
// TODO do we need int versions?
if (!isCommitted())
- _response.getHeaders().put(name, value);
+ getResponse().getHeaders().put(name, value);
}
@Override
@@ -202,136 +230,99 @@ public void addIntHeader(String name, int value)
{
// TODO do we need a native version?
if (!isCommitted())
- _response.getHeaders().add(name, Integer.toString(value));
+ getResponse().getHeaders().add(name, Integer.toString(value));
}
@Override
public void setStatus(int sc)
{
- _response.setStatus(sc);
+ getResponse().setStatus(sc);
}
@Override
public int getStatus()
{
- return _response.getStatus();
+ return getResponse().getStatus();
}
@Override
public String getHeader(String name)
{
- return _response.getHeaders().get(name);
+ return getResponse().getHeaders().get(name);
}
@Override
public Collection getHeaders(String name)
{
- return _response.getHeaders().getValuesList(name);
+ return getResponse().getHeaders().getValuesList(name);
}
@Override
public Collection getHeaderNames()
{
- return _response.getHeaders().getFieldNamesCollection();
+ return getResponse().getHeaders().getFieldNamesCollection();
}
@Override
public String getCharacterEncoding()
{
- return _response.getCharacterEncoding(false);
+ return getServletResponseInfo().getCharacterEncoding(false);
}
@Override
public String getContentType()
{
- return _response.getContentType();
+ return getServletResponseInfo().getContentType();
}
@Override
public ServletOutputStream getOutputStream() throws IOException
{
- if (_response.getOutputType() == ServletContextResponse.OutputType.WRITER)
+ if (getServletResponseInfo().getOutputType() == ServletContextResponse.OutputType.WRITER)
throw new IllegalStateException("WRITER");
- _response.setOutputType(ServletContextResponse.OutputType.STREAM);
- return _response.getHttpOutput();
+ getServletResponseInfo().setOutputType(ServletContextResponse.OutputType.STREAM);
+ return getServletChannel().getHttpOutput();
}
@Override
public PrintWriter getWriter() throws IOException
{
- if (_response.getOutputType() == ServletContextResponse.OutputType.STREAM)
+ if (getServletResponseInfo().getOutputType() == ServletContextResponse.OutputType.STREAM)
throw new IllegalStateException("STREAM");
- if (_response.getOutputType() == ServletContextResponse.OutputType.NONE)
+ ResponseWriter writer = getServletResponseInfo().getWriter();
+ if (getServletResponseInfo().getOutputType() == ServletContextResponse.OutputType.NONE)
{
- String encoding = _response.getCharacterEncoding(true);
+ String encoding = getServletResponseInfo().getCharacterEncoding(true);
Locale locale = getLocale();
- if (_response.getWriter() != null && _response.getWriter().isFor(locale, encoding))
- _response.getWriter().reopen();
+ if (writer != null && writer.isFor(locale, encoding))
+ writer.reopen();
else
{
if (StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
- _response.setWriter(new ResponseWriter(new Iso88591HttpWriter(_response.getHttpOutput()), locale, encoding));
+ getServletResponseInfo().setWriter(writer = new ResponseWriter(new Iso88591HttpWriter(getServletChannel().getHttpOutput()), locale, encoding));
else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
- _response.setWriter(new ResponseWriter(new Utf8HttpWriter(_response.getHttpOutput()), locale, encoding));
+ getServletResponseInfo().setWriter(writer = new ResponseWriter(new Utf8HttpWriter(getServletChannel().getHttpOutput()), locale, encoding));
else
- _response.setWriter(new ResponseWriter(new EncodingHttpWriter(_response.getHttpOutput(), encoding), locale, encoding));
+ getServletResponseInfo().setWriter(writer = new ResponseWriter(new EncodingHttpWriter(getServletChannel().getHttpOutput(), encoding), locale, encoding));
}
// Set the output type at the end, because setCharacterEncoding() checks for it.
- _response.setOutputType(ServletContextResponse.OutputType.WRITER);
+ getServletResponseInfo().setOutputType(ServletContextResponse.OutputType.WRITER);
}
- return _response.getWriter();
+ return writer;
}
@Override
public void setCharacterEncoding(String encoding)
{
- _response.setCharacterEncoding(encoding, ServletContextResponse.EncodingFrom.SET_CHARACTER_ENCODING);
+ getServletResponseInfo().setCharacterEncoding(encoding, ServletContextResponse.EncodingFrom.SET_CHARACTER_ENCODING);
}
@Override
public void setContentLength(int len)
{
- // Protect from setting after committed as default handling
- // of a servlet HEAD request ALWAYS sets _content length, even
- // if the getHandling committed the response!
- if (isCommitted())
- return;
-
- if (len > 0)
- {
- long written = _response.getHttpOutput().getWritten();
- if (written > len)
- throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written);
-
- _response.setContentLength(len);
- _response.getHeaders().put(HttpHeader.CONTENT_LENGTH, len);
- if (_response.isAllContentWritten(written))
- {
- try
- {
- _response.closeOutput();
- }
- catch (IOException e)
- {
- throw new RuntimeIOException(e);
- }
- }
- }
- else if (len == 0)
- {
- long written = _response.getHttpOutput().getWritten();
- if (written > 0)
- throw new IllegalArgumentException("setContentLength(0) when already written " + written);
- _response.setContentLength(len);
- _response.getHeaders().put(HttpFields.CONTENT_LENGTH_0);
- }
- else
- {
- _response.setContentLength(len);
- _response.getHeaders().remove(HttpHeader.CONTENT_LENGTH);
- }
+ setContentLengthLong(len);
}
@Override
@@ -342,8 +333,13 @@ public void setContentLengthLong(long len)
// if the getHandling committed the response!
if (isCommitted())
return;
- _response.setContentLength(len);
- _response.getHeaders().put(HttpHeader.CONTENT_LENGTH, len);
+
+ if (len > 0)
+ getResponse().getHeaders().put(HttpHeader.CONTENT_LENGTH, len);
+ else if (len == 0)
+ getResponse().getHeaders().put(HttpFields.CONTENT_LENGTH_0);
+ else
+ getResponse().getHeaders().remove(HttpHeader.CONTENT_LENGTH);
}
@Override
@@ -354,70 +350,20 @@ public void setContentType(String contentType)
if (contentType == null)
{
- if (_response.isWriting() && _response.getCharacterEncoding() != null)
- throw new IllegalSelectorException();
-
- if (_response.getLocale() == null)
- _response.setCharacterEncoding(null);
- _response.setMimeType(null);
- _response.setContentType(null);
- _response.getHeaders().remove(HttpHeader.CONTENT_TYPE);
+ if (getServletResponseInfo().isWriting() && getServletResponseInfo().getCharacterEncoding() != null)
+ throw new IllegalStateException();
+
+ getResponse().getHeaders().remove(HttpHeader.CONTENT_TYPE);
}
else
{
- _response.setContentType(contentType);
- _response.setMimeType(MimeTypes.CACHE.get(contentType));
-
- String charset = MimeTypes.getCharsetFromContentType(contentType);
- if (charset == null && _response.getMimeType() != null && _response.getMimeType().isCharsetAssumed())
- charset = _response.getMimeType().getCharsetString();
-
- if (charset == null)
- {
- switch (_response.getEncodingFrom())
- {
- case NOT_SET:
- break;
- case DEFAULT:
- case INFERRED:
- case SET_CONTENT_TYPE:
- case SET_LOCALE:
- case SET_CHARACTER_ENCODING:
- {
- _response.setContentType(contentType + ";charset=" + _response.getCharacterEncoding());
- _response.setMimeType(MimeTypes.CACHE.get(_response.getContentType()));
- break;
- }
- default:
- throw new IllegalStateException(_response.getEncodingFrom().toString());
- }
- }
- else if (_response.isWriting() && !charset.equalsIgnoreCase(_response.getCharacterEncoding()))
- {
- // too late to change the character encoding;
- _response.setContentType(MimeTypes.getContentTypeWithoutCharset(_response.getContentType()));
- if (_response.getCharacterEncoding() != null && (_response.getMimeType() == null || !_response.getMimeType().isCharsetAssumed()))
- _response.setContentType(_response.getContentType() + ";charset=" + _response.getCharacterEncoding());
- _response.setMimeType(MimeTypes.CACHE.get(_response.getContentType()));
- }
- else
- {
- _response.setRawCharacterEncoding(charset, ServletContextResponse.EncodingFrom.SET_CONTENT_TYPE);
- }
-
- if (HttpGenerator.__STRICT || _response.getMimeType() == null)
- _response.getHeaders().put(HttpHeader.CONTENT_TYPE, _response.getContentType());
- else
- {
- _response.setContentType(_response.getMimeType().asString());
- _response.getHeaders().put(_response.getMimeType().getContentTypeField());
- }
+ getResponse().getHeaders().put(HttpHeader.CONTENT_TYPE, contentType);
}
}
public long getContentCount()
{
- return _response.getHttpOutput().getWritten();
+ return getServletChannel().getHttpOutput().getWritten();
}
@Override
@@ -429,20 +375,20 @@ public void setBufferSize(int size)
throw new IllegalStateException("cannot set buffer size after response has " + getContentCount() + " bytes already written");
if (size < MIN_BUFFER_SIZE)
size = MIN_BUFFER_SIZE;
- _response.getHttpOutput().setBufferSize(size);
+ getServletChannel().getHttpOutput().setBufferSize(size);
}
@Override
public int getBufferSize()
{
- return _response.getHttpOutput().getBufferSize();
+ return getServletChannel().getHttpOutput().getBufferSize();
}
@Override
public void flushBuffer() throws IOException
{
- if (!_response.getHttpOutput().isClosed())
- _response.getHttpOutput().flush();
+ if (!getServletChannel().getHttpOutput().isClosed())
+ getServletChannel().getHttpOutput().flush();
}
@Override
@@ -450,17 +396,17 @@ public void resetBuffer()
{
if (isCommitted())
throw new IllegalStateException("Committed");
- _response.getHttpOutput().resetBuffer();
- _response.getHttpOutput().reopen();
+ getServletChannel().getHttpOutput().resetBuffer();
+ getServletChannel().getHttpOutput().reopen();
}
@Override
public boolean isCommitted()
{
// If we are in sendError state, we pretend to be committed
- if (_response.getServletContextRequest().getServletChannel().isSendError())
+ if (getServletChannel().isSendError())
return true;
- return _response.getServletContextRequest().getServletChannel().isCommitted();
+ return getServletChannel().isCommitted();
}
@Override
@@ -469,14 +415,13 @@ public void reset()
if (isCommitted())
throw new IllegalStateException("Committed");
- _response.reset();
-
+ getResponse().reset();
- ServletApiRequest servletApiRequest = _response.getServletContextRequest().getServletApiRequest();
- ManagedSession session = servletApiRequest.getServletContextRequest().getManagedSession();
+ ServletApiRequest servletApiRequest = getServletChannel().getServletContextRequest().getServletApiRequest();
+ ManagedSession session = servletApiRequest.getServletRequestInfo().getManagedSession();
if (session != null && session.isNew())
{
- SessionManager sessionManager = servletApiRequest.getServletContextRequest().getSessionManager();
+ SessionManager sessionManager = servletApiRequest.getServletRequestInfo().getSessionManager();
if (sessionManager != null)
{
HttpCookie cookie = sessionManager.getSessionCookie(session, servletApiRequest.getServletConnection().isSecure());
@@ -494,41 +439,41 @@ public void setLocale(Locale locale)
if (locale == null)
{
- _response.setLocale(null);
- _response.getHeaders().remove(HttpHeader.CONTENT_LANGUAGE);
- if (_response.getEncodingFrom() == ServletContextResponse.EncodingFrom.SET_LOCALE)
- _response.setCharacterEncoding(null, ServletContextResponse.EncodingFrom.NOT_SET);
+ getServletResponseInfo().setLocale(null);
+ getResponse().getHeaders().remove(HttpHeader.CONTENT_LANGUAGE);
+ if (getServletResponseInfo().getEncodingFrom() == ServletContextResponse.EncodingFrom.SET_LOCALE)
+ getServletResponseInfo().setCharacterEncoding(null, ServletContextResponse.EncodingFrom.NOT_SET);
}
else
{
- _response.setLocale(locale);
- _response.getHeaders().put(HttpHeader.CONTENT_LANGUAGE, StringUtil.replace(locale.toString(), '_', '-'));
+ getServletResponseInfo().setLocale(locale);
+ getResponse().getHeaders().put(HttpHeader.CONTENT_LANGUAGE, StringUtil.replace(locale.toString(), '_', '-'));
- if (_response.getOutputType() != ServletContextResponse.OutputType.NONE)
+ if (getServletResponseInfo().getOutputType() != ServletContextResponse.OutputType.NONE)
return;
- ServletContextHandler.ServletScopedContext context = _response.getServletContextRequest().getServletChannel().getContext();
+ ServletContextHandler.ServletScopedContext context = getServletChannel().getContext();
if (context == null)
return;
String charset = context.getServletContextHandler().getLocaleEncoding(locale);
- if (!StringUtil.isEmpty(charset) && LOCALE_OVERRIDE.contains(_response.getEncodingFrom()))
- _response.setCharacterEncoding(charset, ServletContextResponse.EncodingFrom.SET_LOCALE);
+ if (!StringUtil.isEmpty(charset) && LOCALE_OVERRIDE.contains(getServletResponseInfo().getEncodingFrom()))
+ getServletResponseInfo().setCharacterEncoding(charset, ServletContextResponse.EncodingFrom.SET_LOCALE);
}
}
@Override
public Locale getLocale()
{
- if (_response.getLocale() == null)
+ if (getServletResponseInfo().getLocale() == null)
return Locale.getDefault();
- return _response.getLocale();
+ return getServletResponseInfo().getLocale();
}
@Override
public Supplier
* Once the parsing of the multipart/form-data content completes successfully,
* objects of this class are completed with a {@link Parts} object.
* Objects of this class may be configured to save multipart files in a configurable
@@ -67,241 +69,31 @@
*
* @see Parts
*/
-public class MultiPartFormData extends CompletableFuture
+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)
- {
- parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
- }
-
- /**
- * @return the boundary string
- */
- public String getBoundary()
+ private MultiPartFormData()
{
- return parser.getBoundary();
}
- /**
- * Parses the given multipart/form-data content.
- * Returns this {@code MultiPartFormData} object,
- * so that it can be used in the typical "fluent"
- * style of {@link CompletableFuture}.
- *
- * @param content the multipart/form-data content to parse
- * @return this {@code MultiPartFormData} object
- */
- public MultiPartFormData parse(Content.Source content)
+ public static CompletableFuture from(Attributes attributes, String boundary, Function> parse)
{
- new Runnable()
+ @SuppressWarnings("unchecked")
+ CompletableFuture futureParts = (CompletableFuture)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 (Content.Chunk.isFailure(chunk))
- {
- listener.onFailure(chunk.getFailure());
- return;
- }
- parse(chunk);
- chunk.release();
- if (chunk.isLast() || isDone())
- return;
- }
- }
- }.run();
- return this;
- }
-
- /**
- * Parses the given chunk containing multipart/form-data bytes.
- * One or more chunks may be passed to this method, until the parsing
- * of the multipart/form-data content completes.
- *
- * @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);
- }
-
- /**
- * Returns the default charset as specified by
- * RFC 7578, section 4.6,
- * that is the charset specified by the part named {@code _charset_}.
- * If that part is not present, returns {@code null}.
- *
- * @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;
- }
-
- /**
- * Sets the directory where the files uploaded in the parts will be saved.
- *
- * @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;
- }
-
- /**
- * Sets the maximum memory file size in bytes, after which files will be saved
- * in the directory specified by {@link #setFilesDirectory(Path)}.
- * Use value {@code 0} to always save the files in the directory.
- * Use value {@code -1} to never save the files in the directory.
- *
- * @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;
}
/**
* An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.
*/
- public class Parts implements Iterable, Closeable
+ public static class Parts implements Iterable, Closeable
{
private final List parts;
@@ -310,11 +102,6 @@ private Parts(List parts)
this.parts = parts;
}
- public MultiPartFormData getMultiPartFormData()
- {
- return MultiPartFormData.this;
- }
-
/**
* 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 @@ protected HttpFields customizePartHeaders(MultiPart.Part part)
}
}
- private class PartsListener extends MultiPart.AbstractPartsListener
+ public static class Parser
{
- private final AutoLock lock = new AutoLock();
- private final List parts = new ArrayList<>();
- private final List 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;
+
+ public Parser(String boundary)
+ {
+ parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
+ }
- @Override
- public void onPartContent(Content.Chunk chunk)
+ public CompletableFuture parse(Content.Source content)
{
- ByteBuffer buffer = chunk.getByteBuffer();
- String fileName = getFileName();
- if (fileName != null || isUseFilesForPartsWithoutFileName())
+ ContentSourceCompletableFuture 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)
+ boolean failed = super.completeExceptionally(failure);
+ if (failed)
+ listener.fail(failure);
+ return failed;
+ }
+ };
+ futureParts.parse();
+ return futureParts;
+ }
+
+ /**
+ * @return the boundary string
+ */
+ public String getBoundary()
+ {
+ return parser.getBoundary();
+ }
+
+ /**
+ * Returns the default charset as specified by
+ * RFC 7578, section 4.6,
+ * that is the charset specified by the part named {@code _charset_}.
+ * If that part is not present, returns {@code null}.
+ *
+ * @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;
+ }
+
+ /**
+ * Sets the directory where the files uploaded in the parts will be saved.
+ *
+ * @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;
+ }
+
+ /**
+ * Sets the maximum memory file size in bytes, after which files will be saved
+ * in the directory specified by {@link #setFilesDirectory(Path)}.
+ * Use value {@code 0} to always save the files in the directory.
+ * Use value {@code -1} to never save the files in the directory.
+ *
+ * @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()
+ {
+ return listener.getPartsSize();
+ }
+
+ private class PartsListener extends MultiPart.AbstractPartsListener
+ {
+ private final AutoLock lock = new AutoLock();
+ private final List parts = new ArrayList<>();
+ private final List partChunks = new ArrayList<>();
+ private long fileSize;
+ private long memoryFileSize;
+ private Path filePath;
+ private SeekableByteChannel fileChannel;
+ private Throwable failure;
+
+ @Override
+ public void onPartContent(Content.Chunk chunk)
+ {
+ ByteBuffer buffer = chunk.getByteBuffer();
+ String fileName = getFileName();
+ if (fileName != null || isUseFilesForPartsWithoutFileName())
+ {
+ long maxFileSize = getMaxFileSize();
+ fileSize += buffer.remaining();
+ if (maxFileSize >= 0 && fileSize > maxFileSize)
{
- try
+ onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
+ return;
+ }
+
+ long maxMemoryFileSize = getMaxMemoryFileSize();
+ if (maxMemoryFileSize >= 0)
+ {
+ memoryFileSize += buffer.remaining();
+ if (memoryFileSize > maxMemoryFileSize)
{
- // Must save to disk.
- if (ensureFileChannel())
+ try
{
- // Write existing memory chunks.
- List partChunks;
- try (AutoLock ignored = lock.lock())
+ // Must save to disk.
+ if (ensureFileChannel())
{
- partChunks = List.copyOf(this.partChunks);
- }
- for (Content.Chunk c : partChunks)
- {
- write(c.getByteBuffer());
+ // Write existing memory chunks.
+ List 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);
}
- write(buffer);
- if (chunk.isLast())
- close();
- }
- catch (Throwable x)
- {
- onFailure(x);
- }
- try (AutoLock ignored = lock.lock())
- {
- partChunks.forEach(Content.Chunk::release);
- partChunks.clear();
+ try (AutoLock ignored = lock.lock())
+ {
+ partChunks.forEach(Content.Chunk::release);
+ partChunks.clear();
+ }
+ return;
}
- return;
}
}
+ // Retain the chunk because it is stored for later use.
+ chunk.retain();
+ try (AutoLock ignored = lock.lock())
+ {
+ partChunks.add(chunk);
+ }
}
- // Retain the chunk because it is stored for later use.
- chunk.retain();
- try (AutoLock ignored = lock.lock())
- {
- partChunks.add(chunk);
- }
- }
- private void write(ByteBuffer buffer) throws Exception
- {
- int remaining = buffer.remaining();
- while (remaining > 0)
+ private void write(ByteBuffer buffer) throws Exception
{
- SeekableByteChannel channel = fileChannel();
- if (channel == null)
- throw new IllegalStateException();
- int written = channel.write(buffer);
- if (written == 0)
- throw new NonWritableChannelException();
- remaining -= written;
+ 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
+ private void close()
{
- Closeable closeable = fileChannel();
- if (closeable != null)
- closeable.close();
- }
- catch (Throwable x)
- {
- onFailure(x);
+ 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())
+ @Override
+ public void onPart(String name, String fileName, HttpFields headers)
{
- 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);
+ 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);
+ }
}
- }
- @Override
- public void onComplete()
- {
- super.onComplete();
- List result;
- try (AutoLock ignored = lock.lock())
+ @Override
+ public void onComplete()
{
- result = List.copyOf(parts);
+ super.onComplete();
+ List result;
+ try (AutoLock ignored = lock.lock())
+ {
+ result = List.copyOf(parts);
+ Parser.this.parts = new Parts(result);
+ }
}
- complete(new Parts(result));
- }
- Charset getDefaultCharset()
- {
- try (AutoLock ignored = lock.lock())
+ Charset getDefaultCharset()
{
- return parts.stream()
- .filter(part -> "_charset_".equals(part.getName()))
- .map(part -> part.getContentAsString(US_ASCII))
- .map(Charset::forName)
- .findFirst()
- .orElse(null);
+ 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);
+ }
}
- }
- int getPartsSize()
- {
- try (AutoLock ignored = lock.lock())
+ int getPartsSize()
{
- return parts.size();
+ try (AutoLock ignored = lock.lock())
+ {
+ return parts.size();
+ }
}
- }
-
- @Override
- public void onFailure(Throwable failure)
- {
- super.onFailure(failure);
- completeExceptionally(failure);
- }
- private void fail(Throwable cause)
- {
- List partsToFail;
- try (AutoLock ignored = lock.lock())
+ @Override
+ public void onFailure(Throwable failure)
{
- if (failure != null)
- return;
- failure = cause;
- partsToFail = List.copyOf(parts);
- parts.clear();
- partChunks.forEach(Content.Chunk::release);
- partChunks.clear();
+ fail(failure);
}
- partsToFail.forEach(p -> p.fail(cause));
- close();
- delete();
- }
- private SeekableByteChannel fileChannel()
- {
- try (AutoLock ignored = lock.lock())
+ private void fail(Throwable cause)
{
- return fileChannel;
+ List 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 void delete()
- {
- try
+ private SeekableByteChannel fileChannel()
{
- Path path = null;
try (AutoLock ignored = lock.lock())
{
- if (filePath != null)
- path = filePath;
- filePath = null;
- fileChannel = null;
+ return fileChannel;
}
- if (path != null)
- Files.delete(path);
- }
- catch (Throwable x)
- {
- if (LOG.isTraceEnabled())
- LOG.trace("IGNORED", x);
}
- }
- private boolean isFailed()
- {
- try (AutoLock ignored = lock.lock())
+ private void delete()
{
- return failure != null;
+ 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 ensureFileChannel()
- {
- try (AutoLock ignored = lock.lock())
+ private boolean isFailed()
{
- if (fileChannel != null)
- return false;
- createFileChannel();
- return true;
+ try (AutoLock ignored = lock.lock())
+ {
+ return failure != null;
+ }
}
- }
- private void createFileChannel()
- {
- try (AutoLock ignored = lock.lock())
+ private boolean ensureFileChannel()
{
- 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())
+ {
+ if (fileChannel != null)
+ return false;
+ createFileChannel();
+ return true;
+ }
}
- catch (Throwable x)
+
+ private void createFileChannel()
{
- onFailure(x);
+ 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);
+ }
}
}
}
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
index a1f771f1ca28..5ccff5406a65 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java
@@ -16,19 +16,20 @@
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 void dispose()
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 void testBadMultiPart() throws Exception
"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 void testFinalBoundaryOnly() throws Exception
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 void testEmpty() throws Exception
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 void testEmptyStringBoundary() throws Exception
----\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 void testEmptyStringBoundary() throws Exception
@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 void testNoBody() throws Exception
@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 void testBodyWithOnlyCRLF() throws Exception
public void testLeadingWhitespaceBodyWithCRLF() throws Exception
{
String body = """
-
+
\r
\r
@@ -303,14 +291,14 @@ public void testLeadingWhitespaceBodyWithCRLF() throws Exception
--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 void testLeadingWhitespaceBodyWithoutCRLF() throws Exception
--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 void testLeadingWhitespaceBodyWithoutCRLF() throws Exception
@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 void testDefaultLimits() throws Exception
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 void testDefaultLimits() throws Exception
@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 void testRequestContentTooBig() throws Exception
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 void testRequestContentTooBig() throws Exception
@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 void testFileTooBig() throws Exception
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 void testFileTooBig() throws Exception
@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 void testTwoFilesOneInMemoryOneOnDisk() throws Exception
$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 void testTwoFilesOneInMemoryOneOnDisk() throws Exception
@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 void testPartWrite() throws Exception
$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 void testPartWrite() throws Exception
@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 void testPathPartDelete() throws Exception
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 void testPathPartDelete() throws Exception
@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 void testAbort()
--AaB03x--\r
""";
// Parse only part of the content.
- formData.parse(asChunk(body, false));
+ Content.Sink.write(source, false, body, Callback.NOOP);
+
+ CompletableFuture 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 void testMaxHeaderLength() throws Exception
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 void testMaxHeaderLength() throws Exception
@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 void testDefaultCharset() throws Exception
\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));
-
- try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
+ CompletableFuture 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 = futureParts.get(5, TimeUnit.SECONDS))
{
Charset defaultCharset = formData.getDefaultCharset();
assertEquals(ISO_8859_1, defaultCharset);
@@ -669,7 +662,8 @@ public void testDefaultCharset() throws Exception
@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 void testPartWithBackSlashInFileName() throws Exception
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 void testPartWithBackSlashInFileName() throws Exception
@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 void testPartWithWindowsFileName() throws Exception
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 void testPartWithWindowsFileName() throws Exception
@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 void testCorrectlyEncodedMSFilename() throws Exception
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 void testCorrectlyEncodedMSFilename() throws Exception
@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 void testWriteFilesForPartWithoutFileName() throws Exception
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 void testWriteFilesForPartWithoutFileName() throws Exception
@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 void testPartsWithSameName() throws Exception
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 void testPartsWithSameName() throws Exception
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;
+ }
+ }
}
diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java
new file mode 100644
index 000000000000..f44d92832618
--- /dev/null
+++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourceCompletableFuture.java
@@ -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;
+
+/**
+ * A utility class to convert content from a {@link Content.Source} to an instance
+ * available via a {@link CompletableFuture}.
+ * An example usage to asynchronously read UTF-8 content is:
+ * {@code
+ * public static class CompletableUTF8String extends ContentSourceCompletableFuture;
+ * {
+ * 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);
+ * }
+ */
+public abstract class ContentSourceCompletableFuture extends CompletableFuture
+{
+ private final Content.Source _content;
+
+ public ContentSourceCompletableFuture(Content.Source content)
+ {
+ _content = content;
+ }
+
+ /**
+ * Initiates the parsing of the {@link Content.Source}.
+ * 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}.
+ * Internally, this method is called multiple times to progress
+ * the parsing in response to {@link Content.Source#demand(Runnable)}
+ * calls.
+ * Exceptions thrown during parsing result in this
+ * {@link CompletableFuture} to be completed exceptionally.
+ */
+ 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;
+ }
+ }
+ }
+
+ /**
+ * Called by {@link #parse()} to parse a {@link org.eclipse.jetty.io.Content.Chunk}.
+ *
+ * @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;
+
+ /**
+ * Callback method that informs the parsing about how to handle transient failures.
+ *
+ * @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;
+ }
+}
+
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java
index 1f7f949add17..eaf96ccb5b87 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java
@@ -22,6 +22,8 @@
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 @@
* 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 implements Runnable
+public class FormFields extends ContentSourceCompletableFuture
{
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,57 +59,117 @@ public static Charset getFormEncodedCharset(Request request)
return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs);
}
+ /**
+ * 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)
+ {
+ 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 get(Request request)
+ {
+ Object attr = request.getAttribute(FormFields.class.getName());
+ if (attr instanceof FormFields futureFormFields)
+ return futureFormFields;
+ 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 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);
}
+ /**
+ * 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 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);
}
- public static void set(Request request, CompletableFuture fields)
+ /**
+ * 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 from(Request request, int maxFields, int maxLength)
{
- request.setAttribute(FormFields.class.getName(), fields);
+ return from(request, getFormEncodedCharset(request), maxFields, maxLength);
}
- public static CompletableFuture get(Request request)
+ /**
+ * 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 from(Request request, Charset charset, int maxFields, int maxLength)
{
- Object attr = request.getAttribute(FormFields.class.getName());
- if (attr instanceof FormFields futureFormFields)
- return futureFormFields;
- return EMPTY;
+ return from(request, request, charset, maxFields, maxLength);
}
- public static CompletableFuture from(Request request, int maxFields, int 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 from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
{
- Object attr = request.getAttribute(FormFields.class.getName());
+ 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 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 @@ private static int getRequestAttribute(Request request, String attribute)
}
}
- private final Content.Source _source;
private final Fields _fields;
private final CharsetStringBuilder _builder;
private final int _maxFields;
@@ -136,9 +197,9 @@ private static int getRequestAttribute(Request request, String attribute)
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 FormFields(Content.Source source, Charset charset, int maxFields, int max
}
@Override
- public void run()
+ protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
{
- Content.Chunk chunk = null;
- try
+ String value = null;
+ ByteBuffer buffer = chunk.getByteBuffer();
+
+ do
{
- while (true)
+ loop:
+ while (BufferUtil.hasContent(buffer))
{
- chunk = _source.read();
- if (chunk == null)
- {
- _source.demand(this);
- return;
- }
-
- if (Content.Chunk.isFailure(chunk))
+ byte b = buffer.get();
+ switch (_percent)
{
- completeExceptionally(chunk.getFailure());
- return;
- }
-
- while (true)
- {
- Fields.Field field = parse(chunk);
- if (field == null)
- break;
- if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
+ case 1 ->
{
- chunk.release();
- // Do not double release if completeExceptionally() throws.
- chunk = null;
- completeExceptionally(new IllegalStateException("form with too many fields"));
- return;
+ _percentCode = b;
+ _percent++;
+ continue;
+ }
+ case 2 ->
+ {
+ _builder.append(decodeHexByte((char)_percentCode, (char)b));
+ _percent = 0;
+ continue;
}
- _fields.add(field);
}
- chunk.release();
- if (chunk.isLast())
+ if (_name == null)
{
- // 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
- {
- String value = null;
- ByteBuffer buffer = chunk.getByteBuffer();
- loop:
- while (BufferUtil.hasContent(buffer))
- {
- byte b = buffer.get();
- switch (_percent)
- {
- case 1 ->
- {
- _percentCode = b;
- _percent++;
- continue;
+ switch (b)
+ {
+ case '=' ->
+ {
+ _name = _builder.build();
+ checkLength(_name);
+ }
+ case '+' -> _builder.append((byte)' ');
+ case '%' -> _percent++;
+ default -> _builder.append(b);
+ }
}
- case 2 ->
+ else
{
- _builder.append(decodeHexByte((char)_percentCode, (char)b));
- _percent = 0;
- continue;
- }
- }
-
- if (_name == null)
- {
- switch (b)
- {
- case '=' ->
+ switch (b)
{
- _name = _builder.build();
- checkLength(_name);
+ case '&' ->
+ {
+ value = _builder.build();
+ checkLength(value);
+ break loop;
+ }
+ case '+' -> _builder.append((byte)' ');
+ case '%' -> _percent++;
+ default -> _builder.append(b);
}
- case '+' -> _builder.append((byte)' ');
- case '%' -> _percent++;
- default -> _builder.append(b);
}
}
- else
+
+ if (_name != null)
{
- switch (b)
+ if (value == null && chunk.isLast())
{
- case '&' ->
+ if (_percent > 0)
{
- value = _builder.build();
- checkLength(value);
- break loop;
+ _builder.append((byte)'%');
+ _builder.append(_percentCode);
}
- case '+' -> _builder.append((byte)' ');
- case '%' -> _percent++;
- default -> _builder.append(b);
+ value = _builder.build();
+ checkLength(value);
}
- }
- }
- if (_name != null)
- {
- if (value == null && chunk.isLast())
- {
- if (_percent > 0)
+ if (value != null)
{
- _builder.append((byte)'%');
- _builder.append(_percentCode);
+ 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);
}
- value = _builder.build();
- checkLength(value);
- }
-
- if (value != null)
- {
- Fields.Field field = new Fields.Field(_name, value);
- _name = null;
- return field;
}
}
+ while (BufferUtil.hasContent(buffer));
- return null;
+ return chunk.isLast() ? _fields : null;
}
private void checkLength(String nameOrValue)
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java
index 5f681da33ec8..a978b054f202 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java
@@ -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.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;
@@ -115,7 +112,6 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte
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);
};
}
@@ -270,102 +266,4 @@ private void executeProcess(Fields fields, Throwable x)
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 (Content.Chunk.isFailure(chunk))
- {
- _formData.completeExceptionally(chunk.getFailure());
- return;
- }
- _formData.parse(chunk);
- chunk.release();
- if (chunk.isLast())
- {
- if (!_formData.isDone())
- process(null, new IOException("Incomplete multipart"));
- return;
- }
- }
- }
- }
}
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java
new file mode 100644
index 000000000000..cfd0b1ec650c
--- /dev/null
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/FormFieldsTest.java
@@ -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 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 chunks, Charset charset, int maxFields, int maxLength, Map expected)
+ throws Exception
+ {
+ AsyncContent source = new AsyncContent();
+ Attributes attributes = new Attributes.Mapped();
+ CompletableFuture 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 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);
+ }
+ }
+}
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartByteRangesTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartByteRangesTest.java
index 6e787edc799e..745de4c3adae 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartByteRangesTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartByteRangesTest.java
@@ -119,9 +119,8 @@ public boolean handle(Request request, Response response, Callback callback)
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);
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
index d46555dd6c4e..e9087a0a8ad3 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
@@ -17,8 +17,6 @@
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 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 void testSimpleMultiPart() throws Exception
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 boolean handle(Request request, Response response, Callback callback)
}
}
- @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 void testEchoMultiPart() throws Exception
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 boolean handle(Request request, Response response, Callback callback)
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));
+ }
}
}
}
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerByteRangesTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerByteRangesTest.java
index f335ace1f6ec..70b07e1f5fcb 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerByteRangesTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerByteRangesTest.java
@@ -175,7 +175,7 @@ private void testTwoRanges(HttpHeader requestRangeHeader, String responseContent
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());
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java
index f7b9c918df9b..1c94cd813cb7 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/gzip/GzipHandlerTest.java
@@ -1984,11 +1984,8 @@ public static class DumpHandler extends Handler.Abstract
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());
diff --git a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartGeneratorConfiguration.java b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartGeneratorConfiguration.java
index 14bd1ce54f6c..f99ebd5ad828 100644
--- a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartGeneratorConfiguration.java
+++ b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartGeneratorConfiguration.java
@@ -764,7 +764,7 @@ private void outholder(XmlAppendable out, MetaData md, ServletHolder holder) thr
}
//multipart-config
- MultipartConfigElement multipartConfig = ((ServletHolder.Registration)holder.getRegistration()).getMultipartConfig();
+ MultipartConfigElement multipartConfig = holder.getRegistration().getMultipartConfigElement();
if (multipartConfig != null)
{
out.openTag("multipart-config", origin(md, holder.getName() + ".servlet.multipart-config"));
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
index d1bc1702c1f0..4cf48e546e76 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
@@ -309,6 +309,15 @@ public Object getAttribute(String name)
{
return null;
}
+ case ServletContextRequest.MULTIPART_CONFIG_ELEMENT ->
+ {
+ // If we already have future parts, return the configuration of the wrapped request.
+ if (super.getAttribute(ServletMultiPartFormData.class.getName()) != null)
+ return super.getAttribute(name);
+ // otherwise, return the configuration of this mapping
+ return _mappedServlet.getServletHolder().getMultipartConfigElement();
+ }
+
default ->
{
return super.getAttribute(name);
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/EagerFormHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/EagerFormHandler.java
new file mode 100644
index 000000000000..015d051edda4
--- /dev/null
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/EagerFormHandler.java
@@ -0,0 +1,83 @@
+//
+// ========================================================================
+// 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.ee10.servlet;
+
+import java.util.concurrent.CompletableFuture;
+
+import jakarta.servlet.ServletRequest;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.FormFields;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Handler to eagerly and asynchronously read and parse {@link MimeTypes.Type#FORM_ENCODED} and
+ * {@link MimeTypes.Type#MULTIPART_FORM_DATA} content prior to invoking the {@link ServletHandler},
+ * which can then consume them with blocking APIs but without blocking.
+ * @see FormFields#from(Request)
+ * @see ServletMultiPartFormData#from(ServletRequest)
+ */
+public class EagerFormHandler extends Handler.Wrapper
+{
+ public EagerFormHandler()
+ {
+ this(null);
+ }
+
+ public EagerFormHandler(Handler handler)
+ {
+ super(handler);
+ }
+
+ @Override
+ public boolean handle(Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception
+ {
+ String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
+ if (contentType == null)
+ return super.handle(request, response, callback);
+
+ MimeTypes.Type mimeType = MimeTypes.getBaseType(contentType);
+ if (mimeType == null)
+ return super.handle(request, response, callback);
+
+ CompletableFuture> future = switch (mimeType)
+ {
+ case FORM_ENCODED -> FormFields.from(request);
+ case MULTIPART_FORM_DATA -> ServletMultiPartFormData.from(Request.as(request, ServletContextRequest.class).getServletApiRequest(), contentType);
+ default -> null;
+ };
+
+ if (future == null)
+ return super.handle(request, response, callback);
+
+ future.whenComplete((result, failure) ->
+ {
+ // The result and failure are not handled here. Rather we call the next handler
+ // to allow the normal processing to handle the result or failure, which will be
+ // provided via the attribute to ServletApiRequest#getParts()
+ try
+ {
+ if (!super.handle(request, response, callback))
+ callback.failed(new IllegalStateException("Not Handled"));
+ }
+ catch (Throwable x)
+ {
+ callback.failed(x);
+ }
+ });
+ return true;
+ }
+}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
index d1e1d1ab8add..9dbf56b12659 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
@@ -34,11 +34,11 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
-import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConnection;
import jakarta.servlet.ServletContext;
@@ -489,33 +489,26 @@ public Collection getParts() throws IOException, ServletException
{
if (_parts == null)
{
- String contentType = getContentType();
- if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
- throw new ServletException("Unsupported Content-Type [%s], expected [%s]".formatted(contentType, MimeTypes.Type.MULTIPART_FORM_DATA.asString()));
-
- MultipartConfigElement config = (MultipartConfigElement)getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
- if (config == null)
- throw new IllegalStateException("No multipart config for servlet");
+ try
+ {
+ CompletableFuture futureServletMultiPartFormData = ServletMultiPartFormData.from(this);
- ServletContextHandler contextHandler = getServletRequestInfo().getServletContext().getServletContextHandler();
- int maxFormContentSize = contextHandler.getMaxFormContentSize();
- int maxFormKeys = contextHandler.getMaxFormKeys();
+ _parts = futureServletMultiPartFormData.get();
- _parts = ServletMultiPartFormData.from(this, maxFormKeys);
- Collection parts = _parts.getParts();
+ Collection parts = _parts.getParts();
- String formCharset = null;
- Part charsetPart = _parts.getPart("_charset_");
- if (charsetPart != null)
- {
- try (InputStream is = charsetPart.getInputStream())
+ String formCharset = null;
+ Part charsetPart = _parts.getPart("_charset_");
+ if (charsetPart != null)
{
- formCharset = IO.toString(is, StandardCharsets.UTF_8);
+ try (InputStream is = charsetPart.getInputStream())
+ {
+ formCharset = IO.toString(is, StandardCharsets.UTF_8);
+ }
}
- }
- /*
- Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
+ /*
+ Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
1. Use the part specific charset as provided in that part's Content-Type header; else
2. Use the overall default charset. Determined by:
a. if part name _charset_ exists, use that part's value.
@@ -523,38 +516,66 @@ public Collection getParts() throws IOException, ServletException
(note, this can be either from the charset field on the request Content-Type
header, or from a manual call to request.setCharacterEncoding())
c. use utf-8.
- */
- Charset defaultCharset;
- if (formCharset != null)
- defaultCharset = Charset.forName(formCharset);
- else if (getCharacterEncoding() != null)
- defaultCharset = Charset.forName(getCharacterEncoding());
- else
- defaultCharset = StandardCharsets.UTF_8;
+ */
+ Charset defaultCharset;
+ if (formCharset != null)
+ defaultCharset = Charset.forName(formCharset);
+ else if (getCharacterEncoding() != null)
+ defaultCharset = Charset.forName(getCharacterEncoding());
+ else
+ defaultCharset = StandardCharsets.UTF_8;
- long formContentSize = 0;
- for (Part p : parts)
- {
- if (p.getSubmittedFileName() == null)
- {
- formContentSize = Math.addExact(formContentSize, p.getSize());
- if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
- throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
+ // Recheck some constraints here, just in case the preloaded parts were not properly configured.
+ ServletContextHandler servletContextHandler = getServletRequestInfo().getServletContext().getServletContextHandler();
+ long maxFormContentSize = servletContextHandler.getMaxFormContentSize();
+ int maxFormKeys = servletContextHandler.getMaxFormKeys();
- // Servlet Spec 3.0 pg 23, parts without filename must be put into params.
- String charset = null;
- if (p.getContentType() != null)
- charset = MimeTypes.getCharsetFromContentType(p.getContentType());
+ long formContentSize = 0;
+ int count = 0;
+ for (Part p : parts)
+ {
+ if (maxFormKeys > 0 && ++count > maxFormKeys)
+ throw new IllegalStateException("Too many form keys > " + maxFormKeys);
- try (InputStream is = p.getInputStream())
+ if (p.getSubmittedFileName() == null)
{
- String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
- if (_contentParameters == null)
- _contentParameters = new Fields();
- _contentParameters.add(p.getName(), content);
+ formContentSize = Math.addExact(formContentSize, p.getSize());
+ if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
+ throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
+
+ // Servlet Spec 3.0 pg 23, parts without filename must be put into params.
+ String charset = null;
+ if (p.getContentType() != null)
+ charset = MimeTypes.getCharsetFromContentType(p.getContentType());
+
+ try (InputStream is = p.getInputStream())
+ {
+ String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
+ if (_contentParameters == null)
+ _contentParameters = new Fields();
+ _contentParameters.add(p.getName(), content);
+ }
}
}
}
+ catch (Throwable t)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("getParts", t);
+
+ Throwable cause;
+ if (t instanceof ExecutionException ee)
+ cause = ee.getCause();
+ else if (t instanceof ServletException se)
+ cause = se.getCause();
+ else
+ cause = t;
+
+ if (cause instanceof IOException ioException)
+ throw ioException;
+
+ throw new ServletException(new BadMessageException("bad multipart", cause));
+ }
}
return _parts.getParts();
@@ -654,6 +675,7 @@ public Object getAttribute(String name)
if (_async != null)
{
// This switch works by allowing the attribute to get underneath any dispatch wrapper.
+ // Note that there are further servlet specific attributes in ServletContextRequest
return switch (name)
{
case AsyncContext.ASYNC_REQUEST_URI -> getRequestURI();
@@ -867,13 +889,24 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
{
getParts();
}
- catch (IOException | ServletException e)
+ catch (IOException e)
{
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
+ catch (ServletException e)
+ {
+ Throwable cause = e.getCause();
+ if (cause instanceof BadMessageException badMessageException)
+ throw badMessageException;
+
+ String msg = "Unable to extract content parameters";
+ if (LOG.isDebugEnabled())
+ LOG.debug(msg, e);
+ throw new RuntimeIOException(msg, e);
+ }
}
else
{
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java
index b09fc4106511..17da4f12ff6e 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java
@@ -32,6 +32,7 @@
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.pathmap.MatchedResource;
+import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.SecureRequestCustomizer;
@@ -239,6 +240,9 @@ public Object getAttribute(String name)
case "jakarta.servlet.request.key_size" -> super.getAttribute(SecureRequestCustomizer.KEY_SIZE_ATTRIBUTE);
case "jakarta.servlet.request.ssl_session_id" -> super.getAttribute(SecureRequestCustomizer.SSL_SESSION_ID_ATTRIBUTE);
case "jakarta.servlet.request.X509Certificate" -> super.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE);
+ case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> _matchedResource.getResource().getServletHolder().getMultipartConfigElement();
+ case FormFields.MAX_FIELDS_ATTRIBUTE -> getServletContext().getServletContextHandler().getMaxFormKeys();
+ case FormFields.MAX_LENGTH_ATTRIBUTE -> getServletContext().getServletContextHandler().getMaxFormContentSize();
default -> super.getAttribute(name);
};
}
@@ -255,6 +259,12 @@ public Set getAttributeNameSet()
names.add("jakarta.servlet.request.ssl_session_id");
if (names.contains(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE))
names.add("jakarta.servlet.request.X509Certificate");
+ if (_matchedResource.getResource().getServletHolder().getMultipartConfigElement() != null)
+ names.add(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
+ if (getServletContext().getServletContextHandler().getMaxFormKeys() >= 0)
+ names.add(FormFields.MAX_FIELDS_ATTRIBUTE);
+ if (getServletContext().getServletContextHandler().getMaxFormContentSize() >= 0L)
+ names.add(FormFields.MAX_FIELDS_ATTRIBUTE);
return names;
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHolder.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHolder.java
index a3079dc50cc3..a290c7d8d8b4 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHolder.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletHolder.java
@@ -74,7 +74,7 @@ public class ServletHolder extends Holder implements Comparable _roleMap;
private String _forcedPath;
private String _runAsRole;
- private ServletRegistration.Dynamic _registration;
+ private ServletHolder.Registration _registration;
private JspContainer _jspContainer;
private volatile Servlet _servlet;
@@ -155,6 +155,11 @@ public ServletHolder(Class extends Servlet> servlet)
setHeldClass(servlet);
}
+ public MultipartConfigElement getMultipartConfigElement()
+ {
+ return _registration == null ? null : _registration.getMultipartConfigElement();
+ }
+
/**
* @return The unavailable exception or null if not unavailable
*/
@@ -710,14 +715,6 @@ protected void prepare(ServletRequest request, ServletResponse response) throws
{
// Ensure the servlet is initialized prior to any filters being invoked
getServlet();
-
- // Check for multipart config
- if (_registration != null)
- {
- MultipartConfigElement mpce = ((Registration)_registration).getMultipartConfig();
- if (mpce != null)
- request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, mpce);
- }
}
/**
@@ -919,7 +916,7 @@ public String getServletName()
public class Registration extends HolderRegistration implements ServletRegistration.Dynamic
{
- protected MultipartConfigElement _multipartConfig;
+ protected MultipartConfigElement _multipartConfigElement;
@Override
public Set addMapping(String... urlPatterns)
@@ -994,12 +991,12 @@ public int getInitOrder()
@Override
public void setMultipartConfig(MultipartConfigElement element)
{
- _multipartConfig = element;
+ _multipartConfigElement = element;
}
- public MultipartConfigElement getMultipartConfig()
+ public MultipartConfigElement getMultipartConfigElement()
{
- return _multipartConfig;
+ return _multipartConfigElement;
}
@Override
@@ -1015,7 +1012,7 @@ public Set setServletSecurity(ServletSecurityElement securityElement)
}
}
- public ServletRegistration.Dynamic getRegistration()
+ public ServletHolder.Registration getRegistration()
{
if (_registration == null)
_registration = new Registration();
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
index 3ea59caec4a8..4d1443a9454e 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
@@ -16,31 +16,32 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.ByteBuffer;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import jakarta.servlet.MultipartConfigElement;
-import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Part;
+import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
-import org.eclipse.jetty.io.RetainableByteBuffer;
+import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.ConnectionMetaData;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
/**
* Servlet specific class for multipart content support.
- * Use {@link #from(ServletApiRequest)} to
+ *
Use {@link #from(ServletRequest)} to
* parse multipart request content into a {@link Parts} object that can
* be used to access Servlet {@link Part} objects.
*
@@ -49,106 +50,103 @@
public class ServletMultiPartFormData
{
/**
- * Parses the request content assuming it is a multipart content,
- * and returns a {@link Parts} objects that can be used to access
- * individual {@link Part}s.
- *
- * @param request the HTTP request with multipart content
- * @return a {@link Parts} object to access the individual {@link Part}s
- * @throws IOException if reading the request content fails
- * @see org.eclipse.jetty.server.handler.DelayedHandler
+ * Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
+ * @param servletRequest A servlet request
+ * @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
+ * @see #from(ServletRequest, String)
*/
- public static Parts from(ServletApiRequest request) throws IOException
+ public static CompletableFuture from(ServletRequest servletRequest)
{
- return from(request, ServletContextHandler.DEFAULT_MAX_FORM_KEYS);
+ return from(servletRequest, servletRequest.getContentType());
}
/**
- * Parses the request content assuming it is a multipart content,
- * and returns a {@link Parts} objects that can be used to access
- * individual {@link Part}s.
- *
- * @param request the HTTP request with multipart content
- * @return a {@link Parts} object to access the individual {@link Part}s
- * @throws IOException if reading the request content fails
- * @see org.eclipse.jetty.server.handler.DelayedHandler
+ * Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
+ * @param servletRequest A servlet request
+ * @param contentType The contentType, passed as an optimization as it has likely already been retrieved.
+ * @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
*/
- public static Parts from(ServletApiRequest request, int maxParts) throws IOException
+ public static CompletableFuture from(ServletRequest servletRequest, String contentType)
{
- try
+ // Look for an existing future (we use the future here rather than the parts as it can remember any failure).
+ @SuppressWarnings("unchecked")
+ CompletableFuture futureServletParts = (CompletableFuture)servletRequest.getAttribute(ServletMultiPartFormData.class.getName());
+ if (futureServletParts == null)
{
- // Look for a previously read and parsed MultiPartFormData from the DelayedHandler.
- MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
- if (parts != null)
- return new Parts(parts);
+ // No existing parts, so we need to try to read them ourselves
- // TODO set the files directory
- return new ServletMultiPartFormData().parse(request, maxParts);
- }
- catch (Throwable x)
- {
- throw IO.rethrow(x);
- }
- }
+ // Is this servlet a valid target for Multipart?
+ MultipartConfigElement config = (MultipartConfigElement)servletRequest.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
+ if (config == null)
+ return CompletableFuture.failedFuture(new IllegalStateException("No multipart configuration element"));
- private Parts parse(ServletApiRequest request, int maxParts) throws IOException
- {
- MultipartConfigElement config = (MultipartConfigElement)request.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
- if (config == null)
- throw new IllegalStateException("No multipart configuration element");
-
- String boundary = MultiPart.extractBoundary(request.getContentType());
- if (boundary == null)
- throw new IllegalStateException("No multipart boundary parameter in Content-Type");
-
- // Store MultiPartFormData as attribute on request so it is released by the HttpChannel.
- MultiPartFormData formData = new MultiPartFormData(boundary);
- formData.setMaxParts(maxParts);
-
- File tmpDirFile = (File)request.getServletContext().getAttribute(ServletContext.TEMPDIR);
- if (tmpDirFile == null)
- tmpDirFile = new File(System.getProperty("java.io.tmpdir"));
- String fileLocation = config.getLocation();
- if (!StringUtil.isBlank(fileLocation))
- tmpDirFile = new File(fileLocation);
-
- formData.setFilesDirectory(tmpDirFile.toPath());
- formData.setMaxMemoryFileSize(config.getFileSizeThreshold());
- formData.setMaxFileSize(config.getMaxFileSize());
- formData.setMaxLength(config.getMaxRequestSize());
- ConnectionMetaData connectionMetaData = request.getRequest().getConnectionMetaData();
- formData.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
-
- ByteBufferPool byteBufferPool = request.getRequest().getComponents().getByteBufferPool();
- Connection connection = connectionMetaData.getConnection();
- int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
- InputStream input = request.getInputStream();
- while (!formData.isDone())
- {
- RetainableByteBuffer retainable = byteBufferPool.acquire(bufferSize, false);
- boolean readEof = false;
- ByteBuffer buffer = retainable.getByteBuffer();
- while (BufferUtil.space(buffer) > bufferSize / 2)
+ // Are we the right content type to produce our own parts?
+ if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
+ return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
+
+ // Do we have a boundary?
+ String boundary = MultiPart.extractBoundary(servletRequest.getContentType());
+ if (boundary == null)
+ return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
+
+ // Can we access the core request, needed for components (eg buffer pools, temp directory, etc.) as well
+ // as IO optimization
+ ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(servletRequest);
+ if (servletContextRequest == null)
+ return CompletableFuture.failedFuture(new IllegalStateException("No core request"));
+
+ // Get a temporary directory for larger parts.
+ File filesDirectory = StringUtil.isBlank(config.getLocation())
+ ? servletContextRequest.getContext().getTempDirectory()
+ : new File(config.getLocation());
+
+ // Look for an existing future MultiPartFormData.Parts
+ CompletableFuture futureFormData = MultiPartFormData.from(servletContextRequest, boundary, parser ->
{
- int read = BufferUtil.readFrom(input, buffer);
- if (read < 0)
+ try
{
- readEof = true;
- break;
+ // No existing core parts, so we need to configure the parser.
+ ServletContextHandler contextHandler = servletContextRequest.getServletContext().getServletContextHandler();
+ ByteBufferPool byteBufferPool = servletContextRequest.getComponents().getByteBufferPool();
+ ConnectionMetaData connectionMetaData = servletContextRequest.getConnectionMetaData();
+ Connection connection = connectionMetaData.getConnection();
+
+ Content.Source source;
+ if (servletRequest instanceof ServletApiRequest servletApiRequest)
+ {
+ source = servletApiRequest.getRequest();
+ }
+ else
+ {
+ int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
+ InputStreamContentSource iscs = new InputStreamContentSource(servletRequest.getInputStream(), byteBufferPool);
+ iscs.setBufferSize(bufferSize);
+ source = iscs;
+ }
+
+ parser.setMaxParts(contextHandler.getMaxFormKeys());
+ parser.setFilesDirectory(filesDirectory.toPath());
+ parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
+ parser.setMaxFileSize(config.getMaxFileSize());
+ parser.setMaxLength(config.getMaxRequestSize());
+ parser.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
+
+ // parse the core parts.
+ return parser.parse(source);
}
- }
+ catch (Throwable failure)
+ {
+ return CompletableFuture.failedFuture(failure);
+ }
+ });
- formData.parse(Content.Chunk.from(buffer, false, retainable::release));
- if (readEof)
- {
- formData.parse(Content.Chunk.EOF);
- break;
- }
- }
+ // When available, convert the core parts to servlet parts
+ futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory.toPath(), formDataParts));
- Parts parts = new Parts(formData.join());
- request.setAttribute(Parts.class.getName(), parts);
- return parts;
+ // cache the result in attributes.
+ servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts);
+ }
+ return futureServletParts;
}
/**
@@ -158,9 +156,9 @@ public static class Parts
{
private final List parts = new ArrayList<>();
- public Parts(MultiPartFormData.Parts parts)
+ public Parts(Path directory, MultiPartFormData.Parts parts)
{
- parts.forEach(part -> this.parts.add(new ServletPart(parts.getMultiPartFormData(), part)));
+ parts.forEach(part -> this.parts.add(new ServletPart(directory, part)));
}
public Part getPart(String name)
@@ -179,12 +177,12 @@ public Collection getParts()
private static class ServletPart implements Part
{
- private final MultiPartFormData _formData;
+ private final Path _directory;
private final MultiPart.Part _part;
- private ServletPart(MultiPartFormData formData, MultiPart.Part part)
+ private ServletPart(Path directory, MultiPart.Part part)
{
- _formData = formData;
+ _directory = directory;
_part = part;
}
@@ -222,8 +220,8 @@ public long getSize()
public void write(String fileName) throws IOException
{
Path filePath = Path.of(fileName);
- if (!filePath.isAbsolute())
- filePath = _formData.getFilesDirectory().resolve(filePath).normalize();
+ if (!filePath.isAbsolute() && Files.isDirectory(_directory))
+ filePath = _directory.resolve(filePath).normalize();
_part.writeTo(filePath);
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
index 2ccbbde45b93..9e46e56f6789 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
@@ -17,7 +17,6 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
-import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@@ -51,9 +50,9 @@
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
-import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -62,7 +61,8 @@
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.ValueSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -93,32 +93,27 @@ public void before() throws Exception
tmpDirString = tmpDir.toAbsolutePath().toString();
}
- private void start(HttpServlet servlet) throws Exception
+ private void start(HttpServlet servlet, MultipartConfigElement config, boolean eager) throws Exception
{
- start(servlet, new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0));
- }
-
- private void start(HttpServlet servlet, MultipartConfigElement config) throws Exception
- {
- start(servlet, config, null);
- }
-
- private void start(HttpServlet servlet, MultipartConfigElement config, ByteBufferPool bufferPool) throws Exception
- {
- server = new Server(null, null, bufferPool);
+ config = config == null ? new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config;
+ server = new Server(null, null, null);
connector = new ServerConnector(server);
server.addConnector(connector);
- ServletContextHandler contextHandler = new ServletContextHandler("/");
+ ServletContextHandler servletContextHandler = new ServletContextHandler("/");
ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.getRegistration().setMultipartConfig(config);
- contextHandler.addServlet(servletHolder, "/");
+ servletContextHandler.addServlet(servletHolder, "/");
+ server.setHandler(servletContextHandler);
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes("multipart/form-data");
gzipHandler.setMinGzipSize(32);
- gzipHandler.setHandler(contextHandler);
- server.setHandler(gzipHandler);
+
+ if (eager)
+ gzipHandler.setHandler(new EagerFormHandler());
+
+ servletContextHandler.insertHandler(gzipHandler);
server.start();
@@ -134,17 +129,18 @@ public void stop() throws Exception
IO.delete(tmpDir.toFile());
}
- @Test
- public void testLargePart() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testLargePart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
- protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ protected void service(HttpServletRequest req, HttpServletResponse resp)
{
req.getParameterMap();
}
- }, new MultipartConfigElement(tmpDirString));
+ }, new MultipartConfigElement(tmpDirString), eager);
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
@@ -170,22 +166,23 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
assert400orEof(listener, responseContent ->
{
- assertThat(responseContent, containsString("Unable to parse form content"));
- assertThat(responseContent, containsString("Form is larger than max length"));
+ assertThat(responseContent, containsString("400: bad"));
+ assertThat(responseContent, containsString("Form is larger than max length"));
});
}
- @Test
- public void testManyParts() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testManyParts(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
- protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ protected void service(HttpServletRequest req, HttpServletResponse resp)
{
req.getParameterMap();
}
- }, new MultipartConfigElement(tmpDirString));
+ }, new MultipartConfigElement(tmpDirString), eager);
byte[] byteArray = new byte[1024];
Arrays.fill(byteArray, (byte)1);
@@ -208,13 +205,14 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
assert400orEof(listener, responseContent ->
{
- assertThat(responseContent, containsString("Unable to parse form content"));
+ assertThat(responseContent, containsString("400: bad"));
assertThat(responseContent, containsString("Form with too many keys"));
});
}
- @Test
- public void testMaxRequestSize() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testMaxRequestSize(boolean eager) throws Exception
{
start(new HttpServlet()
{
@@ -223,7 +221,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
{
req.getParameterMap();
}
- }, new MultipartConfigElement(tmpDirString, -1, 1024, 1024 * 1024 * 8));
+ }, new MultipartConfigElement(tmpDirString, -1, 1024, 1024 * 1024 * 8), eager);
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
@@ -282,13 +280,14 @@ private static void assert400orEof(InputStreamResponseListener listener, Consume
checkbody.accept(responseContent);
}
- @Test
- public void testSimpleMultiPart() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testSimpleMultiPart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
- protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ protected void service(HttpServletRequest request, HttpServletResponse response1) throws ServletException, IOException
{
Collection parts = request.getParts();
assertNotNull(parts);
@@ -298,10 +297,10 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
Collection headerNames = part.getHeaderNames();
assertNotNull(headerNames);
assertEquals(2, headerNames.size());
- String content = IO.toString(part.getInputStream(), UTF_8);
- assertEquals("content1", content);
+ String content1 = IO.toString(part.getInputStream(), UTF_8);
+ assertEquals("content1", content1);
}
- });
+ }, null, eager);
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
@@ -333,21 +332,23 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
}
}
- @Test
- public void testTempFilesDeletedOnError() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testTempFilesDeletedOnError(boolean eager) throws Exception
{
byte[] bytes = new byte[2 * MAX_FILE_SIZE];
Arrays.fill(bytes, (byte)1);
+ // Should throw as the max file size is exceeded.
start(new HttpServlet()
{
@Override
- protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ protected void service(HttpServletRequest request, HttpServletResponse response1) throws ServletException, IOException
{
// Should throw as the max file size is exceeded.
request.getParts();
}
- });
+ }, null, eager);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("largePart", "largeFile.bin", HttpFields.EMPTY, new BytesRequestContent(bytes)));
@@ -361,7 +362,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
.body(multiPart)
.send();
- assertEquals(500, response.getStatus());
+ assertEquals(400, response.getStatus());
assertThat(response.getContentAsString(), containsString("max file size exceeded"));
}
@@ -370,18 +371,34 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
assertThat(fileList.length, is(0));
}
- @Test
- public void testMultiPartGzip() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testMultiPartGzip(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
- protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
+ protected void service(HttpServletRequest request, HttpServletResponse response1) throws IOException, ServletException
{
- response.setContentType(request.getContentType());
- IO.copy(request.getInputStream(), response.getOutputStream());
+ String contentType1 = request.getContentType();
+ response1.setContentType(contentType1);
+ response1.flushBuffer();
+
+ MultiPartRequestContent echoParts = new MultiPartRequestContent(MultiPart.extractBoundary(contentType1));
+ Collection servletParts = request.getParts();
+ for (Part part : servletParts)
+ {
+ HttpFields.Mutable partHeaders = HttpFields.build();
+ for (String h1 : part.getHeaderNames())
+ partHeaders.add(h1, part.getHeader(h1));
+
+ echoParts.addPart(new MultiPart.ContentSourcePart(part.getName(), part.getSubmittedFileName(), partHeaders, new InputStreamContentSource(part.getInputStream())));
+ }
+ echoParts.close();
+ IO.copy(Content.Source.asInputStream(echoParts), response1.getOutputStream());
}
- });
+ }, null, eager);
+
// Do not automatically handle gzip.
client.getContentDecoderFactories().clear();
@@ -409,19 +426,18 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
String boundary = MultiPart.extractBoundary(contentType);
- MultiPartFormData formData = new MultiPartFormData(boundary);
- formData.setMaxParts(1);
-
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
- formData.parse(Content.Chunk.from(ByteBuffer.wrap(IO.readBytes(inputStream)), true));
- MultiPartFormData.Parts parts = formData.join();
+ MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
+ formData.setMaxParts(1);
+ MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
assertThat(parts.size(), is(1));
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));
}
- @Test
- public void testDoubleReadFromPart() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testDoubleReadFromPart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@@ -435,7 +451,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
resp.getWriter().println("Part: name=" + part.getName() + ", size=" + part.getSize() + ", content=" + IO.toString(part.getInputStream()));
}
}
- });
+ }, null, eager);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
@@ -455,8 +471,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
}
- @Test
- public void testPartAsParameter() throws Exception
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testPartAsParameter(boolean eager) throws Exception
{
start(new HttpServlet()
{
@@ -471,7 +488,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
resp.getWriter().println("Parameter: " + entry.getKey() + "=" + entry.getValue()[0]);
}
}
- });
+ }, null, eager);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/StandardDescriptorProcessor.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/StandardDescriptorProcessor.java
index 0e4e9f8d2b03..75a8ec270979 100644
--- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/StandardDescriptorProcessor.java
+++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/StandardDescriptorProcessor.java
@@ -588,7 +588,7 @@ public void visitServlet(WebAppContext context, Descriptor descriptor, XmlParser
case WebFragment:
{
//another fragment set the value, this fragment's values must match exactly or it is an error
- MultipartConfigElement cfg = ((ServletHolder.Registration)holder.getRegistration()).getMultipartConfig();
+ MultipartConfigElement cfg = holder.getRegistration().getMultipartConfigElement();
if (cfg.getMaxFileSize() != element.getMaxFileSize())
throw new IllegalStateException("Conflicting multipart-config max-file-size for servlet " + name + " in " + descriptor.getURI());